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%
5.4 KB · 143 lines swift
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// RecentsStore.swift — historique des éléments consultés, recherches3// sauvegardées et ALERTES honnêtes : à l'ouverture, on recompte le total réel4// de chaque recherche sauvegardée via l'API et on affiche le delta (« +125// nouveautés ») — jamais de faux chiffre. Persistance JSON locale.6// Repris de l'app iOS KA, sans haptique.7import Foundation8import SwiftUI910struct SavedSearch: Identifiable, Codable, Hashable {11    var id: UUID = UUID()12    var name: String13    var universeID: String          // "tous" = recherche universelle14    var query: String15    var params: [String: String] = [:]16    var alertsOn: Bool = true17    var lastTotal: Int?18    var newCount: Int = 019    var lastChecked: Date?20}2122@MainActor23final class RecentsStore: ObservableObject {24    static let shared = RecentsStore()2526    @Published private(set) var viewed: [KAItem] = [] { didSet { save() } }27    @Published var savedSearches: [SavedSearch] = [] { didSet { save() } }2829    var alertCount: Int { savedSearches.filter(\.alertsOn).map(\.newCount).reduce(0, +) }3031    private var fileURL: URL {32        let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]33            .appendingPathComponent("KA", isDirectory: true)34        try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)35        return dir.appendingPathComponent("ka-recents.json")36    }3738    private struct Snapshot: Codable {39        var viewed: [KAItem]40        var searches: [SavedSearch]41    }4243    init() {44        if let data = try? Data(contentsOf: fileURL),45           let s = try? JSONDecoder().decode(Snapshot.self, from: data) {46            viewed = s.viewed47            savedSearches = s.searches48        }49    }5051    private func save() {52        if let data = try? JSONEncoder().encode(Snapshot(viewed: viewed, searches: savedSearches)) {53            try? data.write(to: fileURL, options: .atomic)54        }55    }5657    // MARK: historique consulté5859    func record(_ item: KAItem) {60        viewed.removeAll { $0.id == item.id }61        viewed.insert(item, at: 0)62        if viewed.count > 30 { viewed.removeLast(viewed.count - 30) }63    }6465    func clearHistory() { viewed = [] }6667    // MARK: recherches sauvegardées + alertes6869    func saveSearch(name: String, universeID: String, query: String, params: [String: String]) {70        var s = SavedSearch(name: name.isEmpty ? query : name,71                            universeID: universeID, query: query, params: params)72        savedSearches.insert(s, at: 0)73        // total initial en arrière-plan74        Task { [weak self] in75            guard let self else { return }76            if let u = Ecosystem.universe(universeID) {77                s.lastTotal = await UniverseService.total(u, query: query, params: params)78                s.lastChecked = .now79                if let i = self.savedSearches.firstIndex(where: { $0.id == s.id }) {80                    self.savedSearches[i] = s81                }82            }83        }84    }8586    func remove(_ id: UUID) { savedSearches.removeAll { $0.id == id } }8788    func markSeen(_ id: UUID) {89        guard let i = savedSearches.firstIndex(where: { $0.id == id }) else { return }90        savedSearches[i].lastTotal = (savedSearches[i].lastTotal ?? 0) + savedSearches[i].newCount91        savedSearches[i].newCount = 092    }9394    /// Recompte chaque recherche (deltas réels). Appelé à l'ouverture de l'app.95    func refreshAlerts() async {96        for idx in savedSearches.indices {97            let s = savedSearches[idx]98            guard s.alertsOn, let u = Ecosystem.universe(s.universeID) else { continue }99            if let total = await UniverseService.total(u, query: s.query, params: s.params) {100                if let last = s.lastTotal, total > last {101                    savedSearches[idx].newCount = total - last102                } else if s.lastTotal == nil {103                    savedSearches[idx].lastTotal = total104                }105                savedSearches[idx].lastChecked = .now106            }107        }108    }109}110111// MARK: - Collections éditoriales (requêtes RÉELLES, aucun contenu inventé)112113struct EditorialCollection: Identifiable {114    let id: String115    let title: String116    let subtitle: String117    let symbol: String118    let universeID: String119    let query: String?120    let params: [String: String]121}122123enum Editorial {124    static let collections: [EditorialCollection] = [125        .init(id: "week-end", title: "Le week-end s'organise",126              subtitle: "Sorties gratuites à venir partout au Québec",127              symbol: "party.popper.fill", universeID: "sorti-ka",128              query: nil, params: ["free": "true"]),129        .init(id: "aubaines-epicerie", title: "Le panier futé",130              subtitle: "Les soldes d'épicerie du moment",131              symbol: "tag.fill", universeID: "food-ka",132              query: nil, params: ["on_sale": "true"]),133        .init(id: "premiere-auto", title: "Première auto",134              subtitle: "Des véhicules récents sous 15 000 $",135              symbol: "car.2.fill", universeID: "auto-ka",136              query: nil, params: ["price_max": "15000", "year_min": "2015"]),137        .init(id: "quatre-et-demi", title: "Le classique 4½",138              subtitle: "Les 4½ fraîchement affichés",139              symbol: "key.fill", universeID: "lou-ka",140              query: nil, params: ["unit_type": "4½"]),141    ]142}143