WIP - Complete App refactoring

This commit is contained in:
VincentMeilinger
2025-05-26 15:52:24 +02:00
parent 29fd3c668b
commit 5acf3b9c4f
49 changed files with 1996 additions and 543 deletions

View File

@@ -1,5 +1,5 @@
//
// AccountState.swift
// CookbookState.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 29.05.24.
@@ -7,13 +7,10 @@
import Foundation
import SwiftUI
/*
@Observable
class CookbookState {
let id = UUID()
let account: Account? = nil
/// Caches recipe categories.
var categories: [Category] = []
@@ -33,212 +30,193 @@ class CookbookState {
var keywords: [RecipeKeyword] = []
/// Read and write interfaces.
var readLocal: ReadInterface
var writeLocal: WriteInterface
var readRemote: [ReadInterface]?
var writeRemote: [WriteInterface]?
var localOnly: Bool = false
var localReadInterface: ReadInterface
var localWriteInterface: WriteInterface
var remoteReadInterface: ReadInterface?
var remoteWriteInterface: WriteInterface?
/// UI state variables
var selectedCategory: Category? = nil
var selectedRecipe: RecipeStub? = nil
var navigationPath: NavigationPath = NavigationPath()
var selectedRecipeStub: RecipeStub? = nil
var showSettings: Bool = false
var showGroceries: Bool = false
/// Grocery List
var groceryList = GroceryList()
init(
readLocal: ReadInterface,
writeLocal: WriteInterface,
readRemote: [ReadInterface] = [],
writeRemote: [WriteInterface] = []
localReadInterface: ReadInterface,
localWriteInterface: WriteInterface,
remoteReadInterface: ReadInterface? = nil,
remoteWriteInterface: WriteInterface? = nil
) {
self.readLocal = readLocal
self.writeLocal = writeLocal
self.readRemote = readRemote
self.writeRemote = writeRemote
self.localReadInterface = localReadInterface
self.localWriteInterface = localWriteInterface
self.remoteReadInterface = remoteReadInterface
self.remoteWriteInterface = remoteWriteInterface
}
init() {
let accountLoader = AccountLoader()
rI, wI = accountLoader.load
}
}
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() {
func loadCategories(remoteFirst: Bool = false) async {
if remoteFirst {
if let remoteReadInterface, let categories = await remoteReadInterface.getCategories() {
self.categories = categories
return categories
return
}
if let categories = await localReadInterface.getCategories() {
self.categories = categories
return
}
} else {
if let categories = await localReadInterface.getCategories() {
self.categories = categories
return
}
guard let remoteReadInterface else { return }
if let categories = await remoteReadInterface.getCategories() {
self.categories = categories
return
}
}
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
func loadRecipeStubs(category: String, remoteFirst: Bool = false) async {
if remoteFirst {
if let remoteReadInterface, let stubs = await remoteReadInterface.getRecipeStubs() {
self.recipeStubs[category] = stubs
return
}
if let stubs = await localReadInterface.getRecipeStubs() {
self.recipeStubs[category] = stubs
return
}
} else {
if let stubs = await localReadInterface.getRecipeStubs() {
self.recipeStubs[category] = stubs
return
}
guard let remoteReadInterface else { return }
if let stubs = await remoteReadInterface.getRecipeStubs() {
self.recipeStubs[category] = stubs
return
}
}
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
func loadKeywords(remoteFirst: Bool = false) async {
if remoteFirst {
if let remoteReadInterface, let keywords = await remoteReadInterface.getTags() {
self.keywords = keywords
return
}
if let keywords = await localReadInterface.getTags() {
self.keywords = keywords
return
}
} else {
if let keywords = await localReadInterface.getTags() {
self.keywords = keywords
return
}
guard let remoteReadInterface else { return }
if let keywords = await remoteReadInterface.getTags() {
self.keywords = keywords
return
}
}
return await readLocal.getTags()
}
func getRecipesTagged(keyword: String) async -> [RecipeStub]? {
if !localOnly, let readRemote {
if let stubs = await readRemote.getRecipesTagged(keyword: keyword) {
return stubs
func loadRecipe(id: String, remoteFirst: Bool = false) async {
if remoteFirst {
if let remoteReadInterface, let recipe = await remoteReadInterface.getRecipe(id: id) {
self.recipes[id] = recipe
return
}
if let recipe = await localReadInterface.getRecipe(id: id) {
self.recipes[id] = recipe
return
}
} else {
if let recipe = await localReadInterface.getRecipe(id: id) {
self.recipes[id] = recipe
return
}
guard let remoteReadInterface else { return }
if let recipe = await remoteReadInterface.getRecipe(id: id) {
self.recipes[id] = recipe
return
}
}
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
class AccountLoader {
func initInterfaces() async -> [ReadInterface & WriteInterface] {
let accounts = await self.loadAccounts("accounts.data")
if accounts.isEmpty && UserSettings.shared.serverAddress != "" {
print("Creating new Account from legacy Cookbook client account.")
let auth = Authentication(
baseUrl: UserSettings.shared.serverAddress,
user: UserSettings.shared.username,
token: UserSettings.shared.authString
)
let authKey = "legacyNextcloud"
let legacyAccount = Account(
id: UUID(),
name: "Nextcloud",
type: .nextcloud,
apiVersion: "1.0",
authKey: authKey
)
await saveAccounts([legacyAccount], "accounts.data")
legacyAccount.storeAuth(auth)
let interface = NextcloudDataInterface(auth: auth, version: legacyAccount.apiVersion)
return [interface]
} else {
print("Recovering existing accounts.")
var interfaces: [ReadInterface & WriteInterface] = []
for account in accounts {
if let interface: CookbookInterface = account.getInterface() {
interfaces.append(interface)
}
}
return interfaces
}
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
func loadAccounts(_ path: String) async -> [Account] {
do {
return try await DataStore.shared.load(fromPath: path) ?? []
} catch (let error) {
print(error)
return []
}
}
func saveAccounts(_ accounts: [Account], _ path: String) async {
await DataStore.shared.save(data: accounts, toPath: path)
}
}
*/