Swift 100%
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).5import Foundation6import SwiftUI78@MainActor9final class FavoritesStore: ObservableObject {10 static let shared = FavoritesStore()1112 struct FavCollection: Identifiable, Codable, Hashable {13 var id: UUID = UUID()14 var name: String15 var items: [KAItem] = []16 }1718 @Published private(set) var collections: [FavCollection] = [] {19 didSet { save() }20 }2122 private var fileURL: URL {23 let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]24 try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)25 return dir.appendingPathComponent("ka-favoris.json")26 }2728 init() {29 if let data = try? Data(contentsOf: fileURL),30 let saved = try? JSONDecoder().decode([FavCollection].self, from: data) {31 collections = saved32 }33 if collections.isEmpty {34 collections = [FavCollection(name: "Mes favoris")]35 }36 }3738 private func save() {39 if let data = try? JSONEncoder().encode(collections) {40 try? data.write(to: fileURL, options: .atomic)41 }42 }4344 // MARK: API4546 var allItems: [KAItem] { collections.flatMap(\.items) }4748 func isFavorite(_ item: KAItem) -> Bool {49 collections.contains { $0.items.contains { $0.id == item.id } }50 }5152 func toggle(_ item: KAItem, in collectionID: UUID? = nil) {53 if isFavorite(item) {54 for i in collections.indices {55 collections[i].items.removeAll { $0.id == item.id }56 }57 PersonalizationStore.shared.record(.unfavorite, item: item)58 } else {59 let idx = collections.firstIndex { $0.id == collectionID } ?? 060 collections[idx].items.insert(item, at: 0)61 PersonalizationStore.shared.record(.favorite, item: item)62 }63 Haptics.tap()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