SPB Git forge

spb/ka-ios

Public
20commits 1branches 0releases
17.2 MBsize
maindefault branch
28 days agolast push
Swift 100%
5.3 KB · 142 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.6import Foundation7import SwiftUI89struct SavedSearch: Identifiable, Codable, Hashable {10    var id: UUID = UUID()11    var name: String12    var universeID: String          // "tous" = recherche universelle13    var query: String14    var params: [String: String] = [:]15    var alertsOn: Bool = true16    var lastTotal: Int?17    var newCount: Int = 018    var lastChecked: Date?19}2021@MainActor22final class RecentsStore: ObservableObject {23    static let shared = RecentsStore()2425    @Published private(set) var viewed: [KAItem] = [] { didSet { save() } }26    @Published var savedSearches: [SavedSearch] = [] { didSet { save() } }2728    var alertCount: Int { savedSearches.filter(\.alertsOn).map(\.newCount).reduce(0, +) }2930    private var fileURL: URL {31        let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]32        try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)33        return dir.appendingPathComponent("ka-recents.json")34    }3536    private struct Snapshot: Codable {37        var viewed: [KAItem]38        var searches: [SavedSearch]39    }4041    init() {42        if let data = try? Data(contentsOf: fileURL),43           let s = try? JSONDecoder().decode(Snapshot.self, from: data) {44            viewed = s.viewed45            savedSearches = s.searches46        }47    }4849    private func save() {50        if let data = try? JSONEncoder().encode(Snapshot(viewed: viewed, searches: savedSearches)) {51            try? data.write(to: fileURL, options: .atomic)52        }53    }5455    // MARK: historique consulté5657    func record(_ item: KAItem) {58        viewed.removeAll { $0.id == item.id }59        viewed.insert(item, at: 0)60        if viewed.count > 30 { viewed.removeLast(viewed.count - 30) }61    }6263    func clearHistory() { viewed = [] }6465    // MARK: recherches sauvegardées + alertes6667    func saveSearch(name: String, universeID: String, query: String, params: [String: String]) {68        var s = SavedSearch(name: name.isEmpty ? query : name,69                            universeID: universeID, query: query, params: params)70        savedSearches.insert(s, at: 0)71        Haptics.success()72        // total initial en arrière-plan73        Task { [weak self] in74            guard let self else { return }75            if let u = Ecosystem.universe(universeID) {76                s.lastTotal = await UniverseService.total(u, query: query, params: params)77                s.lastChecked = .now78                if let i = self.savedSearches.firstIndex(where: { $0.id == s.id }) {79                    self.savedSearches[i] = s80                }81            }82        }83    }8485    func remove(_ id: UUID) { savedSearches.removeAll { $0.id == id } }8687    func markSeen(_ id: UUID) {88        guard let i = savedSearches.firstIndex(where: { $0.id == id }) else { return }89        savedSearches[i].lastTotal = (savedSearches[i].lastTotal ?? 0) + savedSearches[i].newCount90        savedSearches[i].newCount = 091    }9293    /// Recompte chaque recherche (deltas réels). Appelé à l'ouverture de l'app.94    func refreshAlerts() async {95        for idx in savedSearches.indices {96            let s = savedSearches[idx]97            guard s.alertsOn, let u = Ecosystem.universe(s.universeID) else { continue }98            if let total = await UniverseService.total(u, query: s.query, params: s.params) {99                if let last = s.lastTotal, total > last {100                    savedSearches[idx].newCount = total - last101                } else if s.lastTotal == nil {102                    savedSearches[idx].lastTotal = total103                }104                savedSearches[idx].lastChecked = .now105            }106        }107    }108}109110// MARK: - Collections éditoriales (requêtes RÉELLES, aucun contenu inventé)111112struct EditorialCollection: Identifiable {113    let id: String114    let title: String115    let subtitle: String116    let symbol: String117    let universeID: String118    let query: String?119    let params: [String: String]120}121122enum Editorial {123    static let collections: [EditorialCollection] = [124        .init(id: "week-end", title: "Le week-end s'organise",125              subtitle: "Sorties gratuites à venir partout au Québec",126              symbol: "party.popper.fill", universeID: "sorti-ka",127              query: nil, params: ["free": "true"]),128        .init(id: "aubaines-epicerie", title: "Le panier futé",129              subtitle: "Les soldes d'épicerie du moment",130              symbol: "tag.fill", universeID: "food-ka",131              query: nil, params: ["on_sale": "true"]),132        .init(id: "premiere-auto", title: "Première auto",133              subtitle: "Des véhicules récents sous 15 000 $",134              symbol: "car.2.fill", universeID: "auto-ka",135              query: nil, params: ["price_max": "15000", "year_min": "2015"]),136        .init(id: "quatre-et-demi", title: "Le classique 4½",137              subtitle: "Les 4½ fraîchement affichés",138              symbol: "key.fill", universeID: "lou-ka",139              query: nil, params: ["unit_type": "4½"]),140    ]141}142