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.
static func updateRecipe(
from serverAdress: String,
auth: String, id: Int
auth: String,
recipe: RecipeDetail
) async -> (NetworkError?)
/// Delete the recipe with the specified id.

View File

@@ -86,13 +86,17 @@ class CookbookApiV1: CookbookApi {
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(
serverAdress: serverAdress,
path: "/api/v1/recipes/\(id)",
path: "/api/v1/recipes/\(recipe.id)",
method: .PUT,
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()

View File

@@ -13,17 +13,15 @@ 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]] = [:]
@Published var recipeDetails: [Int: RecipeDetail] = [:]
private var imageCache: [Int: RecipeImage] = [:]
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) {
print("Created MainViewModel")
@@ -31,21 +29,19 @@ import UIKit
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 {
if let categoryList: [Category] = await loadObject(
localPath: "categories.data",
networkPath: .CATEGORIES,
needsUpdate: needsUpdate
) {
self.categories = categoryList
enum FetchMode {
case preferLocal, preferServer, onlyLocal, onlyServer
}
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(
from: serverAdress,
auth: authString
@@ -53,34 +49,38 @@ import UIKit
if let categories = categories {
self.categories = categories
await saveLocal(categories, path: "categories.data")
serverConnection = true
} else {
// If there's no server connection, try loading categories from local storage
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.
/// - 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)
/**
Fetches recipes for a specified category from either the server or local storage.
- Parameters:
- name: The name of the category. Use "*" to fetch recipes without assigned categories.
- needsUpdate: If true, recipes will be loaded from the server directly; otherwise, they will be loaded from local storage first.
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.
- Note: The category name "*" is used for all uncategorized recipes.
- 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") {
self.recipes[name] = recipes
return true
}
return false
}
}*/
func getCategory(named name: String) async {
let categoryString = name == "*" ? "_" : name
func getServer() async -> Bool {
let (recipes, _) = await api.getCategory(
from: serverAdress,
auth: authString,
@@ -88,25 +88,37 @@ import UIKit
)
if let recipes = recipes {
self.recipes[name] = recipes
} else {
if let recipes: [Recipe] = await loadLocal(path: "category_\(categoryString).data") {
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] = []
for category in categories {
await loadRecipeList(categoryName: category.name)
if let recipeArray = recipes[category.name] {
allRecipes.append(contentsOf: recipeArray)
}
}
return allRecipes.sorted(by: {
$0.name < $1.name
})
}*/
/**
Asynchronously retrieves all recipes either from the server or the locally cached data.
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.
- 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:
```swift
let recipes = await mainViewModel.getRecipes()
*/
func getRecipes() async -> [Recipe] {
let (recipes, error) = await api.getRecipes(
from: serverAdress,
@@ -128,28 +140,29 @@ import UIKit
})
}
/// Try to load the recipe details from cache. If not found, try to load from store or the server.
/// - Parameters
/// - 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 {
if !needsUpdate {
if let recipeDetail = recipeDetails[recipeId] {
return recipeDetail
/**
Asynchronously retrieves a recipe detail either from the server or locally cached data.
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.
- Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
- Parameters:
- 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",
networkPath: .RECIPE_DETAIL(recipeId: recipeId),
needsUpdate: needsUpdate
) {
recipeDetails[recipeId] = recipeDetail
return recipeDetail
}
return RecipeDetail.error
}*/
func getRecipe(id: Int) async -> RecipeDetail {
func getServer() async -> RecipeDetail? {
let (recipe, error) = await api.getRecipe(
from: serverAdress,
auth: authString,
@@ -160,26 +173,52 @@ import UIKit
} else if let error = error {
print(error)
}
guard let recipe: RecipeDetail = await loadLocal(path: "recipe\(id).data") else {
return RecipeDetail.error
}
return recipe
return nil
}
switch fetchMode {
case .preferLocal:
if let recipe = await getLocal() { return recipe }
if let recipe = await getServer() { return recipe }
case .preferServer:
if let recipe = await getServer() { return recipe }
if let recipe = await getLocal() { return recipe }
case .onlyLocal:
if let recipe = await getLocal() { return recipe }
case .onlyServer:
if let recipe = await getServer() { return recipe }
}
return .error
}
/**
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 {
for category in categories {
await getCategory(named: category.name)
await getCategory(named: category.name, fetchMode: .onlyServer)
guard let recipeList = recipes[category.name] else { continue }
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")
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 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)
let image = await getImage(id: recipe.recipe_id, size: .FULL, fetchMode: .onlyServer)
guard let image = image else { continue }
guard let imageData = image.pngData() else { continue }
await saveLocal(imageData.base64EncodedString(), path: "image\(recipe.recipe_id)_full")
@@ -201,77 +240,28 @@ import UIKit
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.
/// - Parameters
/// - recipeId: The id of a recipe.
/// - 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? {
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 {
guard let apiController = apiController else { return nil }
if let data = await apiController.imageDataFromServer(recipeId: recipeId, thumb: thumb) {
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
} else {
imageCache[recipeId] = RecipeImage(imageExists: false)
return nil
}
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.
- Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
- Parameters:
- id: The identifier of the recipe associated with the image.
- size: The size of the desired image (thumbnail or full).
- needsUpdate: If true, the image will be loaded from the server directly; otherwise, it will be loaded from local storage.
Example usage:
```swift
let thumbnail = await mainViewModel.getImage(id: 123, size: .THUMB, needsUpdate: true)
*/
func getImage(id: Int, size: RecipeImage.RecipeImageSize, fetchMode: FetchMode) async -> UIImage? {
func getLocal() async -> UIImage? {
return await imageFromStore(id: id, size: size)
}
// 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 {
guard imageCache[recipeId]!.imageExists else { return nil }
}
// Try to load image from cache
print("Attempting to load image from cache ...")
if let image = imageFromCache(recipeId: recipeId, thumb: thumb) {
print("Image found in cache.")
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
}*/
func getImage(id: Int, size: RecipeImage.RecipeImageSize, needsUpdate: Bool) async -> UIImage? {
if !needsUpdate, let image = await imageFromStore(id: id, size: size) {
return image
}
func getServer() async -> UIImage? {
let (image, _) = await api.getImage(
from: serverAdress,
auth: authString,
@@ -279,31 +269,59 @@ import UIKit
size: size
)
if let image = image { return image }
return await imageFromStore(id: id, size: size)
return nil
}
/*func getKeywords() async -> [String] {
if let keywords: [RecipeKeyword] = await self.loadObject(
localPath: "keywords.data",
networkPath: .KEYWORDS,
needsUpdate: true
) {
return keywords.map { $0.name }
switch fetchMode {
case .preferLocal:
if let image = await getLocal() { return image }
if let image = await getServer() { 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 }
}
return []
}*/
func getKeywords() async -> [String] {
let (tags, error) = await api.getTags(
return nil
}
/**
Asynchronously retrieves and returns a list of keywords (tags).
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.
- Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
Example usage:
```swift
let keywords = await mainViewModel.getKeywords()
*/
func getKeywords(fetchMode: FetchMode) async -> [String] {
func getLocal() async -> [String]? {
return await loadLocal(path: "keywords.data")
}
func getServer() async -> [String]? {
let (tags, _) = 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
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 []
}
@@ -312,37 +330,26 @@ import UIKit
if dataStore.clearAll() {
self.categories = []
self.recipes = [:]
self.imageCache = [:]
self.recipeDetails = [:]
self.requestQueue = []
}
}
/*func deleteRecipe(withId id: Int, categoryName: String) async -> RequestAlert {
let request = RequestWrapper.customRequest(
method: .DELETE,
path: .RECIPE_DETAIL(recipeId: id),
headerFields: [
HeaderField.accept(value: .JSON),
HeaderField.ocsRequest(value: true)
]
)
/**
Asynchronously deletes a recipe with the specified ID from the server and local storage.
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)
}
if await sendRequest(request) {
return .REQUEST_SUCCESS
} else {
requestQueue.append(request)
return .REQUEST_DELAYED
}
}*/
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`.
- Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
- Parameters:
- id: The identifier of the recipe to delete.
- categoryName: The name of the category to which the recipe belongs.
Example usage:
```swift
let requestResult = await mainViewModel.deleteRecipe(withId: 123, categoryName: "Desserts")
*/
func deleteRecipe(withId id: Int, categoryName: String) async -> RequestAlert {
let (error) = await api.deleteRecipe(
from: serverAdress,
@@ -364,21 +371,17 @@ import UIKit
return .REQUEST_SUCCESS
}
/*func checkServerConnection() async -> Bool {
guard let apiController = apiController else { return false }
let req = RequestWrapper.customRequest(
method: .GET,
path: .CONFIG,
headerFields: [
.ocsRequest(value: true),
.accept(value: .JSON)
]
)
if let error = await apiController.sendRequest(req) {
return false
}
return true
}*/
/**
Asynchronously checks the server connection by attempting to fetch categories.
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`.
- Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
Example usage:
```swift
let isConnected = await mainViewModel.checkServerConnection()
*/
func checkServerConnection() async -> Bool {
let (categories, _) = await api.getCategories(
from: serverAdress,
@@ -392,65 +395,41 @@ import UIKit
return false
}
/*func uploadRecipe(recipeDetail: RecipeDetail, createNew: Bool) async -> RequestAlert {
var path: RequestPath? = nil
if createNew {
path = .NEW_RECIPE
} else if let recipeId = Int(recipeDetail.id) {
path = .RECIPE_DETAIL(recipeId: recipeId)
}
/**
Asynchronously uploads a recipe to the server.
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(
method: createNew ? .POST : .PUT,
path: path,
headerFields: [
HeaderField.accept(value: .JSON),
HeaderField.ocsRequest(value: true),
HeaderField.contentType(value: .JSON)
],
body: JSONEncoder.safeEncode(recipeDetail)
)
- Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
if await sendRequest(request) {
return .REQUEST_SUCCESS
} else {
requestQueue.append(request)
return .REQUEST_DELAYED
}
}*/
- Parameters:
- recipeDetail: The detailed information of the recipe to upload.
- createNew: If true, creates a new recipe on the server; otherwise, updates an existing one.
Example usage:
```swift
let uploadResult = await mainViewModel.uploadRecipe(recipeDetail: myRecipeDetail, createNew: true)
*/
func uploadRecipe(recipeDetail: RecipeDetail, createNew: Bool) async -> RequestAlert {
let error = await api.createRecipe(
var error: NetworkError? = nil
if createNew {
error = await api.createRecipe(
from: serverAdress,
auth: authString,
recipe: recipeDetail
)
} else {
error = await api.updateRecipe(
from: serverAdress,
auth: authString,
recipe: recipeDetail
)
}
if let error = error {
return .REQUEST_DROPPED
}
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 }
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? {
do {
let localPath = "image\(id)_\(size == .FULL ? "full" : "thumb")"

View File

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

View File

@@ -61,10 +61,10 @@ struct CategoryDetailView: View {
}
.searchable(text: $searchText, prompt: "Search recipes")
.task {
await viewModel.getCategory(named: categoryName)//.loadRecipeList(categoryName: categoryName)
await viewModel.getCategory(named: categoryName, fetchMode: .preferLocal)
}
.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] {
Task {
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")
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 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)
let image = await viewModel.getImage(id: recipe.recipe_id, size: .FULL, fetchMode: .onlyServer)
guard let image = image else { continue }
guard let imageData = image.pngData() else { continue }
await viewModel.saveLocal(imageData.base64EncodedString(), path: "image\(recipe.recipe_id)_full")

View File

@@ -90,7 +90,7 @@ struct MainView: View {
}
.task {
self.serverConnection = await viewModel.checkServerConnection()
await viewModel.loadCategories()//viewModel.loadCategoryList()
await viewModel.getCategories()//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.loadCategories()//loadCategoryList(needsUpdate: true)
await viewModel.getCategories()//loadCategoryList(needsUpdate: true)
}
}

View File

@@ -51,11 +51,11 @@ struct RecipeCardView: View {
.clipShape(RoundedRectangle(cornerRadius: 17))
.padding(.horizontal)
.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)
}
.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 {
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)
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, fetchMode: .preferLocal)//.loadImage(recipeId: recipe.recipe_id, thumb: false)
self.isDownloaded = viewModel.recipeDetailExists(recipeId: recipe.recipe_id)
}
.refreshable {
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)
recipeDetail = await viewModel.getRecipe(id: recipe.recipe_id, fetchMode: .preferServer)
recipeImage = await viewModel.getImage(id: recipe.recipe_id, size: .FULL, fetchMode: .preferServer)
}
}
}

View File

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