From 29fd3c668b733242987a23ef7d7384baa9c29d2f Mon Sep 17 00:00:00 2001 From: VincentMeilinger Date: Mon, 26 May 2025 15:52:12 +0200 Subject: [PATCH] WIP - Complete App refactoring --- .../Account/AccountManager.swift | 8 + .../Account/AccountProtocol.swift | 8 + .../AccountTypes/CookbookAccount.swift | 30 +++ .../Account/AccountTypes/LocalAccount.swift | 8 + ...ervableRecipeDetail.swift => Recipe.swift} | 0 .../{Network => Networking}/ApiRequest.swift | 0 .../CookbookApi/CookbookApi.swift | 0 .../CookbookApi/CookbookApiV1.swift | 0 .../CookbookApi/CookbookLoginModels.swift} | 19 +- .../CookbookApi/CookbookModelsV1.swift} | 12 +- .../CookbookApi/CookbookProtocols.swift | 8 + .../NetworkError.swift | 0 .../NetworkUtils.swift | 0 .../NextcloudApi/NextcloudApi.swift | 0 .../Persistence/Account 2.swift | 8 + .../Persistence/CookbookState.swift | 244 ++++++++++++++++++ .../Interfaces/DataInterface.swift | 113 ++++++++ .../Interfaces/LocalDataInterface.swift | 161 ++++++++++++ .../Interfaces/NextcloudDataInterface.swift | 95 +++++++ 19 files changed, 691 insertions(+), 23 deletions(-) create mode 100644 Nextcloud Cookbook iOS Client/Account/AccountManager.swift create mode 100644 Nextcloud Cookbook iOS Client/Account/AccountProtocol.swift create mode 100644 Nextcloud Cookbook iOS Client/Account/AccountTypes/CookbookAccount.swift create mode 100644 Nextcloud Cookbook iOS Client/Account/AccountTypes/LocalAccount.swift rename Nextcloud Cookbook iOS Client/Data/{ObservableRecipeDetail.swift => Recipe.swift} (100%) rename Nextcloud Cookbook iOS Client/{Network => Networking}/ApiRequest.swift (100%) rename Nextcloud Cookbook iOS Client/{Network => Networking}/CookbookApi/CookbookApi.swift (100%) rename Nextcloud Cookbook iOS Client/{Network => Networking}/CookbookApi/CookbookApiV1.swift (100%) rename Nextcloud Cookbook iOS Client/{Data/DataModels.swift => Networking/CookbookApi/CookbookLoginModels.swift} (64%) rename Nextcloud Cookbook iOS Client/{Data/RecipeModels.swift => Networking/CookbookApi/CookbookModelsV1.swift} (96%) create mode 100644 Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookProtocols.swift rename Nextcloud Cookbook iOS Client/{Network => Networking}/NetworkError.swift (100%) rename Nextcloud Cookbook iOS Client/{Network => Networking}/NetworkUtils.swift (100%) rename Nextcloud Cookbook iOS Client/{Network => Networking}/NextcloudApi/NextcloudApi.swift (100%) create mode 100644 Nextcloud Cookbook iOS Client/Persistence/Account 2.swift create mode 100644 Nextcloud Cookbook iOS Client/Persistence/CookbookState.swift create mode 100644 Nextcloud Cookbook iOS Client/Persistence/Interfaces/DataInterface.swift create mode 100644 Nextcloud Cookbook iOS Client/Persistence/Interfaces/LocalDataInterface.swift create mode 100644 Nextcloud Cookbook iOS Client/Persistence/Interfaces/NextcloudDataInterface.swift diff --git a/Nextcloud Cookbook iOS Client/Account/AccountManager.swift b/Nextcloud Cookbook iOS Client/Account/AccountManager.swift new file mode 100644 index 0000000..2420bf4 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Account/AccountManager.swift @@ -0,0 +1,8 @@ +// +// AccountManager.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 16.01.25. +// + +import Foundation diff --git a/Nextcloud Cookbook iOS Client/Account/AccountProtocol.swift b/Nextcloud Cookbook iOS Client/Account/AccountProtocol.swift new file mode 100644 index 0000000..01f0836 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Account/AccountProtocol.swift @@ -0,0 +1,8 @@ +// +// AccountProtocol.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 16.01.25. +// + +import Foundation diff --git a/Nextcloud Cookbook iOS Client/Account/AccountTypes/CookbookAccount.swift b/Nextcloud Cookbook iOS Client/Account/AccountTypes/CookbookAccount.swift new file mode 100644 index 0000000..2ba7cf8 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Account/AccountTypes/CookbookAccount.swift @@ -0,0 +1,30 @@ +// +// CookbookV1Account.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 24.01.25. +// + +import Foundation +import KeychainSwift + + +struct CookbookAccount: Account { + let id: UUID + var displayName: String = "Nextcloud Cookbook Account" + let accountType: AccountType = .cookbook + + let baseURL: URL + let username: String + + /// Keychain convenience + func saveTokenToKeychain(_ token: String) { + let keychain = KeychainSwift() + keychain.set(token, forKey: "token-\(id.uuidString)") + } + + func getTokenFromKeychain() -> String? { + let keychain = KeychainSwift() + return keychain.get("token-\(id.uuidString)") + } +} diff --git a/Nextcloud Cookbook iOS Client/Account/AccountTypes/LocalAccount.swift b/Nextcloud Cookbook iOS Client/Account/AccountTypes/LocalAccount.swift new file mode 100644 index 0000000..6389ee1 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Account/AccountTypes/LocalAccount.swift @@ -0,0 +1,8 @@ +// +// LocalAccount.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 16.04.25. +// + +import Foundation diff --git a/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift b/Nextcloud Cookbook iOS Client/Data/Recipe.swift similarity index 100% rename from Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift rename to Nextcloud Cookbook iOS Client/Data/Recipe.swift diff --git a/Nextcloud Cookbook iOS Client/Network/ApiRequest.swift b/Nextcloud Cookbook iOS Client/Networking/ApiRequest.swift similarity index 100% rename from Nextcloud Cookbook iOS Client/Network/ApiRequest.swift rename to Nextcloud Cookbook iOS Client/Networking/ApiRequest.swift diff --git a/Nextcloud Cookbook iOS Client/Network/CookbookApi/CookbookApi.swift b/Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookApi.swift similarity index 100% rename from Nextcloud Cookbook iOS Client/Network/CookbookApi/CookbookApi.swift rename to Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookApi.swift diff --git a/Nextcloud Cookbook iOS Client/Network/CookbookApi/CookbookApiV1.swift b/Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookApiV1.swift similarity index 100% rename from Nextcloud Cookbook iOS Client/Network/CookbookApi/CookbookApiV1.swift rename to Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookApiV1.swift diff --git a/Nextcloud Cookbook iOS Client/Data/DataModels.swift b/Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookLoginModels.swift similarity index 64% rename from Nextcloud Cookbook iOS Client/Data/DataModels.swift rename to Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookLoginModels.swift index 2a20a1f..af453c2 100644 --- a/Nextcloud Cookbook iOS Client/Data/DataModels.swift +++ b/Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookLoginModels.swift @@ -1,26 +1,14 @@ // -// DataModels.swift +// Models.swift // Nextcloud Cookbook iOS Client // -// Created by Vincent Meilinger on 15.09.23. +// Created by Vincent Meilinger on 11.05.24. // import Foundation import SwiftUI -struct Category: Codable { - let name: String - let recipe_count: Int - - private enum CodingKeys: String, CodingKey { - case name, recipe_count - } -} - -extension Category: Identifiable, Hashable { - var id: String { name } -} @@ -54,6 +42,3 @@ struct MetaData: Codable { let status: String let statuscode: Int } - - - diff --git a/Nextcloud Cookbook iOS Client/Data/RecipeModels.swift b/Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookModelsV1.swift similarity index 96% rename from Nextcloud Cookbook iOS Client/Data/RecipeModels.swift rename to Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookModelsV1.swift index f04478c..9a3105a 100644 --- a/Nextcloud Cookbook iOS Client/Data/RecipeModels.swift +++ b/Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookModelsV1.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI -struct Recipe: Codable { +struct CookbookApiRecipeV1: Codable { let name: String let keywords: String? let dateCreated: String? @@ -27,12 +27,12 @@ struct Recipe: Codable { } -extension Recipe: Identifiable, Hashable { +extension CookbookApiRecipeV1: Identifiable, Hashable { var id: String { name } } -struct RecipeDetail: Codable { +struct CookbookApiRecipeDetailV1: Codable { var name: String var keywords: String var dateCreated: String? @@ -120,9 +120,9 @@ struct RecipeDetail: Codable { } -extension RecipeDetail { - static var error: RecipeDetail { - return RecipeDetail( +extension CookbookApiRecipeDetailV1 { + static var error: CookbookApiRecipeDetailV1 { + return CookbookApiRecipeDetailV1( name: "Error: Unable to load recipe.", keywords: "", dateCreated: "", diff --git a/Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookProtocols.swift b/Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookProtocols.swift new file mode 100644 index 0000000..5cfd01c --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Networking/CookbookApi/CookbookProtocols.swift @@ -0,0 +1,8 @@ +// +// CookbookProtocols.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 11.05.24. +// + +import Foundation diff --git a/Nextcloud Cookbook iOS Client/Network/NetworkError.swift b/Nextcloud Cookbook iOS Client/Networking/NetworkError.swift similarity index 100% rename from Nextcloud Cookbook iOS Client/Network/NetworkError.swift rename to Nextcloud Cookbook iOS Client/Networking/NetworkError.swift diff --git a/Nextcloud Cookbook iOS Client/Network/NetworkUtils.swift b/Nextcloud Cookbook iOS Client/Networking/NetworkUtils.swift similarity index 100% rename from Nextcloud Cookbook iOS Client/Network/NetworkUtils.swift rename to Nextcloud Cookbook iOS Client/Networking/NetworkUtils.swift diff --git a/Nextcloud Cookbook iOS Client/Network/NextcloudApi/NextcloudApi.swift b/Nextcloud Cookbook iOS Client/Networking/NextcloudApi/NextcloudApi.swift similarity index 100% rename from Nextcloud Cookbook iOS Client/Network/NextcloudApi/NextcloudApi.swift rename to Nextcloud Cookbook iOS Client/Networking/NextcloudApi/NextcloudApi.swift diff --git a/Nextcloud Cookbook iOS Client/Persistence/Account 2.swift b/Nextcloud Cookbook iOS Client/Persistence/Account 2.swift new file mode 100644 index 0000000..8846610 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Persistence/Account 2.swift @@ -0,0 +1,8 @@ +// +// Account.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 24.01.25. +// + +import Foundation diff --git a/Nextcloud Cookbook iOS Client/Persistence/CookbookState.swift b/Nextcloud Cookbook iOS Client/Persistence/CookbookState.swift new file mode 100644 index 0000000..1d0cd89 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Persistence/CookbookState.swift @@ -0,0 +1,244 @@ +// +// 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) + } +} + diff --git a/Nextcloud Cookbook iOS Client/Persistence/Interfaces/DataInterface.swift b/Nextcloud Cookbook iOS Client/Persistence/Interfaces/DataInterface.swift new file mode 100644 index 0000000..56c8b31 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Persistence/Interfaces/DataInterface.swift @@ -0,0 +1,113 @@ +// +// PersistenceInterface.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 06.05.24. +// + +import Foundation +import SwiftUI +import KeychainSwift + + +protocol CookbookInterface { + /// A unique id of the interface. Used to associate recipes to their respective accounts. + var id: String { get } +} + +protocol ReadInterface { + /// Get either the full image or a thumbnail sized version. + /// - Parameters: + /// - id: The according recipe id. + /// - size: The size of the image. + /// - Returns: The image of the recipe with the specified id. A UserAlert if the request fails, otherwise nil. + func getImage( + id: String, + size: RecipeImage.RecipeImageSize + ) async -> (UIImage?, UserAlert?) + + /// Get all recipe stubs. + /// - Returns: A list of all recipes. + func getRecipeStubs( + ) async -> ([RecipeStub]?, UserAlert?) + + /// Get the recipe with the specified id. + /// - Parameters: + /// - id: The recipe id. + /// - Returns: The recipe if it exists. A UserAlert if the request fails. + func getRecipe( + id: String + ) async -> (Recipe?, UserAlert?) + + /// Get all categories. + /// - Returns: A list of categories. A UserAlert if the request fails. + func getCategories( + ) async -> ([Category]?, UserAlert?) + + /// Get all recipes of a specified category. + /// - Parameters: + /// - categoryName: The category name. + /// - Returns: A list of recipes. A UserAlert if the request fails. + func getRecipeStubsForCategory( + named categoryName: String + ) async -> ([RecipeStub]?, UserAlert?) + + /// Get all keywords/tags. + /// - Returns: A list of tag strings. A UserAlert if the request fails. + func getTags( + ) async -> ([RecipeKeyword]?, UserAlert?) + + /// Get all recipes tagged with the specified keyword. + /// - Parameters: + /// - keyword: The keyword. + /// - Returns: A list of recipes tagged with the specified keyword. A UserAlert if the request fails. + func getRecipesTagged( + keyword: String + ) async -> ([RecipeStub]?, UserAlert?) +} + +protocol WriteInterface { + /// Post either the full image or a thumbnail sized version. + /// - Parameters: + /// - id: The according recipe id. + /// - size: The size of the image. + /// - Returns: A UserAlert if the request fails, otherwise nil. + func postImage( + id: String, + image: UIImage, + size: RecipeImage.RecipeImageSize + ) async -> (UserAlert?) + + /// Create a new recipe. + /// - Parameters: + /// - Returns: A UserAlert if the request fails. Nil otherwise. + func postRecipe( + recipe: Recipe + ) async -> (UserAlert?) + + /// Update an existing recipe with new entries. + /// - Parameters: + /// - recipe: The recipe. + /// - Returns: A UserAlert if the request fails. Nil otherwise. + func updateRecipe( + recipe: Recipe + ) async -> (UserAlert?) + + /// Delete the recipe with the specified id. + /// - Parameters: + /// - id: The recipe id. + /// - Returns: A UserAlert if the request fails. Nil otherwise. + func deleteRecipe( + id: String + ) async -> (UserAlert?) + + /// Rename an existing category. + /// - Parameters: + /// - categoryName: The name of the category to be renamed. + /// - newName: The new category name. + /// - Returns: A UserAlert if the request fails. + func renameCategory( + named categoryName: String, + newName: String + ) async -> (UserAlert?) +} diff --git a/Nextcloud Cookbook iOS Client/Persistence/Interfaces/LocalDataInterface.swift b/Nextcloud Cookbook iOS Client/Persistence/Interfaces/LocalDataInterface.swift new file mode 100644 index 0000000..4184812 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Persistence/Interfaces/LocalDataInterface.swift @@ -0,0 +1,161 @@ +// +// LocalDataInterface.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 07.05.24. +// + +import Foundation +import SwiftUI + + +class LocalDataInterface: CookbookInterface { + var id: String + + init(id: String) { + self.id = id + } + + enum LocalDataPath { + case recipeStubs(category: String), + recipe(id: String), + image(id: String, size: RecipeImage.RecipeImageSize), + categories, + keywords + + var path: String { + switch self { + case .recipe(let id): + "recipe_\(id).data" + case .recipeStubs(let category): + "recipes_\(category).data" + case .image(let id, let size): + if size == .FULL { + "image_\(id).data" + } else { + "thumb_\(id).data" + } + case .categories: + "categories.data" + case .keywords: + "keywords.data" + } + } + } +} + + + +// MARK: - Local Read Interface + +extension LocalDataInterface: ReadInterface { + + func getImage(id: String, size: RecipeImage.RecipeImageSize) async -> (UIImage?, (any UserAlert)?) { + guard let data: String = await load(path: .image(id: id, size: size)) else { + return (nil, PersistenceAlert.LOAD_FAILED) + } + guard let dataDecoded = Data(base64Encoded: data) else { return (nil, PersistenceAlert.DECODING_FAILED) } + if let image = UIImage(data: dataDecoded) { + return (image, nil) + } + return (nil, nil) + } + + func getRecipeStubs() async -> ([RecipeStub]?, (any UserAlert)?) { + return (nil, PersistenceAlert.LOAD_FAILED) + } + + func getRecipe(id: String) async -> (Recipe?, (any UserAlert)?) { + if let recipe: Recipe? = await load(path: LocalDataPath.recipe(id: id)) { + return (recipe, nil) + } + return (nil, nil) + } + + func getCategories() async -> ([Category]?, (any UserAlert)?) { + return (await load(path: LocalDataPath.categories), nil) + } + + func getRecipeStubsForCategory(named categoryName: String) async -> ([RecipeStub]?, (any UserAlert)?) { + if let stubs: [RecipeStub] = await load(path: .recipeStubs(category: categoryName)) { + return (stubs, nil) + } + return (nil, PersistenceAlert.LOAD_FAILED) + } + + func getTags() async -> ([RecipeKeyword]?, (any UserAlert)?) { + if let keywords: [RecipeKeyword] = await load(path: .keywords) { + return (keywords, nil) + } + return (nil, PersistenceAlert.LOAD_FAILED) + } + + func getRecipesTagged(keyword: String) async -> ([RecipeStub]?, (any UserAlert)?) { + return (nil, PersistenceAlert.LOAD_FAILED) + } + +} + + + +// MARK: - Local Write Interface + +extension LocalDataInterface: WriteInterface { + + func postImage(id: String, image: UIImage, size: RecipeImage.RecipeImageSize) async -> ((any UserAlert)?) { + if let data = image.pngData() { + await save( + data, + path: LocalDataPath.image(id: id, size: size) + ) + } + } + + func postRecipe(recipe: Recipe) async -> ((any UserAlert)?) { + await save(recipe, path: LocalDataPath.recipe(id: recipe.id)) + return nil + } + + func updateRecipe(recipe: Recipe) async -> ((any UserAlert)?) { + return await postRecipe(recipe: recipe) + } + + func deleteRecipe(id: String) async -> ((any UserAlert)?) { + await delete(path: .recipe(id: id)) + return nil + } + + func renameCategory(named categoryName: String, newName: String) async -> ((any UserAlert)?) { + guard let stubs: [RecipeStub] = await load(path: .recipeStubs(category: categoryName)) else { + return PersistenceAlert.LOAD_FAILED + } + await delete(path: .recipeStubs(category: categoryName)) + await save(stubs, path: .recipeStubs(category: newName)) + } + +} + + + +// MARK: - Local Data Interface Utils + +extension LocalDataInterface { + + func load(path ldPath: LocalDataPath) async -> T? { + do { + return try await DataStore.shared.load(fromPath: ldPath.path) + } catch (let error) { + print(error) + return nil + } + } + + func save(_ object: T, path ldPath: LocalDataPath) async { + await DataStore.shared.save(data: object, toPath: ldPath.path) + } + + func delete(path ldPath: LocalDataPath) async { + DataStore.shared.delete(path: ldPath.path) + } + +} diff --git a/Nextcloud Cookbook iOS Client/Persistence/Interfaces/NextcloudDataInterface.swift b/Nextcloud Cookbook iOS Client/Persistence/Interfaces/NextcloudDataInterface.swift new file mode 100644 index 0000000..fc35419 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Persistence/Interfaces/NextcloudDataInterface.swift @@ -0,0 +1,95 @@ +// +// NextcloudDataInterface.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 07.05.24. +// + +import Foundation +import SwiftUI + + +class NextcloudDataInterface: CookbookInterface { + var id: String + + var auth: Authentication + var api: CookbookApi.Type + + init(auth: Authentication, version: String) { + self.id = UUID().uuidString + self.auth = auth + switch version { + case "1.0": + self.api = CookbookApiV1.self + default: + self.api = CookbookApiV1.self + } + } +} + + +// MARK: - Nextcloud Read Interface +extension NextcloudDataInterface: ReadInterface { + + func getImage(id: String, size: RecipeImage.RecipeImageSize) async -> (UIImage?, UserAlert?) { + let (image, error) = await api.getImage(auth: auth.token, id: id, size: size) + if let image { + return (image, nil) + } + return (nil, error) + } + + func getRecipeStubs() async -> ([RecipeStub]?, UserAlert?) { + return await api.getRecipes(auth: auth.token) + } + + func getRecipe(id: String) async -> (Recipe?, UserAlert?) { + return await api.getRecipe(auth: auth.token, id: id) + } + + func getCategories() async -> ([Category]?, UserAlert?) { + return await api.getCategories(auth: auth.token) + } + + func getRecipeStubsForCategory(named categoryName: String) async -> ([RecipeStub]?, UserAlert?) { + return await api.getCategory( + auth: UserSettings.shared.authString, + named: categoryName + ) + } + + func getTags() async -> ([RecipeKeyword]?, (any UserAlert)?) { + return await api.getTags(auth: auth.token) + } + + func getRecipesTagged(keyword: String) async -> ([RecipeStub]?, UserAlert?) { + return await api.getRecipesTagged(auth: auth.token, keyword: keyword) + } + +} + + +// MARK: - Nextcloud Write Interface +extension NextcloudDataInterface: WriteInterface { + + func postImage(id: String, image: UIImage, size: RecipeImage.RecipeImageSize) async -> ((any UserAlert)?) { + return nil + } + + func postRecipe(recipe: Recipe) async -> (UserAlert?) { + return await api.createRecipe(auth: auth.token, recipe: recipe) + } + + func updateRecipe(recipe: Recipe) async -> (UserAlert?) { + return await api.updateRecipe(auth: auth.token, recipe: recipe) + } + + func deleteRecipe(id: String) async -> (UserAlert?) { + return await api.deleteRecipe(auth: auth.token, id: id) + } + + func renameCategory(named categoryName: String, newName: String) async -> (UserAlert?) { + return await api.renameCategory(auth: auth.token, named: categoryName, newName: newName) + } + +}