// // LivingAlbum.swift // Focale // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // // An album defined by a query, not a selection (CLAUDE.md §7). // It fills itself as you shoot. // import Foundation import Observation struct LivingAlbum: Codable, Identifiable, Sendable { let id: UUID var name: String var filter: SearchFilter init(id: UUID = UUID(), name: String, filter: SearchFilter) { self.id = id self.name = name self.filter = filter } } @MainActor @Observable final class LivingAlbumStore { private(set) var albums: [LivingAlbum] = [] private let fileURL: URL init(directory: URL = .applicationSupportDirectory) { fileURL = directory.appending(path: "living-albums.json") load() if albums.isEmpty { albums = Self.starters save() } } func add(_ album: LivingAlbum) { albums.append(album) save() } func remove(_ id: LivingAlbum.ID) { albums.removeAll { $0.id == id } save() } // User-facing names in French. private static var starters: [LivingAlbum] { var receipts = SearchFilter.empty receipts.kinds = [PhotoKind.receipt.rawValue] var actionable = SearchFilter.empty actionable.actionableOnly = true return [ LivingAlbum(name: "Tous mes reçus", filter: receipts), LivingAlbum(name: "Infos à conserver", filter: actionable), ] } private func load() { guard let data = try? Data(contentsOf: fileURL), let decoded = try? JSONDecoder().decode([LivingAlbum].self, from: data) else { return } albums = decoded } private func save() { guard let data = try? JSONEncoder().encode(albums) else { return } try? FileManager.default.createDirectory( at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true ) try? data.write(to: fileURL, options: .atomic) } }