Better API call handling.

This commit is contained in:
Vicnet
2023-11-17 12:47:21 +01:00
parent 23e1a665df
commit 5e4b87b201
9 changed files with 286 additions and 346 deletions

View File

@@ -72,7 +72,8 @@ protocol CookbookApi {
/// - Returns: A NetworkError if the request fails. Nil otherwise. /// - Returns: A NetworkError if the request fails. Nil otherwise.
static func updateRecipe( static func updateRecipe(
from serverAdress: String, from serverAdress: String,
auth: String, id: Int auth: String,
recipe: RecipeDetail
) async -> (NetworkError?) ) async -> (NetworkError?)
/// Delete the recipe with the specified id. /// Delete the recipe with the specified id.

View File

@@ -86,13 +86,17 @@ class CookbookApiV1: CookbookApi {
return (JSONDecoder.safeDecode(data), nil) return (JSONDecoder.safeDecode(data), nil)
} }
static func updateRecipe(from serverAdress: String, auth: String, id: Int) async -> (NetworkError?) { static func updateRecipe(from serverAdress: String, auth: String, recipe: RecipeDetail) async -> (NetworkError?) {
guard let recipeData = JSONEncoder.safeEncode(recipe) else {
return .dataError
}
let request = ApiRequest( let request = ApiRequest(
serverAdress: serverAdress, serverAdress: serverAdress,
path: "/api/v1/recipes/\(id)", path: "/api/v1/recipes/\(recipe.id)",
method: .PUT, method: .PUT,
authString: auth, 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() let (data, error) = await request.send()

View File

@@ -13,17 +13,15 @@ import UIKit
@MainActor class MainViewModel: ObservableObject { @MainActor class MainViewModel: ObservableObject {
@AppStorage("authString") var authString = "" @AppStorage("authString") var authString = ""
@AppStorage("serverAddress") var serverAdress = "" @AppStorage("serverAddress") var serverAdress = ""
let api: CookbookApi.Type
@Published var categories: [Category] = [] @Published var categories: [Category] = []
@Published var recipes: [String: [Recipe]] = [:] @Published var recipes: [String: [Recipe]] = [:]
@Published var recipeDetails: [Int: RecipeDetail] = [:] @Published var recipeDetails: [Int: RecipeDetail] = [:]
private var imageCache: [Int: RecipeImage] = [:]
private var requestQueue: [RequestWrapper] = [] private var requestQueue: [RequestWrapper] = []
private var serverConnection: Bool = false
let dataStore: DataStore
private let api: CookbookApi.Type
private let dataStore: DataStore
init(apiVersion api: CookbookApi.Type = CookbookApiV1.self) { init(apiVersion api: CookbookApi.Type = CookbookApiV1.self) {
print("Created MainViewModel") print("Created MainViewModel")
@@ -31,21 +29,19 @@ import UIKit
self.dataStore = DataStore() self.dataStore = DataStore()
} }
/// Try to load the category list from store or the server. enum FetchMode {
/// - Parameters case preferLocal, preferServer, onlyLocal, onlyServer
/// - 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 {
if let categoryList: [Category] = await loadObject(
localPath: "categories.data",
networkPath: .CATEGORIES,
needsUpdate: needsUpdate
) {
self.categories = categoryList
}
print(self.categories)
}*/
func loadCategories() async {
/**
Asynchronously loads and updates the list of categories.
This function attempts to fetch the list of categories from the server. If the server connection is successful, it updates the `categories` property in the `MainViewModel` instance and saves the categories locally. If the server connection fails, it attempts to load the categories from local storage.
- Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
*/
func getCategories() async {
let (categories, _) = await api.getCategories( let (categories, _) = await api.getCategories(
from: serverAdress, from: serverAdress,
auth: authString auth: authString
@@ -53,60 +49,76 @@ import UIKit
if let categories = categories { if let categories = categories {
self.categories = categories self.categories = categories
await saveLocal(categories, path: "categories.data") await saveLocal(categories, path: "categories.data")
serverConnection = true
} else { } else {
// If there's no server connection, try loading categories from local storage
if let categories: [Category] = await loadLocal(path: "categories.data") { if let categories: [Category] = await loadLocal(path: "categories.data") {
self.categories = categories self.categories = categories
} }
serverConnection = false
} }
} }
/// Try to load the recipe list from store or the server.
/// - Warning: The category named '\*' is translated into '\_' for network calls and storage requests in this function. This is necessary for the nextcloud cookbook api.
/// - 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 {
let categoryString = categoryName == "*" ? "_" : categoryName
if let recipeList: [Recipe] = await loadObject(
localPath: "category_\(categoryString).data",
networkPath: .RECIPE_LIST(categoryName: categoryString),
needsUpdate: needsUpdate
) {
recipes[categoryName] = recipeList
print(recipeList)
}
}*/ /**
func getCategory(named name: String) async { Fetches recipes for a specified category from either the server or local storage.
let categoryString = name == "*" ? "_" : name
let (recipes, _) = await api.getCategory( - Parameters:
from: serverAdress, - name: The name of the category. Use "*" to fetch recipes without assigned categories.
auth: authString, - needsUpdate: If true, recipes will be loaded from the server directly; otherwise, they will be loaded from local storage first.
named: name
) This function asynchronously retrieves recipes for the specified category from the server or local storage based on the provided parameters. If `needsUpdate` is true, the function fetches recipes from the server and updates the local storage. If `needsUpdate` is false, it attempts to load recipes from local storage.
if let recipes = recipes {
self.recipes[name] = recipes - Note: The category name "*" is used for all uncategorized recipes.
} else {
- Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
*/
func getCategory(named name: String, fetchMode: FetchMode) async {
func getLocal() async -> Bool {
if let recipes: [Recipe] = await loadLocal(path: "category_\(categoryString).data") { if let recipes: [Recipe] = await loadLocal(path: "category_\(categoryString).data") {
self.recipes[name] = recipes self.recipes[name] = recipes
return true
} }
return false
}
func getServer() async -> Bool {
let (recipes, _) = await api.getCategory(
from: serverAdress,
auth: authString,
named: name
)
if let recipes = recipes {
self.recipes[name] = recipes
return true
}
return false
}
let categoryString = name == "*" ? "_" : name
switch fetchMode {
case .preferLocal:
if await getLocal() { return }
if await getServer() { return }
case .preferServer:
if await getServer() { return }
if await getLocal() { return }
case .onlyLocal:
if await getLocal() { return }
case .onlyServer:
if await getServer() { return }
} }
} }
/*func getAllRecipes() async -> [Recipe] { /**
var allRecipes: [Recipe] = [] Asynchronously retrieves all recipes either from the server or the locally cached data.
for category in categories {
await loadRecipeList(categoryName: category.name) This function attempts to fetch all recipes from the server using the provided `api`. If the server connection is successful, it returns the fetched recipes. If the server connection fails, it falls back to combining locally cached recipes from different categories.
if let recipeArray = recipes[category.name] {
allRecipes.append(contentsOf: recipeArray) - Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance, and categories have been previously loaded.
}
} Example usage:
return allRecipes.sorted(by: { ```swift
$0.name < $1.name let recipes = await mainViewModel.getRecipes()
}) */
}*/
func getRecipes() async -> [Recipe] { func getRecipes() async -> [Recipe] {
let (recipes, error) = await api.getRecipes( let (recipes, error) = await api.getRecipes(
from: serverAdress, from: serverAdress,
@@ -128,58 +140,85 @@ import UIKit
}) })
} }
/// Try to load the recipe details from cache. If not found, try to load from store or the server. /**
/// - Parameters Asynchronously retrieves a recipe detail either from the server or locally cached data.
/// - 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. This function attempts to fetch a recipe detail with the specified `id` from the server using the provided `api`. If the server connection is successful, it returns the fetched recipe detail. If the server connection fails, it falls back to loading the recipe detail from local storage.
/// - 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 { - Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
if !needsUpdate {
if let recipeDetail = recipeDetails[recipeId] { - Parameters:
return recipeDetail - id: The identifier of the recipe to retrieve.
Example usage:
```swift
let recipeDetail = await mainViewModel.getRecipe(id: 123)
*/
func getRecipe(id: Int, fetchMode: FetchMode) async -> RecipeDetail {
func getLocal() async -> RecipeDetail? {
if let recipe: RecipeDetail = await loadLocal(path: "recipe\(id).data") {
return recipe
} }
return nil
} }
if let recipeDetail: RecipeDetail = await loadObject(
localPath: "recipe\(recipeId).data", func getServer() async -> RecipeDetail? {
networkPath: .RECIPE_DETAIL(recipeId: recipeId), let (recipe, error) = await api.getRecipe(
needsUpdate: needsUpdate from: serverAdress,
) { auth: authString,
recipeDetails[recipeId] = recipeDetail id: id
return recipeDetail )
if let recipe = recipe {
return recipe
} else if let error = error {
print(error)
}
return nil
} }
return RecipeDetail.error
}*/ switch fetchMode {
func getRecipe(id: Int) async -> RecipeDetail { case .preferLocal:
let (recipe, error) = await api.getRecipe( if let recipe = await getLocal() { return recipe }
from: serverAdress, if let recipe = await getServer() { return recipe }
auth: authString, case .preferServer:
id: id if let recipe = await getServer() { return recipe }
) if let recipe = await getLocal() { return recipe }
if let recipe = recipe { case .onlyLocal:
return recipe if let recipe = await getLocal() { return recipe }
} else if let error = error { case .onlyServer:
print(error) if let recipe = await getServer() { return recipe }
} }
guard let recipe: RecipeDetail = await loadLocal(path: "recipe\(id).data") else { return .error
return RecipeDetail.error
}
return recipe
} }
/**
Asynchronously downloads and saves details, thumbnails, and full images for all recipes.
This function iterates through all loaded categories, fetches and updates the recipes from the server, and then downloads and saves details, thumbnails, and full images for each recipe.
- Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
Example usage:
```swift
await mainViewModel.downloadAllRecipes()
*/
func downloadAllRecipes() async { func downloadAllRecipes() async {
for category in categories { for category in categories {
await getCategory(named: category.name) await getCategory(named: category.name, fetchMode: .onlyServer)
guard let recipeList = recipes[category.name] else { continue } guard let recipeList = recipes[category.name] else { continue }
for recipe in recipeList { for recipe in recipeList {
let recipeDetail = await getRecipe(id: recipe.recipe_id) let recipeDetail = await getRecipe(id: recipe.recipe_id, fetchMode: .onlyServer)
await saveLocal(recipeDetail, path: "recipe\(recipe.recipe_id).data") await saveLocal(recipeDetail, path: "recipe\(recipe.recipe_id).data")
let thumbnail = await getImage(id: recipe.recipe_id, size: .THUMB, needsUpdate: true) let thumbnail = await getImage(id: recipe.recipe_id, size: .THUMB, fetchMode: .onlyServer)
guard let thumbnail = thumbnail else { continue } guard let thumbnail = thumbnail else { continue }
guard let thumbnailData = thumbnail.pngData() else { continue } guard let thumbnailData = thumbnail.pngData() else { continue }
await saveLocal(thumbnailData.base64EncodedString(), path: "image\(recipe.recipe_id)_thumb") await saveLocal(thumbnailData.base64EncodedString(), path: "image\(recipe.recipe_id)_thumb")
let image = await getImage(id: recipe.recipe_id, size: .FULL, needsUpdate: true) let image = await getImage(id: recipe.recipe_id, size: .FULL, fetchMode: .onlyServer)
guard let image = image else { continue } guard let image = image else { continue }
guard let imageData = image.pngData() else { continue } guard let imageData = image.pngData() else { continue }
await saveLocal(imageData.base64EncodedString(), path: "image\(recipe.recipe_id)_full") await saveLocal(imageData.base64EncodedString(), path: "image\(recipe.recipe_id)_full")
@@ -201,109 +240,88 @@ import UIKit
return false return false
} }
/**
Asynchronously retrieves and returns an image for a recipe with the specified ID and size.
/// Try to load the recipe image from cache. If not found, try to load from store or the server. This function attempts to fetch an image for a recipe with the specified `id` and `size` from the server using the provided `api`. If the server connection is successful, it returns the fetched image. If the server connection fails or `needsUpdate` is false, it attempts to load the image from local storage.
/// - Parameters
/// - recipeId: The id of a recipe. - Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
/// - 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. - Parameters:
/// - Returns: The image if found locally or on the server, otherwise nil. - id: The identifier of the recipe associated with the image.
/*func loadImage(recipeId: Int, thumb: Bool, needsUpdate: Bool = false) async -> UIImage? { - size: The size of the desired image (thumbnail or full).
print("loadImage(recipeId: \(recipeId), thumb: \(thumb), needsUpdate: \(needsUpdate))") - needsUpdate: If true, the image will be loaded from the server directly; otherwise, it will be loaded from local storage.
// If the image needs an update, request it from the server and overwrite the stored image
if needsUpdate { Example usage:
guard let apiController = apiController else { return nil } ```swift
if let data = await apiController.imageDataFromServer(recipeId: recipeId, thumb: thumb) { let thumbnail = await mainViewModel.getImage(id: 123, size: .THUMB, needsUpdate: true)
guard let image = UIImage(data: data) else { */
imageCache[recipeId] = RecipeImage(imageExists: false) func getImage(id: Int, size: RecipeImage.RecipeImageSize, fetchMode: FetchMode) async -> UIImage? {
return nil func getLocal() async -> UIImage? {
} return await imageFromStore(id: id, size: size)
await dataStore.save(data: data.base64EncodedString(), toPath: localImagePath(recipeId, thumb))
imageToCache(image: image, recipeId: recipeId, thumb: thumb)
return image
} else {
imageCache[recipeId] = RecipeImage(imageExists: false)
return nil
}
} }
func getServer() async -> UIImage? {
let (image, _) = await api.getImage(
from: serverAdress,
// Check imageExists flag to detect if we attempted to load a non-existing image before. auth: authString,
// This allows us to avoid sending requests to the server if we already know the recipe has no image. id: id,
if imageCache[recipeId] != nil { size: size
guard imageCache[recipeId]!.imageExists else { return nil } )
if let image = image { return image }
return nil
} }
// Try to load image from cache switch fetchMode {
print("Attempting to load image from cache ...") case .preferLocal:
if let image = imageFromCache(recipeId: recipeId, thumb: thumb) { if let image = await getLocal() { return image }
print("Image found in cache.") if let image = await getServer() { return image }
return image case .preferServer:
if let image = await getServer() { return image }
if let image = await getLocal() { return image }
case .onlyLocal:
if let image = await getLocal() { return image }
case .onlyServer:
if let image = await getServer() { return image }
} }
// Try to load from store
print("Attempting to load image from local storage ...")
if let image = await imageFromStore(recipeId: recipeId, thumb: thumb) {
print("Image found in local storage.")
imageToCache(image: image, recipeId: recipeId, thumb: thumb)
return image
}
// Try to load from the server. Store if successfull.
print("Attempting to load image from server ...")
guard let apiController = apiController else { return nil }
if let data = await apiController.imageDataFromServer(recipeId: recipeId, thumb: thumb) {
print("Image data received.")
// Create empty RecipeImage for each recipe even if no image found, so that further server requests are only sent if explicitly requested.
guard let image = UIImage(data: data) else {
imageCache[recipeId] = RecipeImage(imageExists: false)
return nil
}
await dataStore.save(data: data.base64EncodedString(), toPath: localImagePath(recipeId, thumb))
imageToCache(image: image, recipeId: recipeId, thumb: thumb)
return image
}
imageCache[recipeId] = RecipeImage(imageExists: false)
return nil 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] { /**
if let keywords: [RecipeKeyword] = await self.loadObject( Asynchronously retrieves and returns a list of keywords (tags).
localPath: "keywords.data",
networkPath: .KEYWORDS, This function attempts to fetch a list of keywords from the server using the provided `api`. If the server connection is successful, it returns the fetched keywords. If the server connection fails, it attempts to load the keywords from local storage.
needsUpdate: true
) { - Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
return keywords.map { $0.name }
Example usage:
```swift
let keywords = await mainViewModel.getKeywords()
*/
func getKeywords(fetchMode: FetchMode) async -> [String] {
func getLocal() async -> [String]? {
return await loadLocal(path: "keywords.data")
} }
return []
}*/ func getServer() async -> [String]? {
func getKeywords() async -> [String] { let (tags, _) = await api.getTags(
let (tags, error) = await api.getTags( from: serverAdress,
from: serverAdress, auth: authString
auth: authString )
)
if let tags = tags {
return tags return tags
} else if let error = error {
print(error)
} }
if let keywords: [String] = await loadLocal(path: "keywords.data") {
return keywords switch fetchMode {
case .preferLocal:
if let keywords = await getLocal() { return keywords }
if let keywords = await getServer() { return keywords }
case .preferServer:
if let keywords = await getServer() { return keywords }
if let keywords = await getLocal() { return keywords }
case .onlyLocal:
if let keywords = await getLocal() { return keywords }
case .onlyServer:
if let keywords = await getServer() { return keywords }
} }
return [] return []
} }
@@ -312,37 +330,26 @@ import UIKit
if dataStore.clearAll() { if dataStore.clearAll() {
self.categories = [] self.categories = []
self.recipes = [:] self.recipes = [:]
self.imageCache = [:]
self.recipeDetails = [:] self.recipeDetails = [:]
self.requestQueue = [] self.requestQueue = []
} }
} }
/*func deleteRecipe(withId id: Int, categoryName: String) async -> RequestAlert { /**
let request = RequestWrapper.customRequest( Asynchronously deletes a recipe with the specified ID from the server and local storage.
method: .DELETE,
path: .RECIPE_DETAIL(recipeId: id),
headerFields: [
HeaderField.accept(value: .JSON),
HeaderField.ocsRequest(value: true)
]
)
let path = "recipe\(id).data" This function attempts to delete a recipe with the specified `id` from the server using the provided `api`. If the server connection is successful, it proceeds to delete the local copy of the recipe and its details. If the server connection fails, it returns `RequestAlert.REQUEST_DROPPED`.
dataStore.delete(path: path)
if recipes[categoryName] != nil { - Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
recipes[categoryName]!.removeAll(where: { recipe in
recipe.recipe_id == id ? true : false - Parameters:
}) - id: The identifier of the recipe to delete.
recipeDetails.removeValue(forKey: id) - categoryName: The name of the category to which the recipe belongs.
}
if await sendRequest(request) { Example usage:
return .REQUEST_SUCCESS ```swift
} else { let requestResult = await mainViewModel.deleteRecipe(withId: 123, categoryName: "Desserts")
requestQueue.append(request) */
return .REQUEST_DELAYED
}
}*/
func deleteRecipe(withId id: Int, categoryName: String) async -> RequestAlert { func deleteRecipe(withId id: Int, categoryName: String) async -> RequestAlert {
let (error) = await api.deleteRecipe( let (error) = await api.deleteRecipe(
from: serverAdress, from: serverAdress,
@@ -364,21 +371,17 @@ import UIKit
return .REQUEST_SUCCESS return .REQUEST_SUCCESS
} }
/*func checkServerConnection() async -> Bool { /**
guard let apiController = apiController else { return false } Asynchronously checks the server connection by attempting to fetch categories.
let req = RequestWrapper.customRequest(
method: .GET, This function attempts to fetch categories from the server using the provided `api` to check the server connection status. If the server connection is successful, it updates the `categories` property in the `MainViewModel` instance and saves the categories locally. If the server connection fails, it returns `false`.
path: .CONFIG,
headerFields: [ - Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
.ocsRequest(value: true),
.accept(value: .JSON) Example usage:
] ```swift
) let isConnected = await mainViewModel.checkServerConnection()
if let error = await apiController.sendRequest(req) { */
return false
}
return true
}*/
func checkServerConnection() async -> Bool { func checkServerConnection() async -> Bool {
let (categories, _) = await api.getCategories( let (categories, _) = await api.getCategories(
from: serverAdress, from: serverAdress,
@@ -392,65 +395,41 @@ import UIKit
return false return false
} }
/*func uploadRecipe(recipeDetail: RecipeDetail, createNew: Bool) async -> RequestAlert { /**
var path: RequestPath? = nil Asynchronously uploads a recipe to the server.
if createNew {
path = .NEW_RECIPE
} else if let recipeId = Int(recipeDetail.id) {
path = .RECIPE_DETAIL(recipeId: recipeId)
}
guard let path = path else { return .REQUEST_DROPPED } This function attempts to create or update a recipe on the server using the provided `api`. If the server connection is successful, it uploads the provided `recipeDetail`. If the server connection fails, it returns `RequestAlert.REQUEST_DROPPED`.
let request = RequestWrapper.customRequest( - Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
method: createNew ? .POST : .PUT,
path: path,
headerFields: [
HeaderField.accept(value: .JSON),
HeaderField.ocsRequest(value: true),
HeaderField.contentType(value: .JSON)
],
body: JSONEncoder.safeEncode(recipeDetail)
)
if await sendRequest(request) { - Parameters:
return .REQUEST_SUCCESS - recipeDetail: The detailed information of the recipe to upload.
} else { - createNew: If true, creates a new recipe on the server; otherwise, updates an existing one.
requestQueue.append(request)
return .REQUEST_DELAYED Example usage:
} ```swift
}*/ let uploadResult = await mainViewModel.uploadRecipe(recipeDetail: myRecipeDetail, createNew: true)
*/
func uploadRecipe(recipeDetail: RecipeDetail, createNew: Bool) async -> RequestAlert { func uploadRecipe(recipeDetail: RecipeDetail, createNew: Bool) async -> RequestAlert {
let error = await api.createRecipe( var error: NetworkError? = nil
from: serverAdress, if createNew {
auth: authString, error = await api.createRecipe(
recipe: recipeDetail from: serverAdress,
) auth: authString,
recipe: recipeDetail
)
} else {
error = await api.updateRecipe(
from: serverAdress,
auth: authString,
recipe: recipeDetail
)
}
if let error = error { if let error = error {
return .REQUEST_DROPPED return .REQUEST_DROPPED
} }
return .REQUEST_SUCCESS return .REQUEST_SUCCESS
} }
/*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 }
do {
let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
if let recipeId = json as? Int {
return true
} else if let message = json as? [String : Any] {
print("Server message: ", message["msg"] ?? "-")
return false
}
// TODO: Better error handling (Show error to user!)
} catch {
print("Could not decode server response")
}
return false
}*/
} }
@@ -470,51 +449,7 @@ extension MainViewModel {
guard let data = JSONEncoder.safeEncode(object) else { return } guard let data = JSONEncoder.safeEncode(object) else { return }
await dataStore.save(data: data, toPath: path) await dataStore.save(data: data, toPath: path)
} }
/*private func loadObject<T: Codable>(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.")
return data
} else {
guard let apiController = apiController else { return nil }
let request = RequestWrapper.jsonGetRequest(path: networkPath)
let (data, error): (T?, Error?) = await apiController.sendDataRequest(request)
print(error as Any)
if let data = data {
await dataStore.save(data: data, toPath: localPath)
}
return data
}
} catch {
print("An unknown error occurred.")
}
return nil
}
private func imageToCache(image: UIImage, recipeId: Int, thumb: Bool) {
if imageCache[recipeId] == nil {
imageCache[recipeId] = RecipeImage(imageExists: true)
}
if thumb {
imageCache[recipeId]!.imageExists = true
imageCache[recipeId]!.thumb = image
} else {
imageCache[recipeId]!.imageExists = true
imageCache[recipeId]!.full = image
}
}
private func imageFromCache(recipeId: Int, thumb: Bool) -> UIImage? {
if imageCache[recipeId] != nil {
if thumb {
return imageCache[recipeId]!.thumb
} else {
return imageCache[recipeId]!.full
}
}
return nil
}
*/
private func imageFromStore(id: Int, size: RecipeImage.RecipeImageSize) async -> UIImage? { private func imageFromStore(id: Int, size: RecipeImage.RecipeImageSize) async -> UIImage? {
do { do {
let localPath = "image\(id)_\(size == .FULL ? "full" : "thumb")" let localPath = "image\(id)_\(size == .FULL ? "full" : "thumb")"

View File

@@ -111,8 +111,8 @@ import SwiftUI
func dismissEditView() { func dismissEditView() {
Task { Task {
await mainViewModel.loadCategories() //loadCategoryList(needsUpdate: true) await mainViewModel.getCategories()
await mainViewModel.getCategory(named: recipe.recipeCategory)//.loadRecipeList(categoryName: recipe.recipeCategory, needsUpdate: true) await mainViewModel.getCategory(named: recipe.recipeCategory, fetchMode: .preferServer)
} }
isPresented.wrappedValue = false isPresented.wrappedValue = false
} }

View File

@@ -61,10 +61,10 @@ struct CategoryDetailView: View {
} }
.searchable(text: $searchText, prompt: "Search recipes") .searchable(text: $searchText, prompt: "Search recipes")
.task { .task {
await viewModel.getCategory(named: categoryName)//.loadRecipeList(categoryName: categoryName) await viewModel.getCategory(named: categoryName, fetchMode: .preferLocal)
} }
.refreshable { .refreshable {
await viewModel.getCategory(named: categoryName)//.loadRecipeList(categoryName: categoryName, needsUpdate: true) await viewModel.getCategory(named: categoryName, fetchMode: .preferServer)
} }
} }
@@ -81,15 +81,15 @@ struct CategoryDetailView: View {
if let recipes = viewModel.recipes[categoryName] { if let recipes = viewModel.recipes[categoryName] {
Task { Task {
for recipe in recipes { for recipe in recipes {
let recipeDetail = await viewModel.getRecipe(id: recipe.recipe_id) let recipeDetail = await viewModel.getRecipe(id: recipe.recipe_id, fetchMode: .onlyServer)
await viewModel.saveLocal(recipeDetail, path: "recipe\(recipe.recipe_id).data") await viewModel.saveLocal(recipeDetail, path: "recipe\(recipe.recipe_id).data")
let thumbnail = await viewModel.getImage(id: recipe.recipe_id, size: .THUMB, needsUpdate: true) let thumbnail = await viewModel.getImage(id: recipe.recipe_id, size: .THUMB, fetchMode: .onlyServer)
guard let thumbnail = thumbnail else { continue } guard let thumbnail = thumbnail else { continue }
guard let thumbnailData = thumbnail.pngData() else { continue } guard let thumbnailData = thumbnail.pngData() else { continue }
await viewModel.saveLocal(thumbnailData.base64EncodedString(), path: "image\(recipe.recipe_id)_thumb") await viewModel.saveLocal(thumbnailData.base64EncodedString(), path: "image\(recipe.recipe_id)_thumb")
let image = await viewModel.getImage(id: recipe.recipe_id, size: .FULL, needsUpdate: true) let image = await viewModel.getImage(id: recipe.recipe_id, size: .FULL, fetchMode: .onlyServer)
guard let image = image else { continue } guard let image = image else { continue }
guard let imageData = image.pngData() else { continue } guard let imageData = image.pngData() else { continue }
await viewModel.saveLocal(imageData.base64EncodedString(), path: "image\(recipe.recipe_id)_full") await viewModel.saveLocal(imageData.base64EncodedString(), path: "image\(recipe.recipe_id)_full")

View File

@@ -90,7 +90,7 @@ struct MainView: View {
} }
.task { .task {
self.serverConnection = await viewModel.checkServerConnection() self.serverConnection = await viewModel.checkServerConnection()
await viewModel.loadCategories()//viewModel.loadCategoryList() await viewModel.getCategories()//viewModel.loadCategoryList()
// Open detail view for default category // Open detail view for default category
if userSettings.defaultCategory != "" { if userSettings.defaultCategory != "" {
if let cat = viewModel.categories.first(where: { c in if let cat = viewModel.categories.first(where: { c in
@@ -105,7 +105,7 @@ struct MainView: View {
} }
.refreshable { .refreshable {
self.serverConnection = await viewModel.checkServerConnection() self.serverConnection = await viewModel.checkServerConnection()
await viewModel.loadCategories()//loadCategoryList(needsUpdate: true) await viewModel.getCategories()//loadCategoryList(needsUpdate: true)
} }
} }

View File

@@ -51,11 +51,11 @@ struct RecipeCardView: View {
.clipShape(RoundedRectangle(cornerRadius: 17)) .clipShape(RoundedRectangle(cornerRadius: 17))
.padding(.horizontal) .padding(.horizontal)
.task { .task {
recipeThumb = await viewModel.getImage(id: recipe.recipe_id, size: .THUMB, needsUpdate: false)//loadImage(recipeId: recipe.recipe_id, thumb: true) recipeThumb = await viewModel.getImage(id: recipe.recipe_id, size: .THUMB, fetchMode: .preferLocal)
self.isDownloaded = viewModel.recipeDetailExists(recipeId: recipe.recipe_id) self.isDownloaded = viewModel.recipeDetailExists(recipeId: recipe.recipe_id)
} }
.refreshable { .refreshable {
recipeThumb = await viewModel.getImage(id: recipe.recipe_id, size: .THUMB, needsUpdate: true)//.loadImage(recipeId: recipe.recipe_id, thumb: true, needsUpdate: true) recipeThumb = await viewModel.getImage(id: recipe.recipe_id, size: .THUMB, fetchMode: .preferServer)
} }
} }
} }

View File

@@ -106,13 +106,13 @@ struct RecipeDetailView: View {
} }
} }
.task { .task {
recipeDetail = await viewModel.getRecipe(id: recipe.recipe_id)//loadRecipeDetail(recipeId: recipe.recipe_id) recipeDetail = await viewModel.getRecipe(id: recipe.recipe_id, fetchMode: .preferLocal)//loadRecipeDetail(recipeId: recipe.recipe_id)
recipeImage = await viewModel.getImage(id: recipe.recipe_id, size: .FULL, needsUpdate: false)//.loadImage(recipeId: recipe.recipe_id, thumb: false) recipeImage = await viewModel.getImage(id: recipe.recipe_id, size: .FULL, fetchMode: .preferLocal)//.loadImage(recipeId: recipe.recipe_id, thumb: false)
self.isDownloaded = viewModel.recipeDetailExists(recipeId: recipe.recipe_id) self.isDownloaded = viewModel.recipeDetailExists(recipeId: recipe.recipe_id)
} }
.refreshable { .refreshable {
recipeDetail = await viewModel.getRecipe(id: recipe.recipe_id)//.loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true) recipeDetail = await viewModel.getRecipe(id: recipe.recipe_id, fetchMode: .preferServer)
recipeImage = await viewModel.getImage(id: recipe.recipe_id, size: .FULL, needsUpdate: true)//.loadImage(recipeId: recipe.recipe_id, thumb: false, needsUpdate: true) recipeImage = await viewModel.getImage(id: recipe.recipe_id, size: .FULL, fetchMode: .preferServer)
} }
} }
} }

View File

@@ -143,7 +143,7 @@ struct RecipeEditView: View {
} }
} }
.task { .task {
viewModel.keywordSuggestions = await viewModel.mainViewModel.getKeywords() viewModel.keywordSuggestions = await viewModel.mainViewModel.getKeywords(fetchMode: .preferServer)
} }
.onAppear { .onAppear {
viewModel.prepareView() viewModel.prepareView()