diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj index d7948a5..f58ea5e 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj @@ -64,6 +64,7 @@ A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */ = {isa = PBXBuildFile; productRef = A9CA6CF52B4C63F200F78AB5 /* TPPDF */; }; A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D89AAF2B4FE97800F49D92 /* TimerView.swift */; }; A9D8F9052B99F3E5009BACAE /* RecipeImportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */; }; + A9E78A2B2BE7799F00206866 /* JsonAny.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E78A2A2BE7799F00206866 /* JsonAny.swift */; }; A9FA2AB62B5079B200A43702 /* alarm_sound_0.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */; }; /* End PBXBuildFile section */ @@ -145,6 +146,7 @@ A9D89AAF2B4FE97800F49D92 /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.swift; sourceTree = ""; }; A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeImportSection.swift; sourceTree = ""; }; A9DA25D42B82096B0061FC2B /* Nextcloud-Cookbook-iOS-Client-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Nextcloud-Cookbook-iOS-Client-Info.plist"; sourceTree = SOURCE_ROOT; }; + A9E78A2A2BE7799F00206866 /* JsonAny.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonAny.swift; sourceTree = ""; }; A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = alarm_sound_0.mp3; sourceTree = ""; }; /* End PBXFileReference section */ @@ -375,6 +377,7 @@ A9805BEC2BAAC70E003B7231 /* NumberFormatter.swift */, A79AA8DF2AFF80E3007D25F2 /* DurationComponents.swift */, A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */, + A9E78A2A2BE7799F00206866 /* JsonAny.swift */, ); path = Util; sourceTree = ""; @@ -583,6 +586,7 @@ A787B0782B2B1E6400C2DF1B /* DateExtension.swift in Sources */, A79AA8E92B062DD1007D25F2 /* CookbookApiV1.swift in Sources */, A7CD3FD22B2C546A00D764AD /* CollapsibleView.swift in Sources */, + A9E78A2B2BE7799F00206866 /* JsonAny.swift in Sources */, A9BBB3902B91BE31002DA7FF /* ObservableRecipeDetail.swift in Sources */, A97506212B92104700E86029 /* RecipeMetadataSection.swift in Sources */, A70171B42AB2122900064C43 /* NetworkUtils.swift in Sources */, @@ -793,7 +797,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.10; + MARKETING_VERSION = 1.10.1; PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -837,7 +841,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.10; + MARKETING_VERSION = 1.10.1; PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate index 21581c2..9b812bc 100644 Binary files a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate and b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/xcuserdata/vincie.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Nextcloud Cookbook iOS Client.xcodeproj/xcuserdata/vincie.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..cb206b4 --- /dev/null +++ b/Nextcloud Cookbook iOS Client.xcodeproj/xcuserdata/vincie.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/Nextcloud Cookbook iOS Client/AppState.swift b/Nextcloud Cookbook iOS Client/AppState.swift index 8cbb8e8..11da792 100644 --- a/Nextcloud Cookbook iOS Client/AppState.swift +++ b/Nextcloud Cookbook iOS Client/AppState.swift @@ -135,11 +135,15 @@ import UIKit guard UserSettings.shared.storeRecipes else { return } guard let recipes = self.recipes[category] else { return } for recipe in recipes { - if needsUpdate(category: category, lastModified: recipe.dateModified) { - print("\(recipe.name) needs an update. (last modified: \(recipe.dateModified)") - await updateRecipeDetail(id: recipe.recipe_id, withThumb: UserSettings.shared.storeThumb, withImage: UserSettings.shared.storeImages) + if let dateModified = recipe.dateModified { + if needsUpdate(category: category, lastModified: dateModified) { + print("\(recipe.name) needs an update. (last modified: \(recipe.dateModified)") + await updateRecipeDetail(id: recipe.recipe_id, withThumb: UserSettings.shared.storeThumb, withImage: UserSettings.shared.storeImages) + } else { + print("\(recipe.name) is up to date.") + } } else { - print("\(recipe.name) is up to date.") + await updateRecipeDetail(id: recipe.recipe_id, withThumb: UserSettings.shared.storeThumb, withImage: UserSettings.shared.storeImages) } } } diff --git a/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift b/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift index bf3ce30..7149abb 100644 --- a/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift +++ b/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift @@ -55,12 +55,12 @@ class ObservableRecipeDetail: ObservableObject { id = recipeDetail.id name = recipeDetail.name keywords = recipeDetail.keywords.isEmpty ? [] : recipeDetail.keywords.components(separatedBy: ",") - imageUrl = recipeDetail.imageUrl + imageUrl = recipeDetail.imageUrl ?? "" prepTime = DurationComponents.fromPTString(recipeDetail.prepTime ?? "") cookTime = DurationComponents.fromPTString(recipeDetail.cookTime ?? "") totalTime = DurationComponents.fromPTString(recipeDetail.totalTime ?? "") description = recipeDetail.description - url = recipeDetail.url + url = recipeDetail.url ?? "" recipeYield = recipeDetail.recipeYield == 0 ? 1 : recipeDetail.recipeYield // Recipe yield should not be zero recipeCategory = recipeDetail.recipeCategory tool = recipeDetail.tool diff --git a/Nextcloud Cookbook iOS Client/Data/RecipeModels.swift b/Nextcloud Cookbook iOS Client/Data/RecipeModels.swift index de51043..f04478c 100644 --- a/Nextcloud Cookbook iOS Client/Data/RecipeModels.swift +++ b/Nextcloud Cookbook iOS Client/Data/RecipeModels.swift @@ -12,10 +12,10 @@ import SwiftUI struct Recipe: Codable { let name: String let keywords: String? - let dateCreated: String - let dateModified: String - let imageUrl: String - let imagePlaceholderUrl: String + let dateCreated: String? + let dateModified: String? + let imageUrl: String? + let imagePlaceholderUrl: String? let recipe_id: Int // Properties excluded from Codable @@ -35,15 +35,15 @@ extension Recipe: Identifiable, Hashable { struct RecipeDetail: Codable { var name: String var keywords: String - var dateCreated: String - var dateModified: String - var imageUrl: String + var dateCreated: String? + var dateModified: String? + var imageUrl: String? var id: String var prepTime: String? var cookTime: String? var totalTime: String? var description: String - var url: String + var url: String? var recipeYield: Int var recipeCategory: String var tool: [String] @@ -90,6 +90,33 @@ struct RecipeDetail: Codable { recipeInstructions = [] nutrition = [:] } + + // Custom decoder to handle value type ambiguity + private enum CodingKeys: String, CodingKey { + case name, keywords, dateCreated, dateModified, imageUrl, id, prepTime, cookTime, totalTime, description, url, recipeYield, recipeCategory, tool, recipeIngredient, recipeInstructions, nutrition + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + keywords = try container.decode(String.self, forKey: .keywords) + dateCreated = try container.decodeIfPresent(String.self, forKey: .dateCreated) + dateModified = try container.decodeIfPresent(String.self, forKey: .dateModified) + imageUrl = try container.decodeIfPresent(String.self, forKey: .imageUrl) + id = try container.decode(String.self, forKey: .id) + prepTime = try container.decodeIfPresent(String.self, forKey: .prepTime) + cookTime = try container.decodeIfPresent(String.self, forKey: .cookTime) + totalTime = try container.decodeIfPresent(String.self, forKey: .totalTime) + description = try container.decode(String.self, forKey: .description) + url = try container.decode(String.self, forKey: .url) + recipeYield = try container.decode(Int.self, forKey: .recipeYield) + recipeCategory = try container.decode(String.self, forKey: .recipeCategory) + tool = try container.decode([String].self, forKey: .tool) + recipeIngredient = try container.decode([String].self, forKey: .recipeIngredient) + recipeInstructions = try container.decode([String].self, forKey: .recipeInstructions) + + nutrition = try container.decode(Dictionary.self, forKey: .nutrition).mapValues { String(describing: $0.value) } + } } diff --git a/Nextcloud Cookbook iOS Client/Util/JsonAny.swift b/Nextcloud Cookbook iOS Client/Util/JsonAny.swift new file mode 100644 index 0000000..c8cb2cb --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Util/JsonAny.swift @@ -0,0 +1,35 @@ +// +// JsonAny.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 05.05.24. +// + +import Foundation + +struct JSONAny: Codable { + let value: Any + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let intVal = try? container.decode(Int.self) { + value = intVal + } else if let stringVal = try? container.decode(String.self) { + value = stringVal + } else { + throw DecodingError.typeMismatch(JSONAny.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unsupported type for JSONAny")) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch value { + case let intValue as Int: + try container.encode(intValue) + case let stringValue as String: + try container.encode(stringValue) + default: + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Unsupported type for JSONAny")) + } + } +} diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift index 9f9f89c..4eb75c8 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift @@ -131,8 +131,12 @@ struct MoreInformationSection: View { var body: some View { CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandInfoSection) { VStack(alignment: .leading) { - Text("Created: \(Date.convertISOStringToLocalString(isoDateString: viewModel.recipeDetail.dateCreated) ?? "")") - Text("Last modified: \(Date.convertISOStringToLocalString(isoDateString: viewModel.recipeDetail.dateModified) ?? "")") + if let dateCreated = viewModel.recipeDetail.dateCreated { + Text("Created: \(Date.convertISOStringToLocalString(isoDateString: dateCreated) ?? "")") + } + if let dateModified = viewModel.recipeDetail.dateModified { + Text("Last modified: \(Date.convertISOStringToLocalString(isoDateString: dateModified) ?? "")") + } if viewModel.observableRecipeDetail.url != "", let url = URL(string: viewModel.observableRecipeDetail.url) { HStack(alignment: .top) { Text("URL:")