// // Project.swift // Focale // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // // Project mode: the user declares "chantier cuisine" once, then every // photo taken is tagged. One gesture, zero friction afterwards. // import Foundation import Observation struct Project: Codable, Identifiable, Sendable, Hashable { typealias ID = UUID let id: ID var name: String var createdAt: Date var endedAt: Date? var isActive: Bool { endedAt == nil } init(id: ID = UUID(), name: String, createdAt: Date = .now, endedAt: Date? = nil) { self.id = id self.name = name self.createdAt = createdAt self.endedAt = endedAt } } /// Owns the list of projects and the single currently active one. @MainActor @Observable final class ProjectStore { private(set) var projects: [Project] = [] private(set) var activeProjectID: Project.ID? private let fileURL: URL var activeProject: Project? { guard let activeProjectID else { return nil } return projects.first { $0.id == activeProjectID } } init(directory: URL = .applicationSupportDirectory) { fileURL = directory.appending(path: "projects.json") load() } /// Declare a new project and make it active immediately. @discardableResult func declare(_ name: String) -> Project { let project = Project(name: name) projects.insert(project, at: 0) activeProjectID = project.id save() return project } func activate(_ id: Project.ID?) { activeProjectID = id save() } func end(_ id: Project.ID) { guard let index = projects.firstIndex(where: { $0.id == id }) else { return } projects[index].endedAt = .now if activeProjectID == id { activeProjectID = nil } save() } // MARK: - Persistence private struct Snapshot: Codable { var projects: [Project] var activeProjectID: Project.ID? } private func load() { guard let data = try? Data(contentsOf: fileURL), let snapshot = try? JSONDecoder().decode(Snapshot.self, from: data) else { return } projects = snapshot.projects activeProjectID = snapshot.activeProjectID } private func save() { let snapshot = Snapshot(projects: projects, activeProjectID: activeProjectID) guard let data = try? JSONEncoder().encode(snapshot) else { return } try? FileManager.default.createDirectory( at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true ) try? data.write(to: fileURL, options: .atomic) } }