WIP - Complete App refactoring
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// AccountManager.swift
|
||||||
|
// Nextcloud Cookbook iOS Client
|
||||||
|
//
|
||||||
|
// Created by Vincent Meilinger on 16.01.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// AccountProtocol.swift
|
||||||
|
// Nextcloud Cookbook iOS Client
|
||||||
|
//
|
||||||
|
// Created by Vincent Meilinger on 16.01.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
@@ -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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// LocalAccount.swift
|
||||||
|
// Nextcloud Cookbook iOS Client
|
||||||
|
//
|
||||||
|
// Created by Vincent Meilinger on 16.04.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
@@ -1,26 +1,14 @@
|
|||||||
//
|
//
|
||||||
// DataModels.swift
|
// Models.swift
|
||||||
// Nextcloud Cookbook iOS Client
|
// Nextcloud Cookbook iOS Client
|
||||||
//
|
//
|
||||||
// Created by Vincent Meilinger on 15.09.23.
|
// Created by Vincent Meilinger on 11.05.24.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
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 status: String
|
||||||
let statuscode: Int
|
let statuscode: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
struct Recipe: Codable {
|
struct CookbookApiRecipeV1: Codable {
|
||||||
let name: String
|
let name: String
|
||||||
let keywords: String?
|
let keywords: String?
|
||||||
let dateCreated: String?
|
let dateCreated: String?
|
||||||
@@ -27,12 +27,12 @@ struct Recipe: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension Recipe: Identifiable, Hashable {
|
extension CookbookApiRecipeV1: Identifiable, Hashable {
|
||||||
var id: String { name }
|
var id: String { name }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct RecipeDetail: Codable {
|
struct CookbookApiRecipeDetailV1: Codable {
|
||||||
var name: String
|
var name: String
|
||||||
var keywords: String
|
var keywords: String
|
||||||
var dateCreated: String?
|
var dateCreated: String?
|
||||||
@@ -120,9 +120,9 @@ struct RecipeDetail: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension RecipeDetail {
|
extension CookbookApiRecipeDetailV1 {
|
||||||
static var error: RecipeDetail {
|
static var error: CookbookApiRecipeDetailV1 {
|
||||||
return RecipeDetail(
|
return CookbookApiRecipeDetailV1(
|
||||||
name: "Error: Unable to load recipe.",
|
name: "Error: Unable to load recipe.",
|
||||||
keywords: "",
|
keywords: "",
|
||||||
dateCreated: "",
|
dateCreated: "",
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// CookbookProtocols.swift
|
||||||
|
// Nextcloud Cookbook iOS Client
|
||||||
|
//
|
||||||
|
// Created by Vincent Meilinger on 11.05.24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// Account.swift
|
||||||
|
// Nextcloud Cookbook iOS Client
|
||||||
|
//
|
||||||
|
// Created by Vincent Meilinger on 24.01.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
244
Nextcloud Cookbook iOS Client/Persistence/CookbookState.swift
Normal file
244
Nextcloud Cookbook iOS Client/Persistence/CookbookState.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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?)
|
||||||
|
}
|
||||||
@@ -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<T: Codable>(path ldPath: LocalDataPath) async -> T? {
|
||||||
|
do {
|
||||||
|
return try await DataStore.shared.load(fromPath: ldPath.path)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func save<T: Codable>(_ 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user