SPB Git forge

spb/ka-macos

Public
3commits 1branches 0releases
6.1 MBsize
maindefault branch
1 mo agolast push
Swift 98.3% Shell 1.7%
12.4 KB · 290 lines swift
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// LibraryViews.swift — la bibliothèque personnelle : Favoris par collections3// (multi-univers), Historique consulté, Recherches sauvegardées & ALERTES4// honnêtes (deltas réels recomptés via l'API). Stores repris de l'app iOS KA.5import SwiftUI67// MARK: - Favoris (collections)89struct FavoritesView: View {10    @EnvironmentObject private var favorites: FavoritesStore11    @Environment(\.colorScheme) private var scheme12    @State private var selectedCollection: UUID?13    @State private var selected: KAItem?14    @State private var newName = ""1516    private var current: FavoritesStore.FavCollection? {17        favorites.collections.first { $0.id == selectedCollection } ?? favorites.collections.first18    }1920    var body: some View {21        VStack(spacing: 0) {22            header23            Divider().opacity(0.4)24            HStack(spacing: 0) {25                grid26                    .frame(maxWidth: .infinity)27                Divider().opacity(0.4)28                Group {29                    if let item = selected {30                        ItemDetailPane(item: item)31                    } else {32                        KAEmptyState(symbol: "heart",33                                     title: "Vos favoris, tous univers confondus",34                                     message: "Un 4½, une auto et un resto peuvent vivre dans la même collection.")35                            .frame(maxHeight: .infinity)36                            .background(KATheme.paper(scheme))37                    }38                }39                .frame(width: 440)40            }41        }42    }4344    private var header: some View {45        HStack(spacing: 10) {46            Text("Favoris").font(.title2.weight(.bold))47            Picker("Collection", selection: Binding(48                get: { selectedCollection ?? favorites.collections.first?.id ?? UUID() },49                set: { selectedCollection = $0; selected = nil }50            )) {51                ForEach(favorites.collections) { c in52                    Text("\(c.name) (\(c.items.count))").tag(c.id)53                }54            }55            .labelsHidden()56            .frame(maxWidth: 260)57            if let c = current, favorites.collections.count > 1 {58                Button {59                    favorites.removeCollection(c.id)60                    selectedCollection = favorites.collections.first?.id61                    selected = nil62                } label: {63                    Image(systemName: "trash")64                }65                .help("Supprimer la collection « \(c.name) » (les éléments sont retirés des favoris)")66            }67            Spacer()68            TextField("Nouvelle collection…", text: $newName)69                .textFieldStyle(.roundedBorder)70                .frame(maxWidth: 200)71                .onSubmit { addCollection() }72            Button("Créer") { addCollection() }73                .disabled(newName.trimmingCharacters(in: .whitespaces).isEmpty)74        }75        .padding(.horizontal, 16).padding(.vertical, 11)76    }7778    private var grid: some View {79        ScrollView {80            if let c = current, !c.items.isEmpty {81                LazyVGrid(columns: [GridItem(.adaptive(minimum: 230), spacing: 12)], spacing: 12) {82                    ForEach(c.items) { item in83                        Button { selected = item } label: {84                            KAItemCard(item: item, selected: selected?.id == item.id)85                        }86                        .buttonStyle(KAPressStyle())87                        .contextMenu {88                            Button("Retirer des favoris") {89                                favorites.toggle(item)90                                if selected?.id == item.id { selected = nil }91                            }92                            if favorites.collections.count > 1 {93                                Menu("Déplacer vers") {94                                    ForEach(favorites.collections.filter { $0.id != c.id }) { other in95                                        Button(other.name) { favorites.move(item, to: other.id) }96                                    }97                                }98                            }99                        }100                    }101                }102                .padding(14)103            } else {104                KAEmptyState(symbol: "heart.slash",105                             title: "Aucun favori ici",106                             message: "Cliquez le cœur sur n'importe quelle fiche pour la garder — consultable hors ligne.")107                    .padding(30)108            }109        }110        .background(KATheme.paper(scheme))111    }112113    private func addCollection() {114        favorites.addCollection(newName)115        newName = ""116        selectedCollection = favorites.collections.last?.id117    }118}119120// MARK: - Historique consulté121122struct HistoryView: View {123    @EnvironmentObject private var recents: RecentsStore124    @Environment(\.colorScheme) private var scheme125    @State private var selected: KAItem?126127    var body: some View {128        VStack(spacing: 0) {129            HStack {130                Text("Historique").font(.title2.weight(.bold))131                Text("\(recents.viewed.count) fiche\(recents.viewed.count > 1 ? "s" : "") consultée\(recents.viewed.count > 1 ? "s" : "")")132                    .font(.caption).foregroundStyle(.secondary)133                Spacer()134                Button("Effacer l'historique") { recents.clearHistory(); selected = nil }135                    .disabled(recents.viewed.isEmpty)136            }137            .padding(.horizontal, 16).padding(.vertical, 11)138            Divider().opacity(0.4)139            HStack(spacing: 0) {140                ScrollView {141                    if recents.viewed.isEmpty {142                        KAEmptyState(symbol: "clock.arrow.circlepath",143                                     title: "Rien encore",144                                     message: "Les fiches que vous consultez apparaîtront ici, consultables hors ligne.")145                            .padding(30)146                    } else {147                        LazyVGrid(columns: [GridItem(.adaptive(minimum: 230), spacing: 12)], spacing: 12) {148                            ForEach(recents.viewed) { item in149                                Button { selected = item } label: {150                                    KAItemCard(item: item, selected: selected?.id == item.id)151                                }152                                .buttonStyle(KAPressStyle())153                            }154                        }155                        .padding(14)156                    }157                }158                .background(KATheme.paper(scheme))159                .frame(maxWidth: .infinity)160                Divider().opacity(0.4)161                Group {162                    if let item = selected {163                        ItemDetailPane(item: item)164                    } else {165                        KAEmptyState(symbol: "clock",166                                     title: "Choisissez une fiche",167                                     message: "Votre historique reste local, sur ce Mac.")168                            .frame(maxHeight: .infinity)169                            .background(KATheme.paper(scheme))170                    }171                }172                .frame(width: 440)173            }174        }175    }176}177178// MARK: - Recherches sauvegardées & alertes179180struct AlertsView: View {181    @EnvironmentObject private var recents: RecentsStore182    @EnvironmentObject private var state: AppState183    @Environment(\.colorScheme) private var scheme184    @State private var refreshing = false185186    var body: some View {187        ScrollView {188            VStack(alignment: .leading, spacing: 14) {189                HStack {190                    Text("Recherches & alertes").font(.title2.weight(.bold))191                    Spacer()192                    Button {193                        refreshing = true194                        Task { await recents.refreshAlerts(); refreshing = false }195                    } label: {196                        if refreshing { ProgressView().controlSize(.small) }197                        else { Label("Recompter maintenant", systemImage: "arrow.clockwise").font(.caption) }198                    }199                    .disabled(refreshing || recents.savedSearches.isEmpty)200                }201                Text("KA recompte le total réel de chaque recherche via l'API du site et affiche le delta — jamais de faux chiffre. Sauvegardez une recherche depuis n'importe quel univers (bouton « Sauvegarder + alerte »).")202                    .font(.caption).foregroundStyle(.secondary)203204                if recents.savedSearches.isEmpty {205                    KAEmptyState(symbol: "bell.slash",206                                 title: "Aucune recherche sauvegardée",207                                 message: "Dans un univers, lancez une recherche ou des filtres puis « Sauvegarder + alerte ».")208                        .padding(20)209                } else {210                    LazyVStack(spacing: 10) {211                        ForEach(recents.savedSearches) { s in212                            row(s)213                        }214                    }215                }216            }217            .padding(18)218            .frame(maxWidth: 860)219            .frame(maxWidth: .infinity)220        }221        .background(KATheme.paper(scheme))222    }223224    private func row(_ s: SavedSearch) -> some View {225        let u = Ecosystem.universe(s.universeID)226        return HStack(spacing: 12) {227            Image(systemName: u?.symbol ?? "magnifyingglass")228                .foregroundStyle(u?.accent ?? .gray)229                .frame(width: 30, height: 30)230                .background((u?.accent ?? .gray).opacity(0.12), in: RoundedRectangle(cornerRadius: 8))231            VStack(alignment: .leading, spacing: 3) {232                HStack(spacing: 7) {233                    Text(s.name.isEmpty ? "(filtres seulement)" : s.name)234                        .font(.subheadline.weight(.bold))235                    if s.newCount > 0 {236                        Text("+\(s.newCount) nouveauté\(s.newCount > 1 ? "s" : "")")237                            .font(.system(.caption2, design: .monospaced).weight(.bold))238                            .padding(.horizontal, 7).padding(.vertical, 2)239                            .background(Color.red.opacity(0.85), in: Capsule())240                            .foregroundStyle(.white)241                    }242                }243                HStack(spacing: 6) {244                    if let u { KAChip(text: u.wordmark, accent: u.accent) }245                    if !s.params.isEmpty {246                        Text(s.params.map { "\($0.key)=\($0.value)" }.sorted().joined(separator: " · "))247                            .font(.system(size: 10, design: .monospaced))248                            .foregroundStyle(.secondary).lineLimit(1)249                    }250                    if let total = s.lastTotal {251                        Text("\(total.fr) résultats")252                            .font(.system(size: 10, design: .monospaced).weight(.bold))253                            .foregroundStyle(u?.accent ?? .secondary)254                    }255                    if let d = s.lastChecked {256                        Text("vérifié \(d.formatted(.relative(presentation: .named, unitsStyle: .narrow)))")257                            .font(.system(size: 10)).foregroundStyle(.tertiary)258                    }259                }260            }261            Spacer()262            Toggle("Alerte", isOn: Binding(263                get: { s.alertsOn },264                set: { v in265                    if let i = recents.savedSearches.firstIndex(where: { $0.id == s.id }) {266                        recents.savedSearches[i].alertsOn = v267                    }268                }))269                .toggleStyle(.switch)270                .controlSize(.mini)271                .labelsHidden()272                .help(s.alertsOn ? "Alerte active" : "Alerte en pause")273            Button("Ouvrir") {274                if s.newCount > 0 { recents.markSeen(s.id) }275                state.openUniverse(s.universeID, query: s.query, params: s.params)276            }277            .tint(u?.accent)278            Button {279                recents.remove(s.id)280            } label: {281                Image(systemName: "trash")282            }283            .help("Supprimer cette recherche sauvegardée")284        }285        .padding(12)286        .frame(maxWidth: .infinity, alignment: .leading)287        .kaCard(accent: s.newCount > 0 ? .red : u?.accent)288    }289}290