From 23e1a665df5991ccfbe3c824dbd35fe837c2d875 Mon Sep 17 00:00:00 2001 From: Vicnet <35202538+VincentMeilinger@users.noreply.github.com> Date: Thu, 16 Nov 2023 17:53:30 +0100 Subject: [PATCH] Simpler api endpoints are now integrated into the MainViewModel --- .../Data/UserSettings.swift | 10 +- .../Localizable.xcstrings | 18 ++ .../Network/CookbookApi/ApiRequest.swift | 8 +- .../Network/CookbookApi/CookbookApi.swift | 8 +- .../Network/CookbookApi/CookbookApiV1.swift | 11 +- .../Nextcloud_Cookbook_iOS_ClientApp.swift | 6 +- .../ViewModels/MainViewModel.swift | 224 +++++++++++++++--- .../ViewModels/RecipeEditViewModel.swift | 12 +- .../Views/CategoryDetailView.swift | 23 +- .../Views/MainView.swift | 8 +- .../Views/RecipeCardView.swift | 4 +- .../Views/RecipeDetailView.swift | 8 +- .../Views/RecipeEditView.swift | 18 +- 13 files changed, 284 insertions(+), 74 deletions(-) diff --git a/Nextcloud Cookbook iOS Client/Data/UserSettings.swift b/Nextcloud Cookbook iOS Client/Data/UserSettings.swift index c721b28..62b950b 100644 --- a/Nextcloud Cookbook iOS Client/Data/UserSettings.swift +++ b/Nextcloud Cookbook iOS Client/Data/UserSettings.swift @@ -13,14 +13,12 @@ class UserSettings: ObservableObject { @Published var username: String { didSet { UserDefaults.standard.set(username, forKey: "username") - self.authString = setAuthString() } } @Published var token: String { didSet { UserDefaults.standard.set(token, forKey: "token") - self.authString = setAuthString() } } @@ -69,6 +67,14 @@ class UserSettings: ObservableObject { self.defaultCategory = UserDefaults.standard.object(forKey: "defaultCategory") as? String ?? "" self.language = UserDefaults.standard.object(forKey: "language") as? String ?? SupportedLanguage.DEVICE.rawValue self.downloadRecipes = UserDefaults.standard.object(forKey: "downloadRecipes") as? Bool ?? false + + if authString == "" { + if token != "" && username != "" { + let loginString = "\(self.username):\(self.token)" + let loginData = loginString.data(using: String.Encoding.utf8)! + authString = loginData.base64EncodedString() + } + } } func setAuthString() -> String { diff --git a/Nextcloud Cookbook iOS Client/Localizable.xcstrings b/Nextcloud Cookbook iOS Client/Localizable.xcstrings index 5cb380f..5c125ed 100644 --- a/Nextcloud Cookbook iOS Client/Localizable.xcstrings +++ b/Nextcloud Cookbook iOS Client/Localizable.xcstrings @@ -292,6 +292,12 @@ } } } + }, + "Action completed." : { + + }, + "Action delayed" : { + }, "Add" : { "localizations" : { @@ -622,6 +628,9 @@ } } } + }, + "Could not establish a connection to the server. The action will be retried upon reconnection." : { + }, "Delete" : { "localizations" : { @@ -864,6 +873,9 @@ } } } + }, + "Error" : { + }, "Error." : { "localizations" : { @@ -1860,6 +1872,9 @@ } } } + }, + "Success" : { + }, "Support" : { "localizations" : { @@ -2080,6 +2095,9 @@ } } } + }, + "Unable to complete action." : { + }, "Unable to connect to server." : { "localizations" : { diff --git a/Nextcloud Cookbook iOS Client/Network/CookbookApi/ApiRequest.swift b/Nextcloud Cookbook iOS Client/Network/CookbookApi/ApiRequest.swift index bac5b58..ea6f272 100644 --- a/Nextcloud Cookbook iOS Client/Network/CookbookApi/ApiRequest.swift +++ b/Nextcloud Cookbook iOS Client/Network/CookbookApi/ApiRequest.swift @@ -40,8 +40,9 @@ struct ApiRequest { Logger.network.debug("\(method.rawValue) \(path) sending ...") // Prepare URL - let urlString = serverAddress + cookbookPath + path - Logger.network.debug("Full path: \(urlString)") + let urlString = "https://" + serverAddress + cookbookPath + path + print("Full path: \(urlString)") + //Logger.network.debug("Full path: \(urlString)") guard let urlStringSanitized = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return (nil, .unknownError) } guard let url = URL(string: urlStringSanitized) else { return (nil, .unknownError) } @@ -76,6 +77,9 @@ struct ApiRequest { do { (data, response) = try await URLSession.shared.data(for: request) Logger.network.debug("\(method.rawValue) \(path) SUCCESS!") + if let data = data { + print(data, String(data: data, encoding: .utf8)) + } return (data, nil) } catch { let error = decodeURLResponse(response: response as? HTTPURLResponse) diff --git a/Nextcloud Cookbook iOS Client/Network/CookbookApi/CookbookApi.swift b/Nextcloud Cookbook iOS Client/Network/CookbookApi/CookbookApi.swift index 37a070a..bb81234 100644 --- a/Nextcloud Cookbook iOS Client/Network/CookbookApi/CookbookApi.swift +++ b/Nextcloud Cookbook iOS Client/Network/CookbookApi/CookbookApi.swift @@ -49,7 +49,8 @@ protocol CookbookApi { /// - Returns: A NetworkError if the request fails. Nil otherwise. static func createRecipe( from serverAdress: String, - auth: String + auth: String, + recipe: RecipeDetail ) async -> (NetworkError?) /// Get the recipe with the specified id. @@ -94,7 +95,7 @@ protocol CookbookApi { static func getCategories( from serverAdress: String, auth: String - ) async -> ([String]?, NetworkError?) + ) async -> ([Category]?, NetworkError?) /// Get all recipes of a specified category. /// - Parameters: @@ -185,3 +186,6 @@ protocol CookbookApi { ) async -> (NetworkError?) } + + + diff --git a/Nextcloud Cookbook iOS Client/Network/CookbookApi/CookbookApiV1.swift b/Nextcloud Cookbook iOS Client/Network/CookbookApi/CookbookApiV1.swift index e1dadc6..2c473bc 100644 --- a/Nextcloud Cookbook iOS Client/Network/CookbookApi/CookbookApiV1.swift +++ b/Nextcloud Cookbook iOS Client/Network/CookbookApi/CookbookApiV1.swift @@ -43,13 +43,18 @@ class CookbookApiV1: CookbookApi { return (JSONDecoder.safeDecode(data), nil) } - static func createRecipe(from serverAdress: String, auth: String) async -> (NetworkError?) { + static func createRecipe(from serverAdress: String, auth: String, recipe: RecipeDetail) async -> (NetworkError?) { + guard let recipeData = JSONEncoder.safeEncode(recipe) else { + return .dataError + } + let request = ApiRequest( serverAdress: serverAdress, path: "/api/v1/recipes", method: .POST, authString: auth, - headerFields: [HeaderField.ocsRequest(value: true), HeaderField.accept(value: .JSON)] + headerFields: [HeaderField.ocsRequest(value: true), HeaderField.accept(value: .JSON)], + body: recipeData ) let (data, error) = await request.send() @@ -119,7 +124,7 @@ class CookbookApiV1: CookbookApi { return nil } - static func getCategories(from serverAdress: String, auth: String) async -> ([String]?, NetworkError?) { + static func getCategories(from serverAdress: String, auth: String) async -> ([Category]?, NetworkError?) { let request = ApiRequest( serverAdress: serverAdress, path: "/api/v1/categories", diff --git a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift index c6f45b2..25a31dd 100644 --- a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift +++ b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift @@ -10,7 +10,6 @@ import SwiftUI @main struct Nextcloud_Cookbook_iOS_ClientApp: App { @StateObject var userSettings = UserSettings() - @StateObject var mainViewModel = MainViewModel() var body: some Scene { WindowGroup { @@ -18,10 +17,7 @@ struct Nextcloud_Cookbook_iOS_ClientApp: App { if userSettings.onboarding { OnboardingView(userSettings: userSettings) } else { - MainView(viewModel: mainViewModel, userSettings: userSettings) - .onAppear { - mainViewModel.apiController = APIController(userSettings: userSettings) - } + MainView(userSettings: userSettings) } } .transition(.slide) diff --git a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift index 6f0a03b..b083e96 100644 --- a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift +++ b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift @@ -11,33 +11,30 @@ import UIKit @MainActor class MainViewModel: ObservableObject { + @AppStorage("authString") var authString = "" + @AppStorage("serverAddress") var serverAdress = "" + let api: CookbookApi.Type + @Published var categories: [Category] = [] @Published var recipes: [String: [Recipe]] = [:] - private var recipeDetails: [Int: RecipeDetail] = [:] + @Published var recipeDetails: [Int: RecipeDetail] = [:] private var imageCache: [Int: RecipeImage] = [:] private var requestQueue: [RequestWrapper] = [] + private var serverConnection: Bool = false let dataStore: DataStore - var apiController: APIController? = nil - /// The path of an image in storage - private var localImagePath: (Int, Bool) -> (String) = { recipeId, thumb in - return "image\(recipeId)_\(thumb ? "thumb" : "full")" - } - /// The path of an image on the server - private var networkImagePath: (Int, Bool) -> (String) = { recipeId, thumb in - return "recipes/\(recipeId)/image?size=\(thumb ? "thumb" : "full")" - } - - init() { + init(apiVersion api: CookbookApi.Type = CookbookApiV1.self) { + print("Created MainViewModel") + self.api = api self.dataStore = DataStore() } /// Try to load the category list from store or the server. /// - Parameters /// - needsUpdate: If true, the recipe will be loaded from the server directly, otherwise it will be loaded from store first. - func loadCategoryList(needsUpdate: Bool = false) async { + /*func loadCategoryList(needsUpdate: Bool = false) async { if let categoryList: [Category] = await loadObject( localPath: "categories.data", networkPath: .CATEGORIES, @@ -46,6 +43,23 @@ import UIKit self.categories = categoryList } print(self.categories) + }*/ + + func loadCategories() async { + let (categories, _) = await api.getCategories( + from: serverAdress, + auth: authString + ) + if let categories = categories { + self.categories = categories + await saveLocal(categories, path: "categories.data") + serverConnection = true + } else { + if let categories: [Category] = await loadLocal(path: "categories.data") { + self.categories = categories + } + serverConnection = false + } } /// Try to load the recipe list from store or the server. @@ -53,7 +67,7 @@ import UIKit /// - Parameters /// - categoryName: The name of the category containing the requested list of recipes. /// - needsUpdate: If true, the recipe will be loaded from the server directly, otherwise it will be loaded from store first. - func loadRecipeList(categoryName: String, needsUpdate: Bool = false) async { + /*func loadRecipeList(categoryName: String, needsUpdate: Bool = false) async { let categoryString = categoryName == "*" ? "_" : categoryName if let recipeList: [Recipe] = await loadObject( localPath: "category_\(categoryString).data", @@ -64,9 +78,24 @@ import UIKit print(recipeList) } + }*/ + func getCategory(named name: String) async { + let categoryString = name == "*" ? "_" : name + let (recipes, _) = await api.getCategory( + from: serverAdress, + auth: authString, + named: name + ) + if let recipes = recipes { + self.recipes[name] = recipes + } else { + if let recipes: [Recipe] = await loadLocal(path: "category_\(categoryString).data") { + self.recipes[name] = recipes + } + } } - func getAllRecipes() async -> [Recipe] { + /*func getAllRecipes() async -> [Recipe] { var allRecipes: [Recipe] = [] for category in categories { await loadRecipeList(categoryName: category.name) @@ -77,6 +106,26 @@ import UIKit return allRecipes.sorted(by: { $0.name < $1.name }) + }*/ + func getRecipes() async -> [Recipe] { + let (recipes, error) = await api.getRecipes( + from: serverAdress, + auth: authString + ) + if let recipes = recipes { + return recipes + } else if let error = error { + print(error) + } + var allRecipes: [Recipe] = [] + for category in categories { + if let recipeArray = self.recipes[category.name] { + allRecipes.append(contentsOf: recipeArray) + } + } + return allRecipes.sorted(by: { + $0.name < $1.name + }) } /// Try to load the recipe details from cache. If not found, try to load from store or the server. @@ -84,7 +133,7 @@ import UIKit /// - recipeId: The id of the recipe. /// - needsUpdate: If true, the recipe will be loaded from the server directly, otherwise it will be loaded from cache/store first. /// - Returns: RecipeDetail struct. If not found locally, and unable to load from server, a RecipeDetail struct containing an error message. - func loadRecipeDetail(recipeId: Int, needsUpdate: Bool = false) async -> RecipeDetail { + /*func loadRecipeDetail(recipeId: Int, needsUpdate: Bool = false) async -> RecipeDetail { if !needsUpdate { if let recipeDetail = recipeDetails[recipeId] { return recipeDetail @@ -99,19 +148,46 @@ import UIKit return recipeDetail } return RecipeDetail.error + }*/ + func getRecipe(id: Int) async -> RecipeDetail { + let (recipe, error) = await api.getRecipe( + from: serverAdress, + auth: authString, + id: id + ) + if let recipe = recipe { + return recipe + } else if let error = error { + print(error) + } + guard let recipe: RecipeDetail = await loadLocal(path: "recipe\(id).data") else { + return RecipeDetail.error + } + return recipe } func downloadAllRecipes() async { for category in categories { - await loadRecipeList(categoryName: category.name, needsUpdate: true) + await getCategory(named: category.name) guard let recipeList = recipes[category.name] else { continue } for recipe in recipeList { - let _ = await loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true) - let _ = await loadImage(recipeId: recipe.recipe_id, thumb: true) + let recipeDetail = await getRecipe(id: recipe.recipe_id) + await saveLocal(recipeDetail, path: "recipe\(recipe.recipe_id).data") + + let thumbnail = await getImage(id: recipe.recipe_id, size: .THUMB, needsUpdate: true) + guard let thumbnail = thumbnail else { continue } + guard let thumbnailData = thumbnail.pngData() else { continue } + await saveLocal(thumbnailData.base64EncodedString(), path: "image\(recipe.recipe_id)_thumb") + + let image = await getImage(id: recipe.recipe_id, size: .FULL, needsUpdate: true) + guard let image = image else { continue } + guard let imageData = image.pngData() else { continue } + await saveLocal(imageData.base64EncodedString(), path: "image\(recipe.recipe_id)_full") } } } + /// Check if recipeDetail is stored locally, either in cache or on disk /// - Parameters /// - recipeId: The id of a recipe. @@ -132,7 +208,7 @@ import UIKit /// - full: If true, load the full resolution image. Otherwise, load a thumbnail-sized image. /// - needsUpdate: Determines wether the image should be loaded directly from the server, or if it should be loaded from cache/store first. /// - Returns: The image if found locally or on the server, otherwise nil. - func loadImage(recipeId: Int, thumb: Bool, needsUpdate: Bool = false) async -> UIImage? { + /*func loadImage(recipeId: Int, thumb: Bool, needsUpdate: Bool = false) async -> UIImage? { print("loadImage(recipeId: \(recipeId), thumb: \(thumb), needsUpdate: \(needsUpdate))") // If the image needs an update, request it from the server and overwrite the stored image if needsUpdate { @@ -151,6 +227,9 @@ import UIKit } } + + + // Check imageExists flag to detect if we attempted to load a non-existing image before. // This allows us to avoid sending requests to the server if we already know the recipe has no image. if imageCache[recipeId] != nil { @@ -188,9 +267,22 @@ import UIKit } imageCache[recipeId] = RecipeImage(imageExists: false) return nil + }*/ + func getImage(id: Int, size: RecipeImage.RecipeImageSize, needsUpdate: Bool) async -> UIImage? { + if !needsUpdate, let image = await imageFromStore(id: id, size: size) { + return image + } + let (image, _) = await api.getImage( + from: serverAdress, + auth: authString, + id: id, + size: size + ) + if let image = image { return image } + return await imageFromStore(id: id, size: size) } - func getKeywords() async -> [String] { + /*func getKeywords() async -> [String] { if let keywords: [RecipeKeyword] = await self.loadObject( localPath: "keywords.data", networkPath: .KEYWORDS, @@ -199,6 +291,21 @@ import UIKit return keywords.map { $0.name } } return [] + }*/ + func getKeywords() async -> [String] { + let (tags, error) = await api.getTags( + from: serverAdress, + auth: authString + ) + if let tags = tags { + return tags + } else if let error = error { + print(error) + } + if let keywords: [String] = await loadLocal(path: "keywords.data") { + return keywords + } + return [] } func deleteAllData() { @@ -211,7 +318,7 @@ import UIKit } } - func deleteRecipe(withId id: Int, categoryName: String) async -> RequestAlert { + /*func deleteRecipe(withId id: Int, categoryName: String) async -> RequestAlert { let request = RequestWrapper.customRequest( method: .DELETE, path: .RECIPE_DETAIL(recipeId: id), @@ -235,9 +342,29 @@ import UIKit requestQueue.append(request) return .REQUEST_DELAYED } + }*/ + func deleteRecipe(withId id: Int, categoryName: String) async -> RequestAlert { + let (error) = await api.deleteRecipe( + from: serverAdress, + auth: authString, + id: id + ) + + if let error = error { + return .REQUEST_DROPPED + } + let path = "recipe\(id).data" + dataStore.delete(path: path) + if recipes[categoryName] != nil { + recipes[categoryName]!.removeAll(where: { recipe in + recipe.recipe_id == id ? true : false + }) + recipeDetails.removeValue(forKey: id) + } + return .REQUEST_SUCCESS } - func checkServerConnection() async -> Bool { + /*func checkServerConnection() async -> Bool { guard let apiController = apiController else { return false } let req = RequestWrapper.customRequest( method: .GET, @@ -251,9 +378,21 @@ import UIKit return false } return true + }*/ + func checkServerConnection() async -> Bool { + let (categories, _) = await api.getCategories( + from: serverAdress, + auth: authString + ) + if let categories = categories { + self.categories = categories + await saveLocal(categories, path: "categories.data") + return true + } + return false } - func uploadRecipe(recipeDetail: RecipeDetail, createNew: Bool) async -> RequestAlert { + /*func uploadRecipe(recipeDetail: RecipeDetail, createNew: Bool) async -> RequestAlert { var path: RequestPath? = nil if createNew { path = .NEW_RECIPE @@ -280,9 +419,21 @@ import UIKit requestQueue.append(request) return .REQUEST_DELAYED } + }*/ + func uploadRecipe(recipeDetail: RecipeDetail, createNew: Bool) async -> RequestAlert { + let error = await api.createRecipe( + from: serverAdress, + auth: authString, + recipe: recipeDetail + ) + + if let error = error { + return .REQUEST_DROPPED + } + return .REQUEST_SUCCESS } - func sendRequest(_ request: RequestWrapper) async -> Bool { + /*func sendRequest(_ request: RequestWrapper) async -> Bool { guard let apiController = apiController else { return false } let (data, _): (Data?, Error?) = await apiController.sendDataRequest(request) guard let data = data else { return false } @@ -299,14 +450,27 @@ import UIKit print("Could not decode server response") } return false - } + }*/ } extension MainViewModel { - private func loadObject(localPath: String, networkPath: RequestPath, needsUpdate: Bool = false) async -> T? { + func loadLocal(path: String) async -> T? { + do { + return try await dataStore.load(fromPath: path) + } catch (let error) { + print(error) + return nil + } + } + + func saveLocal(_ object: T, path: String) async { + guard let data = JSONEncoder.safeEncode(object) else { return } + await dataStore.save(data: data, toPath: path) + } + /*private func loadObject(localPath: String, networkPath: RequestPath, needsUpdate: Bool = false) async -> T? { do { if !needsUpdate, let data: T = try await dataStore.load(fromPath: localPath) { print("Data found locally.") @@ -350,10 +514,10 @@ extension MainViewModel { } return nil } - - private func imageFromStore(recipeId: Int, thumb: Bool) async -> UIImage? { + */ + private func imageFromStore(id: Int, size: RecipeImage.RecipeImageSize) async -> UIImage? { do { - let localPath = localImagePath(recipeId, thumb) + let localPath = "image\(id)_\(size == .FULL ? "full" : "thumb")" if let data: String = try await dataStore.load(fromPath: localPath) { guard let dataDecoded = Data(base64Encoded: data) else { return nil } let image = UIImage(data: dataDecoded) diff --git a/Nextcloud Cookbook iOS Client/ViewModels/RecipeEditViewModel.swift b/Nextcloud Cookbook iOS Client/ViewModels/RecipeEditViewModel.swift index c4685cb..ce38cf2 100644 --- a/Nextcloud Cookbook iOS Client/ViewModels/RecipeEditViewModel.swift +++ b/Nextcloud Cookbook iOS Client/ViewModels/RecipeEditViewModel.swift @@ -26,7 +26,7 @@ import SwiftUI @Published var presentAlert = false var alertType: UserAlert = RecipeCreationError.GENERIC - var alertAction: @MainActor () -> () = {} + var alertAction: @MainActor () async -> (RequestAlert) = { return .REQUEST_DROPPED } var uploadNew: Bool = true var waitingForUpload: Bool = false @@ -57,7 +57,7 @@ import SwiftUI // Check if the recipe has a name if recipe.name.replacingOccurrences(of: " ", with: "") == "" { alertType = RecipeCreationError.NO_TITLE - alertAction = {} + alertAction = {return .REQUEST_DROPPED} presentAlert = true return false } @@ -72,7 +72,7 @@ import SwiftUI .lowercased() { alertType = RecipeCreationError.DUPLICATE - alertAction = {} + alertAction = {return .REQUEST_DROPPED} presentAlert = true return false } @@ -111,8 +111,8 @@ import SwiftUI func dismissEditView() { Task { - await mainViewModel.loadCategoryList(needsUpdate: true) - await mainViewModel.loadRecipeList(categoryName: recipe.recipeCategory, needsUpdate: true) + await mainViewModel.loadCategories() //loadCategoryList(needsUpdate: true) + await mainViewModel.getCategory(named: recipe.recipeCategory)//.loadRecipeList(categoryName: recipe.recipeCategory, needsUpdate: true) } isPresented.wrappedValue = false } @@ -140,7 +140,7 @@ import SwiftUI } if let error = error { self.alertType = error - self.alertAction = {} + self.alertAction = {return .REQUEST_DROPPED} self.presentAlert = true } } catch { diff --git a/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift b/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift index 94d5ede..deef201 100644 --- a/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift +++ b/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift @@ -61,10 +61,10 @@ struct CategoryDetailView: View { } .searchable(text: $searchText, prompt: "Search recipes") .task { - await viewModel.loadRecipeList(categoryName: categoryName) + await viewModel.getCategory(named: categoryName)//.loadRecipeList(categoryName: categoryName) } .refreshable { - await viewModel.loadRecipeList(categoryName: categoryName, needsUpdate: true) + await viewModel.getCategory(named: categoryName)//.loadRecipeList(categoryName: categoryName, needsUpdate: true) } } @@ -79,13 +79,20 @@ struct CategoryDetailView: View { func downloadRecipes() { if let recipes = viewModel.recipes[categoryName] { - let dispatchQueue = DispatchQueue(label: "RecipeDownload", qos: .background) - dispatchQueue.async { + Task { for recipe in recipes { - Task { - let _ = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id) - let _ = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: false) - } + let recipeDetail = await viewModel.getRecipe(id: recipe.recipe_id) + await viewModel.saveLocal(recipeDetail, path: "recipe\(recipe.recipe_id).data") + + let thumbnail = await viewModel.getImage(id: recipe.recipe_id, size: .THUMB, needsUpdate: true) + guard let thumbnail = thumbnail else { continue } + guard let thumbnailData = thumbnail.pngData() else { continue } + await viewModel.saveLocal(thumbnailData.base64EncodedString(), path: "image\(recipe.recipe_id)_thumb") + + let image = await viewModel.getImage(id: recipe.recipe_id, size: .FULL, needsUpdate: true) + guard let image = image else { continue } + guard let imageData = image.pngData() else { continue } + await viewModel.saveLocal(imageData.base64EncodedString(), path: "image\(recipe.recipe_id)_full") } } } diff --git a/Nextcloud Cookbook iOS Client/Views/MainView.swift b/Nextcloud Cookbook iOS Client/Views/MainView.swift index c9e0b3e..136a590 100644 --- a/Nextcloud Cookbook iOS Client/Views/MainView.swift +++ b/Nextcloud Cookbook iOS Client/Views/MainView.swift @@ -9,8 +9,8 @@ import SwiftUI struct MainView: View { - @ObservedObject var viewModel: MainViewModel @ObservedObject var userSettings: UserSettings + @StateObject var viewModel = MainViewModel() @State private var selectedCategory: Category? = nil @State private var showEditView: Bool = false @@ -90,7 +90,7 @@ struct MainView: View { } .task { self.serverConnection = await viewModel.checkServerConnection() - await viewModel.loadCategoryList() + await viewModel.loadCategories()//viewModel.loadCategoryList() // Open detail view for default category if userSettings.defaultCategory != "" { if let cat = viewModel.categories.first(where: { c in @@ -105,7 +105,7 @@ struct MainView: View { } .refreshable { self.serverConnection = await viewModel.checkServerConnection() - await viewModel.loadCategoryList(needsUpdate: true) + await viewModel.loadCategories()//loadCategoryList(needsUpdate: true) } } @@ -208,7 +208,7 @@ struct RecipeSearchView: View { .navigationTitle("Search recipe") } .task { - allRecipes = await viewModel.getAllRecipes() + allRecipes = await viewModel.getRecipes()//.getAllRecipes() } } diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift index f5a7fd6..57e7b84 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift @@ -51,11 +51,11 @@ struct RecipeCardView: View { .clipShape(RoundedRectangle(cornerRadius: 17)) .padding(.horizontal) .task { - recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: true) + recipeThumb = await viewModel.getImage(id: recipe.recipe_id, size: .THUMB, needsUpdate: false)//loadImage(recipeId: recipe.recipe_id, thumb: true) self.isDownloaded = viewModel.recipeDetailExists(recipeId: recipe.recipe_id) } .refreshable { - recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: true, needsUpdate: true) + recipeThumb = await viewModel.getImage(id: recipe.recipe_id, size: .THUMB, needsUpdate: true)//.loadImage(recipeId: recipe.recipe_id, thumb: true, needsUpdate: true) } } } diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift index 0f6f2d0..cb99fbf 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift @@ -106,13 +106,13 @@ struct RecipeDetailView: View { } } .task { - recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id) - recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: false) + recipeDetail = await viewModel.getRecipe(id: recipe.recipe_id)//loadRecipeDetail(recipeId: recipe.recipe_id) + recipeImage = await viewModel.getImage(id: recipe.recipe_id, size: .FULL, needsUpdate: false)//.loadImage(recipeId: recipe.recipe_id, thumb: false) self.isDownloaded = viewModel.recipeDetailExists(recipeId: recipe.recipe_id) } .refreshable { - recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true) - recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: false, needsUpdate: true) + recipeDetail = await viewModel.getRecipe(id: recipe.recipe_id)//.loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true) + recipeImage = await viewModel.getImage(id: recipe.recipe_id, size: .FULL, needsUpdate: true)//.loadImage(recipeId: recipe.recipe_id, thumb: false, needsUpdate: true) } } } diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift index 323d081..dd830c4 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift @@ -45,10 +45,12 @@ struct RecipeEditView: View { } Spacer() Button() { - if viewModel.uploadNew { - viewModel.uploadNewRecipe() - } else { - viewModel.uploadEditedRecipe() + Task { + if viewModel.uploadNew { + await viewModel.uploadNewRecipe() + } else { + await viewModel.uploadEditedRecipe() + } } } label: { Text("Upload") @@ -150,13 +152,17 @@ struct RecipeEditView: View { ForEach(viewModel.alertType.alertButtons) { buttonType in if buttonType == .OK { Button(AlertButton.OK.rawValue, role: .cancel) { - viewModel.alertAction() + Task { + await viewModel.alertAction() + } } } else if buttonType == .CANCEL { Button(AlertButton.CANCEL.rawValue, role: .cancel) { } } else if buttonType == .DELETE { Button(AlertButton.DELETE.rawValue, role: .destructive) { - viewModel.alertAction() + Task { + await viewModel.alertAction() + } } } }