SPB Git forge

spb/ka-ios

Public
20commits 1branches 0releases
17.2 MBsize
maindefault branch
28 days agolast push
Swift 100%
13.4 KB · 289 lines swift
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// ProfileView.swift — profil : KA ID (compte unique de l'écosystème), univers3// favoris, apparence, contacts complets du Groupe KA, pages légales, à propos.4import SwiftUI56struct ProfileView: View {7    @AppStorage("ka.favUniverses") private var favUniversesRaw = ""8    @AppStorage("ka.appearance") private var appearance = "clair"9    @Environment(\.colorScheme) private var scheme10    @StateObject private var kaid = KAIDManager.shared11    @EnvironmentObject private var favorites: FavoritesStore12    @EnvironmentObject private var recents: RecentsStore13    @StateObject private var perso = PersonalizationStore.shared14    @AppStorage(PersonalizationStore.enabledKey) private var persoEnabled = true15    @State private var showHubSite = false16    @State private var confirmReset = false1718    private var favIDs: Set<String> {19        Set(favUniversesRaw.split(separator: ",").map(String.init))20    }2122    var body: some View {23        NavigationStack {24            List {25                Section {26                    if let p = kaid.profile {27                        KAIDCard(profile: p)28                            .listRowInsets(EdgeInsets())29                            .listRowBackground(Color.clear)30                        Link(destination: Ecosystem.hubURL.appendingPathComponent("/compte")) {31                            Label("Gérer mon profil sur groupe-ka.com", systemImage: "person.text.rectangle")32                        }33                        Button(role: .destructive) { kaid.logout() } label: {34                            Label("Se déconnecter", systemImage: "rectangle.portrait.and.arrow.right")35                        }36                    } else {37                        HStack(spacing: 14) {38                            ZStack {39                                RoundedRectangle(cornerRadius: 14, style: .continuous)40                                    .fill(KATheme.inkLight)41                                    .frame(width: 56, height: 56)42                                Text("KA").font(.system(size: 22, weight: .bold, design: .rounded))43                                    .foregroundStyle(KATheme.lime)44                                    .rotationEffect(.degrees(-4))45                            }46                            VStack(alignment: .leading, spacing: 3) {47                                Text("Votre KA ID").font(.headline)48                                Text("Un seul compte pour les 13 plateformes de l'écosystème.")49                                    .font(.caption).foregroundStyle(.secondary)50                            }51                        }52                        Button {53                            kaid.login()54                        } label: {55                            HStack {56                                Label("Se connecter avec KA ID", systemImage: "person.crop.circle.badge.checkmark")57                                Spacer()58                                if kaid.busy { ProgressView() }59                            }60                        }61                        .disabled(kaid.busy)62                        Link(destination: Ecosystem.signupURL) {63                            Label("Créer un compte sur groupe-ka.com", systemImage: "arrow.up.right.square")64                        }65                        if let e = kaid.lastError {66                            Text(e).font(.caption).foregroundStyle(.red)67                        }68                    }69                }7071                Section("Mes contenus") {72                    NavigationLink { FavoritesView() } label: {73                        Label("Favoris & collections", systemImage: "heart.fill")74                            .badge(favorites.allItems.count)75                    }76                    NavigationLink { HistoryView() } label: {77                        Label("Historique consulté", systemImage: "clock.arrow.circlepath")78                            .badge(recents.viewed.count)79                    }80                    NavigationLink { SavedSearchesView() } label: {81                        Label("Recherches sauvegardées & alertes", systemImage: "bell.badge")82                            .badge(recents.alertCount > 0 ? "\(recents.alertCount) nouv." : "")83                    }84                }8586                Section {87                    Toggle(isOn: $persoEnabled) {88                        Label("Recommandations personnalisées", systemImage: "sparkles")89                    }90                    if persoEnabled, perso.profile.hasSignals {91                        let p = perso.profile92                        if !p.affinity.isEmpty {93                            LabeledContent("Univers suivis",94                                           value: p.topUniverses(3)95                                               .compactMap { Ecosystem.universe($0)?.name }96                                               .joined(separator: ", "))97                        }98                        if !p.topCities.isEmpty {99                            LabeledContent("Villes", value: p.topCities.prefix(3).joined(separator: ", "))100                        }101                        LabeledContent("Signaux enregistrés", value: "\(p.eventCount)")102                    }103                    Button(role: .destructive) { confirmReset = true } label: {104                        Label("Effacer ce que KA a appris", systemImage: "trash")105                    }106                    .disabled(perso.events.isEmpty)107                } header: {108                    Text("Personnalisation")109                } footer: {110                    Text("Vos consultations, favoris et recherches personnalisent l'accueil et le feed « Pour vous ». Tout reste sur cet appareil — rien n'est envoyé aux serveurs.")111                }112113                Section {114                    ForEach(Ecosystem.all) { u in115                        Button {116                            var ids = favIDs117                            if ids.contains(u.id) { ids.remove(u.id) } else { ids.insert(u.id) }118                            favUniversesRaw = ids.sorted().joined(separator: ",")119                            Haptics.tap()120                        } label: {121                            HStack {122                                KALogo(universe: u, size: 26)123                                Text(u.name).foregroundStyle(.primary)124                                Spacer()125                                if favIDs.contains(u.id) {126                                    Image(systemName: "star.fill").foregroundStyle(.yellow)127                                }128                            }129                        }130                    }131                } header: {132                    Text("Mes univers favoris")133                } footer: {134                    Text("Vos univers favoris remontent en tête de l'accueil.")135                }136137                Section("Apparence") {138                    Picker("Thème", selection: $appearance) {139                        Text("Automatique").tag("auto")140                        Text("Clair").tag("clair")141                        Text("Sombre").tag("sombre")142                    }143                }144145                Section("Joindre le Groupe KA") {146                    ForEach(Ecosystem.contacts, id: \.email) { c in147                        Link(destination: URL(string: "mailto:\(c.email)")!) {148                            VStack(alignment: .leading, spacing: 2) {149                                Text(c.email).font(.subheadline.weight(.semibold))150                                Text(c.role).font(.caption).foregroundStyle(.secondary)151                            }152                        }153                    }154                    Button { showHubSite = true } label: {155                        HStack(spacing: 10) {156                            KALogo(assetID: "groupe-ka", size: 22)157                            Text("groupe-ka.com — le portail")158                        }159                    }160                    Link(destination: Ecosystem.statusURL) {161                        Label("État des services en direct", systemImage: "waveform.path.ecg")162                    }163                }164165                Section("Légal") {166                    ForEach(Ecosystem.legal, id: \.path) { l in167                        Link(destination: Ecosystem.hubURL.appendingPathComponent(l.path)) {168                            Text(l.label)169                        }170                    }171                    Text(Ecosystem.disclaimer)172                        .font(.caption2).foregroundStyle(.secondary)173                }174175                Section("À propos") {176                    LabeledContent("Version", value: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0")177                    LabeledContent("Écosystème", value: "\(Ecosystem.all.count) univers")178                    Text("KA — tout l'écosystème Groupe-KA dans votre poche. Fait au Québec. 💚")179                        .font(.caption).foregroundStyle(.secondary)180                }181            }182            .navigationTitle("Profil")183            .scrollContentBackground(.hidden)184            .background(KATheme.paper(scheme))185            .fullScreenCover(isPresented: $showHubSite) {186                SiteBrowserCover(title: "Groupe KA", accent: KATheme.green,187                                 logoID: "groupe-ka", url: Ecosystem.hubURL)188            }189            .confirmationDialog("Effacer les données d'apprentissage ?",190                                isPresented: $confirmReset, titleVisibility: .visible) {191                Button("Tout effacer", role: .destructive) {192                    perso.reset()193                    Haptics.success()194                }195            } message: {196                Text("Le feed « Pour vous » repartira de zéro. Vos favoris et votre historique ne sont pas touchés.")197            }198        }199    }200}201202203// MARK: - Historique consulté204205struct HistoryView: View {206    @EnvironmentObject private var recents: RecentsStore207    @Environment(\.colorScheme) private var scheme208    var body: some View {209        ScrollView {210            LazyVStack(spacing: 10) {211                if recents.viewed.isEmpty {212                    KAEmptyState(symbol: "clock", title: "Rien de consulté encore",213                                 message: "Les fiches que vous ouvrez apparaîtront ici.")214                }215                ForEach(recents.viewed) { item in216                    NavigationLink(value: item) { KAItemRow(item: item) }217                        .buttonStyle(KAPressStyle())218                }219            }220            .padding(14)221        }222        .background(KATheme.paper(scheme))223        .navigationTitle("Historique")224        .navigationDestination(for: KAItem.self) { ItemDetailView(item: $0) }225        .toolbar {226            if !recents.viewed.isEmpty {227                ToolbarItem(placement: .topBarTrailing) {228                    Button("Effacer") { recents.clearHistory() }229                }230            }231        }232    }233}234235// MARK: - Recherches sauvegardées & alertes236237struct SavedSearchesView: View {238    @EnvironmentObject private var recents: RecentsStore239    @Environment(\.colorScheme) private var scheme240    var body: some View {241        List {242            if recents.savedSearches.isEmpty {243                KAEmptyState(symbol: "bell.slash", title: "Aucune recherche sauvegardée",244                             message: "Dans Recherche, touchez 🔔 pour suivre une recherche : KA recomptera les résultats et affichera les nouveautés.")245                    .listRowBackground(Color.clear)246            }247            ForEach($recents.savedSearches) { $s in248                VStack(alignment: .leading, spacing: 6) {249                    HStack {250                        Text(s.name).font(.headline)251                        if s.newCount > 0 {252                            Text("+\(s.newCount) nouveautés")253                                .font(.system(size: 10, design: .monospaced).weight(.bold))254                                .padding(.horizontal, 7).padding(.vertical, 3)255                                .background(.red, in: Capsule()).foregroundStyle(.white)256                        }257                        Spacer()258                        Toggle("", isOn: $s.alertsOn).labelsHidden()259                            .accessibilityLabel("Alerte pour \(s.name)")260                    }261                    HStack(spacing: 8) {262                        if let u = Ecosystem.universe(s.universeID) { KAChip(text: u.wordmark, accent: u.accent) }263                        Text("« \(s.query) »").font(.caption).foregroundStyle(.secondary)264                        Spacer()265                        if let total = s.lastTotal {266                            Text("\(total + s.newCount) résultats")267                                .font(.system(.caption2, design: .monospaced))268                                .foregroundStyle(.tertiary)269                        }270                    }271                    if s.newCount > 0 {272                        Button("Marquer comme vues") { recents.markSeen(s.id) }273                            .font(.caption.weight(.semibold))274                    }275                }276                .swipeActions {277                    Button(role: .destructive) { recents.remove(s.id) } label: {278                        Label("Supprimer", systemImage: "trash")279                    }280                }281            }282        }283        .navigationTitle("Recherches & alertes")284        .refreshable { await recents.refreshAlerts() }285        .scrollContentBackground(.hidden)286        .background(KATheme.paper(scheme))287    }288}289