Modernize networking layer and fix category navigation and recipe list bugs
Network layer: - Replace static CookbookApi protocol with instance-based CookbookApiProtocol using async/throws instead of tuple returns - Refactor ApiRequest to use URLComponents for proper URL encoding, replace print statements with OSLog, and return typed NetworkError cases - Add structured NetworkError variants (httpError, connectionError, etc.) - Remove global cookbookApi constant in favor of injected dependency on AppState - Delete unused RecipeEditViewModel, RecipeScraper, and Scraper playground Data & model fixes: - Add custom Decodable for RecipeDetail with safe fallbacks for malformed JSON - Make Category Hashable/Equatable use only `name` so NavigationSplitView selection survives category refreshes with updated recipe_count - Return server-assigned ID from uploadRecipe so new recipes get their ID before the post-upload refresh block executes View updates: - Refresh both old and new category recipe lists after upload when category changes, mapping empty recipeCategory to "*" for uncategorized recipes - Raise deployment target to iOS 18, adopt new SwiftUI API conventions - Clean up alerts, onboarding views, and settings Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
@@ -19,97 +20,75 @@ import UIKit
|
||||
var imagesNeedUpdate: [Int: [String: Bool]] = [:]
|
||||
var lastUpdates: [String: Date] = [:]
|
||||
var allKeywords: [RecipeKeyword] = []
|
||||
|
||||
|
||||
private let dataStore: DataStore
|
||||
|
||||
init() {
|
||||
print("Created MainViewModel")
|
||||
private let api: CookbookApiProtocol
|
||||
|
||||
init(api: CookbookApiProtocol? = nil) {
|
||||
Logger.network.debug("Created AppState")
|
||||
self.dataStore = DataStore()
|
||||
|
||||
self.api = api ?? CookbookApiFactory.makeClient()
|
||||
|
||||
if UserSettings.shared.authString == "" {
|
||||
let loginString = "\(UserSettings.shared.username):\(UserSettings.shared.token)"
|
||||
let loginData = loginString.data(using: String.Encoding.utf8)!
|
||||
UserSettings.shared.authString = loginData.base64EncodedString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum FetchMode {
|
||||
case preferLocal, preferServer, onlyLocal, onlyServer
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
// MARK: - Categories
|
||||
|
||||
func getCategories() async {
|
||||
let (categories, _) = await cookbookApi.getCategories(
|
||||
auth: UserSettings.shared.authString
|
||||
)
|
||||
if let categories = categories {
|
||||
print("Successfully loaded categories")
|
||||
do {
|
||||
let categories = try await api.getCategories()
|
||||
Logger.data.debug("Successfully loaded categories")
|
||||
self.categories = categories
|
||||
await saveLocal(self.categories, path: "categories.data")
|
||||
} else {
|
||||
// If there's no server connection, try loading categories from local storage
|
||||
print("Loading categories from store ...")
|
||||
} catch {
|
||||
Logger.data.debug("Loading categories from store ...")
|
||||
if let categories: [Category] = await loadLocal(path: "categories.data") {
|
||||
self.categories = categories
|
||||
print("Success!")
|
||||
Logger.data.debug("Loaded categories from local store")
|
||||
} else {
|
||||
print("Failure!")
|
||||
Logger.data.error("Failed to load categories from local store")
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the lastUpdates with distantPast dates, so that each recipeDetail is updated on launch for all categories
|
||||
|
||||
for category in self.categories {
|
||||
lastUpdates[category.name] = Date.distantPast
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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 {
|
||||
print("getCategory(\(name), fetchMode: \(fetchMode))")
|
||||
Logger.data.debug("getCategory(\(name), fetchMode: \(String(describing: fetchMode)))")
|
||||
func getLocal() async -> Bool {
|
||||
let categoryString = name == "*" ? "_" : name
|
||||
if let recipes: [Recipe] = await loadLocal(path: "category_\(categoryString).data") {
|
||||
self.recipes[name] = recipes
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func getServer(store: Bool = false) async -> Bool {
|
||||
let (recipes, _) = await cookbookApi.getCategory(
|
||||
auth: UserSettings.shared.authString,
|
||||
named: categoryString
|
||||
)
|
||||
if let recipes = recipes {
|
||||
let categoryString = name == "*" ? "_" : name
|
||||
do {
|
||||
let recipes = try await api.getCategory(named: categoryString)
|
||||
self.recipes[name] = recipes
|
||||
if store {
|
||||
await saveLocal(recipes, path: "category_\(categoryString).data")
|
||||
}
|
||||
//userSettings.lastUpdate = Date()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
let categoryString = name == "*" ? "_" : name
|
||||
|
||||
switch fetchMode {
|
||||
case .preferLocal:
|
||||
if await getLocal() { return }
|
||||
@@ -123,50 +102,38 @@ import UIKit
|
||||
if await getServer() { return }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Recipe details
|
||||
|
||||
func updateAllRecipeDetails() async {
|
||||
for category in self.categories {
|
||||
await updateRecipeDetails(in: category.name)
|
||||
}
|
||||
UserSettings.shared.lastUpdate = Date()
|
||||
}
|
||||
|
||||
|
||||
func updateRecipeDetails(in category: String) async {
|
||||
guard UserSettings.shared.storeRecipes else { return }
|
||||
guard let recipes = self.recipes[category] else { return }
|
||||
for recipe in recipes {
|
||||
if let dateModified = recipe.dateModified {
|
||||
if needsUpdate(category: category, lastModified: dateModified) {
|
||||
print("\(recipe.name) needs an update. (last modified: \(recipe.dateModified ?? "unknown")")
|
||||
Logger.data.debug("\(recipe.name) needs an update. (last modified: \(recipe.dateModified ?? "unknown"))")
|
||||
await updateRecipeDetail(id: recipe.recipe_id, withThumb: UserSettings.shared.storeThumb, withImage: UserSettings.shared.storeImages)
|
||||
} else {
|
||||
print("\(recipe.name) is up to date.")
|
||||
Logger.data.debug("\(recipe.name) is up to date.")
|
||||
}
|
||||
} else {
|
||||
await updateRecipeDetail(id: recipe.recipe_id, withThumb: UserSettings.shared.storeThumb, withImage: UserSettings.shared.storeImages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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 cookbookApi.getRecipes(
|
||||
auth: UserSettings.shared.authString
|
||||
)
|
||||
if let recipes = recipes {
|
||||
return recipes
|
||||
} else if let error = error {
|
||||
print(error)
|
||||
do {
|
||||
return try await api.getRecipes()
|
||||
} catch {
|
||||
Logger.network.error("Failed to fetch recipes: \(error.localizedDescription)")
|
||||
}
|
||||
var allRecipes: [Recipe] = []
|
||||
for category in categories {
|
||||
@@ -174,48 +141,29 @@ import UIKit
|
||||
allRecipes.append(contentsOf: recipeArray)
|
||||
}
|
||||
}
|
||||
return allRecipes.sorted(by: {
|
||||
$0.name < $1.name
|
||||
})
|
||||
return allRecipes.sorted(by: { $0.name < $1.name })
|
||||
}
|
||||
|
||||
/**
|
||||
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, save: Bool = false) async -> RecipeDetail? {
|
||||
func getLocal() async -> RecipeDetail? {
|
||||
if let recipe: RecipeDetail = await loadLocal(path: "recipe\(id).data") { return recipe }
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func getServer() async -> RecipeDetail? {
|
||||
let (recipe, error) = await cookbookApi.getRecipe(
|
||||
auth: UserSettings.shared.authString,
|
||||
id: id
|
||||
)
|
||||
if let recipe = recipe {
|
||||
do {
|
||||
let recipe = try await api.getRecipe(id: id)
|
||||
if save {
|
||||
self.recipeDetails[id] = recipe
|
||||
await self.saveLocal(recipe, path: "recipe\(id).data")
|
||||
}
|
||||
return recipe
|
||||
} else if let error = error {
|
||||
print(error)
|
||||
} catch {
|
||||
Logger.network.error("Failed to fetch recipe \(id): \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
switch fetchMode {
|
||||
case .preferLocal:
|
||||
if let recipe = await getLocal() { return recipe }
|
||||
@@ -230,31 +178,19 @@ import UIKit
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
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 updateRecipeDetail(id: Int, withThumb: Bool, withImage: Bool) async {
|
||||
if let recipeDetail = await getRecipe(id: id, fetchMode: .onlyServer) {
|
||||
await saveLocal(recipeDetail, path: "recipe\(id).data")
|
||||
}
|
||||
|
||||
|
||||
if withThumb {
|
||||
let thumbnail = await getImage(id: id, size: .THUMB, fetchMode: .onlyServer)
|
||||
guard let thumbnail = thumbnail else { return }
|
||||
guard let thumbnailData = thumbnail.pngData() else { return }
|
||||
await saveLocal(thumbnailData.base64EncodedString(), path: "image\(id)_thumb")
|
||||
}
|
||||
|
||||
|
||||
if withImage {
|
||||
let image = await getImage(id: id, size: .FULL, fetchMode: .onlyServer)
|
||||
guard let image = image else { return }
|
||||
@@ -262,50 +198,26 @@ import UIKit
|
||||
await saveLocal(imageData.base64EncodedString(), path: "image\(id)_full")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Check if recipeDetail is stored locally, either in cache or on disk
|
||||
/// - Parameters
|
||||
/// - recipeId: The id of a recipe.
|
||||
/// - Returns: True if the recipeDetail is stored, otherwise false
|
||||
|
||||
func recipeDetailExists(recipeId: Int) -> Bool {
|
||||
if (dataStore.recipeDetailExists(recipeId: recipeId)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return dataStore.recipeDetailExists(recipeId: recipeId)
|
||||
}
|
||||
|
||||
/**
|
||||
Asynchronously retrieves and returns an image for a recipe with the specified ID and size.
|
||||
|
||||
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.
|
||||
// MARK: - Images
|
||||
|
||||
- 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)
|
||||
}
|
||||
|
||||
|
||||
func getServer() async -> UIImage? {
|
||||
let (image, _) = await cookbookApi.getImage(
|
||||
auth: UserSettings.shared.authString,
|
||||
id: id,
|
||||
size: size
|
||||
)
|
||||
if let image = image { return image }
|
||||
return nil
|
||||
do {
|
||||
return try await api.getImage(id: id, size: size)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
switch fetchMode {
|
||||
case .preferLocal:
|
||||
if let image = imageFromCache(id: id, size: size) {
|
||||
@@ -355,28 +267,20 @@ import UIKit
|
||||
imagesNeedUpdate[id] = [size.rawValue: false]
|
||||
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.
|
||||
// MARK: - Keywords
|
||||
|
||||
- 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 -> [RecipeKeyword] {
|
||||
func getLocal() async -> [RecipeKeyword]? {
|
||||
return await loadLocal(path: "keywords.data")
|
||||
}
|
||||
|
||||
|
||||
func getServer() async -> [RecipeKeyword]? {
|
||||
let (tags, _) = await cookbookApi.getTags(
|
||||
auth: UserSettings.shared.authString
|
||||
)
|
||||
return tags
|
||||
do {
|
||||
return try await api.getTags()
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch fetchMode {
|
||||
@@ -399,7 +303,9 @@ import UIKit
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Data management
|
||||
|
||||
func deleteAllData() {
|
||||
if dataStore.clearAll() {
|
||||
self.categories = []
|
||||
@@ -409,31 +315,14 @@ import UIKit
|
||||
self.imagesNeedUpdate = [:]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Asynchronously deletes a recipe with the specified ID from the server and local storage.
|
||||
|
||||
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 cookbookApi.deleteRecipe(
|
||||
auth: UserSettings.shared.authString,
|
||||
id: id
|
||||
)
|
||||
|
||||
if let error = error {
|
||||
do {
|
||||
try await api.deleteRecipe(id: id)
|
||||
} catch {
|
||||
return .REQUEST_DROPPED
|
||||
}
|
||||
|
||||
let path = "recipe\(id).data"
|
||||
dataStore.delete(path: path)
|
||||
if recipes[categoryName] != nil {
|
||||
@@ -444,95 +333,59 @@ import UIKit
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
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 cookbookApi.getCategories(
|
||||
auth: UserSettings.shared.authString
|
||||
)
|
||||
if let categories = categories {
|
||||
do {
|
||||
let categories = try await api.getCategories()
|
||||
self.categories = categories
|
||||
await saveLocal(categories, path: "categories.data")
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
Asynchronously uploads a recipe to the server.
|
||||
|
||||
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`.
|
||||
|
||||
- Important: This function assumes that the server address, authentication string, and API have been properly configured in the `MainViewModel` instance.
|
||||
|
||||
- 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? {
|
||||
var error: NetworkError? = nil
|
||||
if createNew {
|
||||
error = await cookbookApi.createRecipe(
|
||||
auth: UserSettings.shared.authString,
|
||||
recipe: recipeDetail
|
||||
)
|
||||
} else {
|
||||
error = await cookbookApi.updateRecipe(
|
||||
auth: UserSettings.shared.authString,
|
||||
recipe: recipeDetail
|
||||
)
|
||||
}
|
||||
if error != nil {
|
||||
return .REQUEST_DROPPED
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func importRecipe(url: String) async -> (RecipeDetail?, RequestAlert?) {
|
||||
guard let data = JSONEncoder.safeEncode(RecipeImportRequest(url: url)) else { return (nil, .REQUEST_DROPPED) }
|
||||
let (recipeDetail, error) = await cookbookApi.importRecipe(
|
||||
auth: UserSettings.shared.authString,
|
||||
data: data
|
||||
)
|
||||
if error != nil {
|
||||
func uploadRecipe(recipeDetail: RecipeDetail, createNew: Bool) async -> (Int?, RequestAlert?) {
|
||||
do {
|
||||
if createNew {
|
||||
let id = try await api.createRecipe(recipeDetail)
|
||||
return (id, nil)
|
||||
} else {
|
||||
let id = try await api.updateRecipe(recipeDetail)
|
||||
return (id, nil)
|
||||
}
|
||||
} catch {
|
||||
return (nil, .REQUEST_DROPPED)
|
||||
}
|
||||
}
|
||||
|
||||
func importRecipe(url: String) async -> (RecipeDetail?, RequestAlert?) {
|
||||
do {
|
||||
let recipeDetail = try await api.importRecipe(url: url)
|
||||
return (recipeDetail, nil)
|
||||
} catch {
|
||||
return (nil, .REQUEST_DROPPED)
|
||||
}
|
||||
return (recipeDetail, nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Local storage helpers
|
||||
|
||||
extension AppState {
|
||||
func loadLocal<T: Codable>(path: String) async -> T? {
|
||||
do {
|
||||
return try await dataStore.load(fromPath: path)
|
||||
} catch (let error) {
|
||||
print(error)
|
||||
} catch {
|
||||
Logger.data.debug("Failed to load local data: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func saveLocal<T: Codable>(_ object: T, path: String) async {
|
||||
await dataStore.save(data: object, toPath: path)
|
||||
}
|
||||
|
||||
|
||||
private func imageFromStore(id: Int, size: RecipeImage.RecipeImageSize) async -> UIImage? {
|
||||
do {
|
||||
let localPath = "image\(id)_\(size == .FULL ? "full" : "thumb")"
|
||||
@@ -542,18 +395,18 @@ extension AppState {
|
||||
return image
|
||||
}
|
||||
} catch {
|
||||
print("Could not find image in local storage.")
|
||||
Logger.data.debug("Could not find image in local storage.")
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
private func imageToStore(id: Int, size: RecipeImage.RecipeImageSize, image: UIImage) async {
|
||||
if let data = image.pngData() {
|
||||
await saveLocal(data.base64EncodedString(), path: "image\(id)_\(size.rawValue)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func imageToCache(id: Int, size: RecipeImage.RecipeImageSize, image: UIImage) {
|
||||
if recipeImages[id] != nil {
|
||||
recipeImages[id]![size.rawValue] = image
|
||||
@@ -566,14 +419,14 @@ extension AppState {
|
||||
imagesNeedUpdate[id] = [size.rawValue: false]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func imageFromCache(id: Int, size: RecipeImage.RecipeImageSize) -> UIImage? {
|
||||
if recipeImages[id] != nil {
|
||||
return recipeImages[id]![size.rawValue]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
private func imageUpdateNeeded(id: Int, size: RecipeImage.RecipeImageSize) -> Bool {
|
||||
if imagesNeedUpdate[id] != nil {
|
||||
if imagesNeedUpdate[id]![size.rawValue] != nil {
|
||||
@@ -582,26 +435,22 @@ extension AppState {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
private func needsUpdate(category: String, lastModified: String) -> Bool {
|
||||
print("=======================")
|
||||
print("original date string: \(lastModified)")
|
||||
// Create a DateFormatter
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
|
||||
// Convert the string to a Date object
|
||||
|
||||
if let date = dateFormatter.date(from: lastModified), let lastUpdate = lastUpdates[category] {
|
||||
if date < lastUpdate {
|
||||
print("No update needed. (recipe: \(dateFormatter.string(from: date)), last: \(dateFormatter.string(from: lastUpdate))")
|
||||
Logger.data.debug("No update needed for \(category)")
|
||||
return false
|
||||
} else {
|
||||
print("Update needed. (recipe: \(dateFormatter.string(from: date)), last: \(dateFormatter.string(from: lastUpdate))")
|
||||
Logger.data.debug("Update needed for \(category)")
|
||||
return true
|
||||
}
|
||||
}
|
||||
print("String is not a date. Update needed.")
|
||||
Logger.data.debug("Date parse failed, update needed for \(category)")
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -624,11 +473,11 @@ extension AppState {
|
||||
timers[recipeId] = timer
|
||||
return timer
|
||||
}
|
||||
|
||||
|
||||
func getTimer(forRecipe recipeId: String, duration: DurationComponents) -> RecipeTimer {
|
||||
return timers[recipeId] ?? createTimer(forRecipe: recipeId, duration: duration)
|
||||
}
|
||||
|
||||
|
||||
func deleteTimer(forRecipe recipeId: String) {
|
||||
timers.removeValue(forKey: recipeId)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user