// // AccountState.swift // Nextcloud Cookbook iOS Client // // Created by Vincent Meilinger on 29.05.24. // import Foundation import SwiftUI @Observable class CookbookState { let id = UUID() let account: Account? = nil /// Caches recipe categories. var categories: [Category] = [] /// Caches RecipeStubs. var recipeStubs: [String: [RecipeStub]] = [:] /// Caches Recipes by recipe id. var recipes: [String: Recipe] = [:] /// Caches recipe thumbnails by recipe id. var thumbnails: [String: UIImage] = [:] /// Caches recipe images by recipe id. var images: [String: UIImage] = [:] /// Caches recipe keywords. var keywords: [RecipeKeyword] = [] /// Read and write interfaces. var readLocal: ReadInterface var writeLocal: WriteInterface var readRemote: [ReadInterface]? var writeRemote: [WriteInterface]? var localOnly: Bool = false /// UI state variables var selectedCategory: Category? = nil var selectedRecipe: RecipeStub? = nil var navigationPath: NavigationPath = NavigationPath() /// Grocery List var groceryList = GroceryList() init( readLocal: ReadInterface, writeLocal: WriteInterface, readRemote: [ReadInterface] = [], writeRemote: [WriteInterface] = [] ) { self.readLocal = readLocal self.writeLocal = writeLocal self.readRemote = readRemote self.writeRemote = writeRemote } } extension CookbookState { func removeRecipe(_ id: String) { for key in recipeStubs.keys { recipeStubs[key]?.removeAll(where: { $0.id == id }) } recipes.removeValue(forKey: id) thumbnails.removeValue(forKey: id) images.removeValue(forKey: id) } func imgToCache(_ image: UIImage, id: String, size: RecipeImage.RecipeImageSize) { if size == .THUMB { thumbnails[id] = image } else { images[id] = image } } func imgFromCache(id: String, size: RecipeImage.RecipeImageSize) -> UIImage? { if size == .THUMB { return thumbnails[id] } else { return images[id] } } } extension CookbookState: ReadInterface { func getImage(id: String, size: RecipeImage.RecipeImageSize) async -> UIImage? { if let image = imgFromCache(id: id, size: size) { return image } if !localOnly, let readRemote { if let image = await readRemote.getImage(id: id, size: size) { return image } } if let image = await readLocal.getImage(id: id, size: size) { return image } return nil } func getRecipeStubs() async -> [RecipeStub]? { if !localOnly, let readRemote { if let stubs = await readRemote.getRecipeStubs() { return stubs } } if categories.isEmpty { self.categories = await readLocal.getCategories() ?? [] } for category in self.categories { self.recipeStubs[category.name] = await readLocal.getRecipeStubsForCategory(named: category.name) } return self.recipeStubs.flatMap({_, val in val}) } func getRecipe(id: String) async -> Recipe? { if let recipe = self.recipes[id] { return recipe } if !localOnly, let readRemote { if let recipe = await readRemote.getRecipe(id: id) { return recipe } } return await readLocal.getRecipe(id: id) } func getCategories() async -> [Category]? { if !localOnly, let readRemote { if let categories = await readRemote.getCategories() { self.categories = categories return categories } } if self.categories.isEmpty, let categories = await readLocal.getCategories() { self.categories = categories return categories } return self.categories } func getRecipeStubsForCategory(named categoryName: String) async -> [RecipeStub]? { if let stubs = self.recipeStubs[categoryName] { return stubs } if !localOnly, let readRemote { if let stubs = await readRemote.getRecipeStubsForCategory(named: categoryName) { self.recipeStubs[categoryName] = stubs } } if let stubs = await readLocal.getRecipeStubsForCategory(named: categoryName) { self.recipeStubs[categoryName] = stubs return stubs } return nil } func getTags() async -> [RecipeKeyword]? { if !keywords.isEmpty { return keywords } if !localOnly, let readRemote { if let tags = await readRemote.getTags() { self.keywords = tags } } return await readLocal.getTags() } func getRecipesTagged(keyword: String) async -> [RecipeStub]? { if !localOnly, let readRemote { if let stubs = await readRemote.getRecipesTagged(keyword: keyword) { return stubs } } return await getRecipeStubs()?.filter({ recipe in recipe.keywords?.contains(keyword.lowercased()) ?? false }) } } extension CookbookState: WriteInterface { func postImage(id: String, image: UIImage, size: RecipeImage.RecipeImageSize) async -> ((any UserAlert)?) { let _ = await writeLocal.postImage(id: id, image: image, size: size) let _ = await writeRemote?.postImage(id: id, image: image, size: size) return nil } func postRecipe(recipe: Recipe) async -> ((any UserAlert)?) { let _ = await writeLocal.postRecipe(recipe: recipe) let _ = await writeRemote?.postRecipe(recipe: recipe) return nil } func updateRecipe(recipe: Recipe) async -> ((any UserAlert)?) { let _ = await writeLocal.updateRecipe(recipe: recipe) let _ = await writeRemote?.updateRecipe(recipe: recipe) return nil } func deleteRecipe(id: String) async -> ((any UserAlert)?) { let _ = await writeLocal.deleteRecipe(id: id) let _ = await writeRemote?.deleteRecipe(id: id) return nil } func renameCategory(named categoryName: String, newName: String) async -> ((any UserAlert)?) { let _ = await writeLocal.renameCategory(named: categoryName, newName: newName) let _ = await writeRemote?.renameCategory(named: categoryName, newName: newName) return nil } } extension AccountState: Hashable, Identifiable { static func == (lhs: AccountState, rhs: AccountState) -> Bool { lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(id) } }