Swift 98.3%
Shell 1.7%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// FavoritesStore.swift — favoris et collections UNIFIÉS (tous univers dans une3// même collection : « Déménagement à Val-d'Or » peut contenir un 4½, une auto4// et un resto). Persistance JSON locale (consultation hors ligne).5// Repris de l'app iOS KA, sans haptique.6import Foundation7import SwiftUI89@MainActor10final class FavoritesStore: ObservableObject {11 static let shared = FavoritesStore()1213 struct FavCollection: Identifiable, Codable, Hashable {14 var id: UUID = UUID()15 var name: String16 var items: [KAItem] = []17 }1819 @Published private(set) var collections: [FavCollection] = [] {20 didSet { save() }21 }2223 private var fileURL: URL {24 let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]25 .appendingPathComponent("KA", isDirectory: true)26 try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)27 return dir.appendingPathComponent("ka-favoris.json")28 }2930 init() {31 if let data = try? Data(contentsOf: fileURL),32 let saved = try? JSONDecoder().decode([FavCollection].self, from: data) {33 collections = saved34 }35 if collections.isEmpty {36 collections = [FavCollection(name: "Mes favoris")]37 }38 }3940 private func save() {41 if let data = try? JSONEncoder().encode(collections) {42 try? data.write(to: fileURL, options: .atomic)43 }44 }4546 // MARK: API4748 var allItems: [KAItem] { collections.flatMap(\.items) }49 var count: Int { collections.reduce(0) { $0 + $1.items.count } }5051 func isFavorite(_ item: KAItem) -> Bool {52 collections.contains { $0.items.contains { $0.id == item.id } }53 }5455 func toggle(_ item: KAItem, in collectionID: UUID? = nil) {56 if isFavorite(item) {57 for i in collections.indices {58 collections[i].items.removeAll { $0.id == item.id }59 }60 } else {61 let idx = collections.firstIndex { $0.id == collectionID } ?? 062 collections[idx].items.insert(item, at: 0)63 }64 }6566 func addCollection(_ name: String) {67 let trimmed = name.trimmingCharacters(in: .whitespaces)68 guard !trimmed.isEmpty else { return }69 collections.append(FavCollection(name: trimmed))70 }7172 func removeCollection(_ id: UUID) {73 guard collections.count > 1 else { return }74 collections.removeAll { $0.id == id }75 }7677 func move(_ item: KAItem, to collectionID: UUID) {78 for i in collections.indices { collections[i].items.removeAll { $0.id == item.id } }79 if let idx = collections.firstIndex(where: { $0.id == collectionID }) {80 collections[idx].items.insert(item, at: 0)81 }82 }83}84