Recipe decoding fixes
This commit is contained in:
@@ -64,6 +64,7 @@
|
|||||||
A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */ = {isa = PBXBuildFile; productRef = A9CA6CF52B4C63F200F78AB5 /* TPPDF */; };
|
A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */ = {isa = PBXBuildFile; productRef = A9CA6CF52B4C63F200F78AB5 /* TPPDF */; };
|
||||||
A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D89AAF2B4FE97800F49D92 /* TimerView.swift */; };
|
A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D89AAF2B4FE97800F49D92 /* TimerView.swift */; };
|
||||||
A9D8F9052B99F3E5009BACAE /* RecipeImportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D8F9042B99F3E4009BACAE /* RecipeImportSection.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 */; };
|
A9FA2AB62B5079B200A43702 /* alarm_sound_0.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@@ -145,6 +146,7 @@
|
|||||||
A9D89AAF2B4FE97800F49D92 /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.swift; sourceTree = "<group>"; };
|
A9D89AAF2B4FE97800F49D92 /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.swift; sourceTree = "<group>"; };
|
||||||
A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeImportSection.swift; sourceTree = "<group>"; };
|
A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeImportSection.swift; sourceTree = "<group>"; };
|
||||||
A9DA25D42B82096B0061FC2B /* Nextcloud-Cookbook-iOS-Client-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Nextcloud-Cookbook-iOS-Client-Info.plist"; sourceTree = SOURCE_ROOT; };
|
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 = "<group>"; };
|
||||||
A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = alarm_sound_0.mp3; sourceTree = "<group>"; };
|
A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = alarm_sound_0.mp3; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@@ -375,6 +377,7 @@
|
|||||||
A9805BEC2BAAC70E003B7231 /* NumberFormatter.swift */,
|
A9805BEC2BAAC70E003B7231 /* NumberFormatter.swift */,
|
||||||
A79AA8DF2AFF80E3007D25F2 /* DurationComponents.swift */,
|
A79AA8DF2AFF80E3007D25F2 /* DurationComponents.swift */,
|
||||||
A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */,
|
A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */,
|
||||||
|
A9E78A2A2BE7799F00206866 /* JsonAny.swift */,
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -583,6 +586,7 @@
|
|||||||
A787B0782B2B1E6400C2DF1B /* DateExtension.swift in Sources */,
|
A787B0782B2B1E6400C2DF1B /* DateExtension.swift in Sources */,
|
||||||
A79AA8E92B062DD1007D25F2 /* CookbookApiV1.swift in Sources */,
|
A79AA8E92B062DD1007D25F2 /* CookbookApiV1.swift in Sources */,
|
||||||
A7CD3FD22B2C546A00D764AD /* CollapsibleView.swift in Sources */,
|
A7CD3FD22B2C546A00D764AD /* CollapsibleView.swift in Sources */,
|
||||||
|
A9E78A2B2BE7799F00206866 /* JsonAny.swift in Sources */,
|
||||||
A9BBB3902B91BE31002DA7FF /* ObservableRecipeDetail.swift in Sources */,
|
A9BBB3902B91BE31002DA7FF /* ObservableRecipeDetail.swift in Sources */,
|
||||||
A97506212B92104700E86029 /* RecipeMetadataSection.swift in Sources */,
|
A97506212B92104700E86029 /* RecipeMetadataSection.swift in Sources */,
|
||||||
A70171B42AB2122900064C43 /* NetworkUtils.swift in Sources */,
|
A70171B42AB2122900064C43 /* NetworkUtils.swift in Sources */,
|
||||||
@@ -793,7 +797,7 @@
|
|||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 1.10;
|
MARKETING_VERSION = 1.10.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
|
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
@@ -837,7 +841,7 @@
|
|||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 1.10;
|
MARKETING_VERSION = 1.10.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
|
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Bucket
|
||||||
|
uuid = "12548645-7E78-4898-BA6E-B381B584DE15"
|
||||||
|
type = "1"
|
||||||
|
version = "2.0">
|
||||||
|
</Bucket>
|
||||||
@@ -135,12 +135,16 @@ import UIKit
|
|||||||
guard UserSettings.shared.storeRecipes else { return }
|
guard UserSettings.shared.storeRecipes else { return }
|
||||||
guard let recipes = self.recipes[category] else { return }
|
guard let recipes = self.recipes[category] else { return }
|
||||||
for recipe in recipes {
|
for recipe in recipes {
|
||||||
if needsUpdate(category: category, lastModified: recipe.dateModified) {
|
if let dateModified = recipe.dateModified {
|
||||||
|
if needsUpdate(category: category, lastModified: dateModified) {
|
||||||
print("\(recipe.name) needs an update. (last modified: \(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)
|
await updateRecipeDetail(id: recipe.recipe_id, withThumb: UserSettings.shared.storeThumb, withImage: UserSettings.shared.storeImages)
|
||||||
} else {
|
} else {
|
||||||
print("\(recipe.name) is up to date.")
|
print("\(recipe.name) is up to date.")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
await updateRecipeDetail(id: recipe.recipe_id, withThumb: UserSettings.shared.storeThumb, withImage: UserSettings.shared.storeImages)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
id = recipeDetail.id
|
id = recipeDetail.id
|
||||||
name = recipeDetail.name
|
name = recipeDetail.name
|
||||||
keywords = recipeDetail.keywords.isEmpty ? [] : recipeDetail.keywords.components(separatedBy: ",")
|
keywords = recipeDetail.keywords.isEmpty ? [] : recipeDetail.keywords.components(separatedBy: ",")
|
||||||
imageUrl = recipeDetail.imageUrl
|
imageUrl = recipeDetail.imageUrl ?? ""
|
||||||
prepTime = DurationComponents.fromPTString(recipeDetail.prepTime ?? "")
|
prepTime = DurationComponents.fromPTString(recipeDetail.prepTime ?? "")
|
||||||
cookTime = DurationComponents.fromPTString(recipeDetail.cookTime ?? "")
|
cookTime = DurationComponents.fromPTString(recipeDetail.cookTime ?? "")
|
||||||
totalTime = DurationComponents.fromPTString(recipeDetail.totalTime ?? "")
|
totalTime = DurationComponents.fromPTString(recipeDetail.totalTime ?? "")
|
||||||
description = recipeDetail.description
|
description = recipeDetail.description
|
||||||
url = recipeDetail.url
|
url = recipeDetail.url ?? ""
|
||||||
recipeYield = recipeDetail.recipeYield == 0 ? 1 : recipeDetail.recipeYield // Recipe yield should not be zero
|
recipeYield = recipeDetail.recipeYield == 0 ? 1 : recipeDetail.recipeYield // Recipe yield should not be zero
|
||||||
recipeCategory = recipeDetail.recipeCategory
|
recipeCategory = recipeDetail.recipeCategory
|
||||||
tool = recipeDetail.tool
|
tool = recipeDetail.tool
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import SwiftUI
|
|||||||
struct Recipe: Codable {
|
struct Recipe: Codable {
|
||||||
let name: String
|
let name: String
|
||||||
let keywords: String?
|
let keywords: String?
|
||||||
let dateCreated: String
|
let dateCreated: String?
|
||||||
let dateModified: String
|
let dateModified: String?
|
||||||
let imageUrl: String
|
let imageUrl: String?
|
||||||
let imagePlaceholderUrl: String
|
let imagePlaceholderUrl: String?
|
||||||
let recipe_id: Int
|
let recipe_id: Int
|
||||||
|
|
||||||
// Properties excluded from Codable
|
// Properties excluded from Codable
|
||||||
@@ -35,15 +35,15 @@ extension Recipe: Identifiable, Hashable {
|
|||||||
struct RecipeDetail: Codable {
|
struct RecipeDetail: Codable {
|
||||||
var name: String
|
var name: String
|
||||||
var keywords: String
|
var keywords: String
|
||||||
var dateCreated: String
|
var dateCreated: String?
|
||||||
var dateModified: String
|
var dateModified: String?
|
||||||
var imageUrl: String
|
var imageUrl: String?
|
||||||
var id: String
|
var id: String
|
||||||
var prepTime: String?
|
var prepTime: String?
|
||||||
var cookTime: String?
|
var cookTime: String?
|
||||||
var totalTime: String?
|
var totalTime: String?
|
||||||
var description: String
|
var description: String
|
||||||
var url: String
|
var url: String?
|
||||||
var recipeYield: Int
|
var recipeYield: Int
|
||||||
var recipeCategory: String
|
var recipeCategory: String
|
||||||
var tool: [String]
|
var tool: [String]
|
||||||
@@ -90,6 +90,33 @@ struct RecipeDetail: Codable {
|
|||||||
recipeInstructions = []
|
recipeInstructions = []
|
||||||
nutrition = [:]
|
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<String, JSONAny>.self, forKey: .nutrition).mapValues { String(describing: $0.value) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
35
Nextcloud Cookbook iOS Client/Util/JsonAny.swift
Normal file
35
Nextcloud Cookbook iOS Client/Util/JsonAny.swift
Normal file
@@ -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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,8 +131,12 @@ struct MoreInformationSection: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandInfoSection) {
|
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandInfoSection) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("Created: \(Date.convertISOStringToLocalString(isoDateString: viewModel.recipeDetail.dateCreated) ?? "")")
|
if let dateCreated = viewModel.recipeDetail.dateCreated {
|
||||||
Text("Last modified: \(Date.convertISOStringToLocalString(isoDateString: viewModel.recipeDetail.dateModified) ?? "")")
|
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) {
|
if viewModel.observableRecipeDetail.url != "", let url = URL(string: viewModel.observableRecipeDetail.url) {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
Text("URL:")
|
Text("URL:")
|
||||||
|
|||||||
Reference in New Issue
Block a user