SPB Git

spb/focale Public

Swift 100%
2.6 KB · 99 lines swift
Raw Blame History
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