spb/focale Public
Swift 100%
1//2// Project.swift3// Focale4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7//8// Project mode: the user declares "chantier cuisine" once, then every9// photo taken is tagged. One gesture, zero friction afterwards.10//1112import Foundation13import Observation1415struct Project: Codable, Identifiable, Sendable, Hashable {16 typealias ID = UUID1718 let id: ID19 var name: String20 var createdAt: Date21 var endedAt: Date?2223 var isActive: Bool { endedAt == nil }2425 init(id: ID = UUID(), name: String, createdAt: Date = .now, endedAt: Date? = nil) {26 self.id = id27 self.name = name28 self.createdAt = createdAt29 self.endedAt = endedAt30 }31}3233/// Owns the list of projects and the single currently active one.34@MainActor35@Observable36final class ProjectStore {37 private(set) var projects: [Project] = []38 private(set) var activeProjectID: Project.ID?3940 private let fileURL: URL4142 var activeProject: Project? {43 guard let activeProjectID else { return nil }44 return projects.first { $0.id == activeProjectID }45 }4647 init(directory: URL = .applicationSupportDirectory) {48 fileURL = directory.appending(path: "projects.json")49 load()50 }5152 /// Declare a new project and make it active immediately.53 @discardableResult54 func declare(_ name: String) -> Project {55 let project = Project(name: name)56 projects.insert(project, at: 0)57 activeProjectID = project.id58 save()59 return project60 }6162 func activate(_ id: Project.ID?) {63 activeProjectID = id64 save()65 }6667 func end(_ id: Project.ID) {68 guard let index = projects.firstIndex(where: { $0.id == id }) else { return }69 projects[index].endedAt = .now70 if activeProjectID == id { activeProjectID = nil }71 save()72 }7374 // MARK: - Persistence7576 private struct Snapshot: Codable {77 var projects: [Project]78 var activeProjectID: Project.ID?79 }8081 private func load() {82 guard let data = try? Data(contentsOf: fileURL),83 let snapshot = try? JSONDecoder().decode(Snapshot.self, from: data)84 else { return }85 projects = snapshot.projects86 activeProjectID = snapshot.activeProjectID87 }8889 private func save() {90 let snapshot = Snapshot(projects: projects, activeProjectID: activeProjectID)91 guard let data = try? JSONEncoder().encode(snapshot) else { return }92 try? FileManager.default.createDirectory(93 at: fileURL.deletingLastPathComponent(),94 withIntermediateDirectories: true95 )96 try? data.write(to: fileURL, options: .atomic)97 }98}99