spb/focale Public
Swift 100%
1//2// LivingAlbum.swift3// Focale4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7//8// An album defined by a query, not a selection (CLAUDE.md §7).9// It fills itself as you shoot.10//1112import Foundation13import Observation1415struct LivingAlbum: Codable, Identifiable, Sendable {16 let id: UUID17 var name: String18 var filter: SearchFilter1920 init(id: UUID = UUID(), name: String, filter: SearchFilter) {21 self.id = id22 self.name = name23 self.filter = filter24 }25}2627@MainActor28@Observable29final class LivingAlbumStore {30 private(set) var albums: [LivingAlbum] = []31 private let fileURL: URL3233 init(directory: URL = .applicationSupportDirectory) {34 fileURL = directory.appending(path: "living-albums.json")35 load()36 if albums.isEmpty {37 albums = Self.starters38 save()39 }40 }4142 func add(_ album: LivingAlbum) {43 albums.append(album)44 save()45 }4647 func remove(_ id: LivingAlbum.ID) {48 albums.removeAll { $0.id == id }49 save()50 }5152 // User-facing names in French.53 private static var starters: [LivingAlbum] {54 var receipts = SearchFilter.empty55 receipts.kinds = [PhotoKind.receipt.rawValue]5657 var actionable = SearchFilter.empty58 actionable.actionableOnly = true5960 return [61 LivingAlbum(name: "Tous mes reçus", filter: receipts),62 LivingAlbum(name: "Infos à conserver", filter: actionable),63 ]64 }6566 private func load() {67 guard let data = try? Data(contentsOf: fileURL),68 let decoded = try? JSONDecoder().decode([LivingAlbum].self, from: data)69 else { return }70 albums = decoded71 }7273 private func save() {74 guard let data = try? JSONEncoder().encode(albums) else { return }75 try? FileManager.default.createDirectory(76 at: fileURL.deletingLastPathComponent(),77 withIntermediateDirectories: true78 )79 try? data.write(to: fileURL, options: .atomic)80 }81}82