// // RecipeModels.swift // Nextcloud Cookbook iOS Client // // Created by Vincent Meilinger on 17.02.24. // import Foundation import SwiftUI struct Recipe: Codable { let name: String let keywords: String? let dateCreated: String? let dateModified: String? let imageUrl: String? let imagePlaceholderUrl: String? let recipe_id: Int // Properties excluded from Codable var storedLocally: Bool? = nil private enum CodingKeys: String, CodingKey { case name, keywords, dateCreated, dateModified, imageUrl, imagePlaceholderUrl, recipe_id } } extension Recipe: Identifiable, Hashable { var id: Int { recipe_id } } struct RecipeDetail: Codable { var name: String var keywords: 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 recipeYield: Int var recipeCategory: String var tool: [String] var recipeIngredient: [String] var recipeInstructions: [String] var nutrition: [String:String] var groceryState: GroceryState? var mealPlanAssignment: MealPlanAssignment? init(name: String, keywords: String, dateCreated: String, dateModified: String, imageUrl: String, id: String, prepTime: String? = nil, cookTime: String? = nil, totalTime: String? = nil, description: String, url: String, recipeYield: Int, recipeCategory: String, tool: [String], recipeIngredient: [String], recipeInstructions: [String], nutrition: [String:String], groceryState: GroceryState? = nil, mealPlanAssignment: MealPlanAssignment? = nil) { self.name = name self.keywords = keywords self.dateCreated = dateCreated self.dateModified = dateModified self.imageUrl = imageUrl self.id = id self.prepTime = prepTime self.cookTime = cookTime self.totalTime = totalTime self.description = description self.url = url self.recipeYield = recipeYield self.recipeCategory = recipeCategory self.tool = tool self.recipeIngredient = recipeIngredient self.recipeInstructions = recipeInstructions self.nutrition = nutrition self.groceryState = groceryState self.mealPlanAssignment = mealPlanAssignment } init() { name = "" keywords = "" dateCreated = "" dateModified = "" imageUrl = "" id = "" prepTime = "" cookTime = "" totalTime = "" description = "" url = "" recipeYield = 0 recipeCategory = "" tool = [] recipeIngredient = [] recipeInstructions = [] nutrition = [:] groceryState = nil mealPlanAssignment = nil } // Custom decoder to handle value type ambiguity private enum CodingKeys: String, CodingKey { case name, keywords, dateCreated, dateModified, image, imageUrl, id, prepTime, cookTime, totalTime, description, url, recipeYield, recipeCategory, tool, recipeIngredient, recipeInstructions, nutrition case groceryState = "_groceryState" case mealPlanAssignment = "_mealPlanAssignment" } 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) // Server import returns "image"; show/index responses and local storage use "imageUrl" imageUrl = (try? container.decodeIfPresent(String.self, forKey: .image)) ?? (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) recipeCategory = (try? container.decode(String.self, forKey: .recipeCategory)) ?? "" // recipeYield: try Int first, then parse leading digits from String if let yieldInt = try? container.decode(Int.self, forKey: .recipeYield) { recipeYield = yieldInt } else if let yieldString = try? container.decode(String.self, forKey: .recipeYield) { let digits = yieldString.prefix(while: { $0.isNumber }) recipeYield = Int(digits) ?? 0 } else { recipeYield = 0 } tool = (try? container.decode([String].self, forKey: .tool)) ?? [] recipeIngredient = (try? container.decode([String].self, forKey: .recipeIngredient)) ?? [] recipeInstructions = (try? container.decode([String].self, forKey: .recipeInstructions)) ?? [] if let nutritionDict = try? container.decode(Dictionary.self, forKey: .nutrition) { nutrition = nutritionDict.mapValues { String(describing: $0.value) } } else { nutrition = [:] } groceryState = try? container.decode(GroceryState.self, forKey: .groceryState) mealPlanAssignment = try? container.decode(MealPlanAssignment.self, forKey: .mealPlanAssignment) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(name, forKey: .name) try container.encode(keywords, forKey: .keywords) try container.encodeIfPresent(dateCreated, forKey: .dateCreated) try container.encodeIfPresent(dateModified, forKey: .dateModified) // Encode under "image" — the key the server expects for create/update try container.encodeIfPresent(imageUrl, forKey: .image) try container.encode(id, forKey: .id) try container.encodeIfPresent(prepTime, forKey: .prepTime) try container.encodeIfPresent(cookTime, forKey: .cookTime) try container.encodeIfPresent(totalTime, forKey: .totalTime) try container.encode(description, forKey: .description) try container.encodeIfPresent(url, forKey: .url) try container.encode(recipeYield, forKey: .recipeYield) try container.encode(recipeCategory, forKey: .recipeCategory) try container.encode(tool, forKey: .tool) try container.encode(recipeIngredient, forKey: .recipeIngredient) try container.encode(recipeInstructions, forKey: .recipeInstructions) try container.encode(nutrition, forKey: .nutrition) try container.encodeIfPresent(groceryState, forKey: .groceryState) try container.encodeIfPresent(mealPlanAssignment, forKey: .mealPlanAssignment) } } extension RecipeDetail { static var error: RecipeDetail { return RecipeDetail( name: "Error: Unable to load recipe.", keywords: "", dateCreated: "", dateModified: "", imageUrl: "", id: "", prepTime: "", cookTime: "", totalTime: "", description: "", url: "", recipeYield: 0, recipeCategory: "", tool: [], recipeIngredient: [], recipeInstructions: [], nutrition: [:] ) } func getKeywordsArray() -> [String] { if keywords == "" { return [] } return keywords.components(separatedBy: ",") } mutating func setKeywordsFromArray(_ keywordsArray: [String]) { if !keywordsArray.isEmpty { self.keywords = keywordsArray.joined(separator: ",") } } } struct RecipeImage { enum RecipeImageSize: String { case THUMB="thumb", FULL="full" } var imageExists: Bool = true var thumb: UIImage? var full: UIImage? } struct RecipeKeyword: Codable { let name: String let recipe_count: Int } enum Nutrition: CaseIterable { case servingSize, calories, carbohydrateContent, cholesterolContent, fatContent, saturatedFatContent, unsaturatedFatContent, transFatContent, fiberContent, proteinContent, sodiumContent, sugarContent var localizedDescription: String { switch self { case .servingSize: return NSLocalizedString("Serving size", comment: "Serving size") case .calories: return NSLocalizedString("Calories", comment: "Calories") case .carbohydrateContent: return NSLocalizedString("Carbohydrate content", comment: "Carbohydrate content") case .cholesterolContent: return NSLocalizedString("Cholesterol content", comment: "Cholesterol content") case .fatContent: return NSLocalizedString("Fat content", comment: "Fat content") case .saturatedFatContent: return NSLocalizedString("Saturated fat content", comment: "Saturated fat content") case .unsaturatedFatContent: return NSLocalizedString("Unsaturated fat content", comment: "Unsaturated fat content") case .transFatContent: return NSLocalizedString("Trans fat content", comment: "Trans fat content") case .fiberContent: return NSLocalizedString("Fiber content", comment: "Fiber content") case .proteinContent: return NSLocalizedString("Protein content", comment: "Protein content") case .sodiumContent: return NSLocalizedString("Sodium content", comment: "Sodium content") case .sugarContent: return NSLocalizedString("Sugar content", comment: "Sugar content") } } var dictKey: String { switch self { case .servingSize: "servingSize" case .calories: "calories" case .carbohydrateContent: "carbohydrateContent" case .cholesterolContent: "cholesterolContent" case .fatContent: "fatContent" case .saturatedFatContent: "saturatedFatContent" case .unsaturatedFatContent: "unsaturatedFatContent" case .transFatContent: "transFatContent" case .fiberContent: "fiberContent" case .proteinContent: "proteinContent" case .sodiumContent: "sodiumContent" case .sugarContent: "sugarContent" } } }