// // RecipeStore.swift // Focale // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // import Foundation import Observation @MainActor @Observable final class RecipeStore { private(set) var recipes: [Recipe] = [] var activeRecipeID: Recipe.ID? { didSet { save() } } private let fileURL: URL var activeRecipe: Recipe? { guard let activeRecipeID else { return nil } return recipes.first { $0.id == activeRecipeID } } init(directory: URL = .applicationSupportDirectory) { fileURL = directory.appending(path: "recipes.json") load() if recipes.isEmpty { recipes = Recipe.builtIns save() } } func add(_ recipe: Recipe) { recipes.append(recipe) save() } func update(_ recipe: Recipe) { guard let index = recipes.firstIndex(where: { $0.id == recipe.id }) else { return } recipes[index] = recipe save() } func remove(_ id: Recipe.ID) { recipes.removeAll { $0.id == id } if activeRecipeID == id { activeRecipeID = nil } save() } @discardableResult func importRecipe(from data: Data) throws -> Recipe { var recipe = try Recipe(importing: data) // Re-identify on import so two devices can hold the same shared file. recipe = Recipe( name: recipe.name, settings: recipe.settings, autoProjectName: recipe.autoProjectName, sceneTrigger: recipe.sceneTrigger, showsTimestamp: recipe.showsTimestamp, prioritizeOCR: recipe.prioritizeOCR ) add(recipe) return recipe } // MARK: - Persistence private struct Snapshot: Codable { var recipes: [Recipe] var activeRecipeID: Recipe.ID? } private func load() { guard let data = try? Data(contentsOf: fileURL), let snapshot = try? JSONDecoder().decode(Snapshot.self, from: data) else { return } recipes = snapshot.recipes activeRecipeID = snapshot.activeRecipeID } private func save() { let snapshot = Snapshot(recipes: recipes, activeRecipeID: activeRecipeID) 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) } }