From 04980b64c7d803565532e6f1e306e7f9c9aee563 Mon Sep 17 00:00:00 2001 From: Vicnet <35202538+VincentMeilinger@users.noreply.github.com> Date: Tue, 24 Oct 2023 19:09:21 +0200 Subject: [PATCH] Added recipe description, nutrition information and keywords to recipe detail view --- .../Data/DataModels.swift | 72 ++++++--- .../Localizable.xcstrings | 12 ++ .../ViewModels/MainViewModel.swift | 2 +- .../Views/MainView.swift | 1 - .../Views/RecipeDetailView.swift | 148 +++++++++++++++--- 5 files changed, 192 insertions(+), 43 deletions(-) diff --git a/Nextcloud Cookbook iOS Client/Data/DataModels.swift b/Nextcloud Cookbook iOS Client/Data/DataModels.swift index 37270ce..4040e98 100644 --- a/Nextcloud Cookbook iOS Client/Data/DataModels.swift +++ b/Nextcloud Cookbook iOS Client/Data/DataModels.swift @@ -31,6 +31,8 @@ extension Recipe: Identifiable, Hashable { var id: String { name } } + + struct RecipeDetail: Codable { var name: String var keywords: String @@ -48,8 +50,9 @@ struct RecipeDetail: Codable { var tool: [String] var recipeIngredient: [String] var recipeInstructions: [String] + var nutrition: [String:String] - 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]) { + 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]) { self.name = name self.keywords = keywords self.dateCreated = dateCreated @@ -66,6 +69,7 @@ struct RecipeDetail: Codable { self.tool = tool self.recipeIngredient = recipeIngredient self.recipeInstructions = recipeInstructions + self.nutrition = nutrition } init() { @@ -85,36 +89,60 @@ struct RecipeDetail: Codable { tool = [] recipeIngredient = [] recipeInstructions = [] - } - - static func 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 = [:] } } +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 getNutritionList() -> [String]? { + var stringList: [String] = [] + if let value = nutrition["calories"] { stringList.append("Calories: \(value)") } + if let value = nutrition["carbohydrateContent"] { stringList.append("Carbohydrates: \(value)") } + if let value = nutrition["cholesterolContent"] { stringList.append("Cholesterol: \(value)") } + if let value = nutrition["fatContent"] { stringList.append("Fat: \(value)") } + if let value = nutrition["saturatedFatContent"] { stringList.append("Saturated fat: \(value)") } + if let value = nutrition["unsaturatedFatContent"] { stringList.append("Unsaturated fat: \(value)") } + if let value = nutrition["transFatContent"] { stringList.append("Trans fat: \(value)") } + if let value = nutrition["fiberContent"] { stringList.append("Fibers: \(value)") } + if let value = nutrition["proteinContent"] { stringList.append("Protein: \(value)") } + if let value = nutrition["sodiumContent"] { stringList.append("Sodium: \(value)") } + if let value = nutrition["sugarContent"] { stringList.append("Sugar: \(value)") } + return stringList.isEmpty ? nil : stringList + } +} + + + struct RecipeImage { var imageExists: Bool = true var thumb: UIImage? var full: UIImage? } + + struct RecipeKeyword: Codable { let name: String let recipe_count: Int diff --git a/Nextcloud Cookbook iOS Client/Localizable.xcstrings b/Nextcloud Cookbook iOS Client/Localizable.xcstrings index ec2c01d..e60a0c2 100644 --- a/Nextcloud Cookbook iOS Client/Localizable.xcstrings +++ b/Nextcloud Cookbook iOS Client/Localizable.xcstrings @@ -832,6 +832,12 @@ } } } + }, + "No keywords." : { + + }, + "No nutritional information." : { + }, "None" : { "localizations" : { @@ -848,6 +854,12 @@ } } } + }, + "Nutrition" : { + + }, + "Nutrition (%@)" : { + }, "Ok" : { "localizations" : { diff --git a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift index 65c22b1..cbc1d1c 100644 --- a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift +++ b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift @@ -96,7 +96,7 @@ import SwiftUI recipeDetails[recipeId] = recipeDetail return recipeDetail } - return RecipeDetail.error() + return RecipeDetail.error } func downloadAllRecipes() async { diff --git a/Nextcloud Cookbook iOS Client/Views/MainView.swift b/Nextcloud Cookbook iOS Client/Views/MainView.swift index 5061e6f..a514504 100644 --- a/Nextcloud Cookbook iOS Client/Views/MainView.swift +++ b/Nextcloud Cookbook iOS Client/Views/MainView.swift @@ -51,7 +51,6 @@ struct MainView: View { } } } - .navigationTitle("Cookbooks") .navigationDestination(isPresented: $showSettingsView) { SettingsView(userSettings: userSettings, viewModel: viewModel) diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift index 169cee1..cdab9cd 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift @@ -17,6 +17,8 @@ struct RecipeDetailView: View { @State var showTitle: Bool = false @State var isDownloaded: Bool? = nil @State private var presentEditView: Bool = false + @State private var presentNutritionPopover: Bool = false + @State private var presentKeywordPopover: Bool = false var body: some View { ScrollView(showsIndicators: false) { @@ -31,7 +33,6 @@ struct RecipeDetailView: View { if let recipeDetail = recipeDetail { LazyVStack (alignment: .leading) { - Divider() HStack { Text(recipeDetail.name) .font(.title) @@ -51,18 +52,28 @@ struct RecipeDetailView: View { .padding() } } + + if recipeDetail.description != "" { + Text(recipeDetail.description) + .padding([.bottom, .horizontal]) + } + Divider() + RecipeDurationSection(recipeDetail: recipeDetail) + LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) { if(!recipeDetail.recipeIngredient.isEmpty) { RecipeIngredientSection(recipeDetail: recipeDetail) } if(!recipeDetail.tool.isEmpty) { - RecipeToolSection(recipeDetail: recipeDetail) + RecipeListSection(title: "Tools", list: recipeDetail.tool) } if(!recipeDetail.recipeInstructions.isEmpty) { RecipeInstructionSection(recipeDetail: recipeDetail) } + RecipeNutritionSection(recipeDetail: recipeDetail, presentNutritionPopover: $presentNutritionPopover) + RecipeKeywordSection(recipeDetail: recipeDetail, presentKeywordPopover: $presentKeywordPopover) } }.padding(.horizontal, 5) @@ -104,9 +115,9 @@ fileprivate struct RecipeDurationSection: View { @State var recipeDetail: RecipeDetail var body: some View { - HStack(alignment: .center) { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 150), alignment: .leading)]) { if let prepTime = recipeDetail.prepTime { - VStack { + VStack(alignment: .leading) { SecondaryLabel(text: LocalizedStringKey("Preparation")) Text(DateFormatter.formatDate(duration: prepTime)) .lineLimit(1) @@ -114,7 +125,7 @@ fileprivate struct RecipeDurationSection: View { } if let cookTime = recipeDetail.cookTime { - VStack { + VStack(alignment: .leading) { SecondaryLabel(text: LocalizedStringKey("Cooking")) Text(DateFormatter.formatDate(duration: cookTime)) .lineLimit(1) @@ -122,7 +133,7 @@ fileprivate struct RecipeDurationSection: View { } if let totalTime = recipeDetail.totalTime { - VStack { + VStack(alignment: .leading) { SecondaryLabel(text: LocalizedStringKey("Total time")) Text(DateFormatter.formatDate(duration: totalTime)) .lineLimit(1) @@ -134,11 +145,92 @@ fileprivate struct RecipeDurationSection: View { +fileprivate struct RecipeNutritionSection: View { + @State var recipeDetail: RecipeDetail + @Binding var presentNutritionPopover: Bool + + var body: some View { + Button { + presentNutritionPopover.toggle() + } label: { + HStack { + SecondaryLabel(text: "Nutrition") + Image(systemName: "chevron.right") + .foregroundStyle(Color.secondary) + .bold() + Spacer() + }.padding() + } + .buttonStyle(.plain) + .popover(isPresented: $presentNutritionPopover) { + if let nutritionList = recipeDetail.getNutritionList() { + ScrollView(showsIndicators: false) { + if let servingSize = recipeDetail.nutrition["servingSize"] { + RecipeListSection(title: "Nutrition (\(servingSize))", list: nutritionList) + .presentationCompactAdaptation(.popover) + } else { + RecipeListSection(title: "Nutrition", list: nutritionList) + .presentationCompactAdaptation(.popover) + } + } + } else { + Text(LocalizedStringKey("No nutritional information.")) + .foregroundStyle(Color.secondary) + .bold() + .padding() + .presentationCompactAdaptation(.popover) + } + } + } +} + + +fileprivate struct RecipeKeywordSection: View { + @State var recipeDetail: RecipeDetail + @Binding var presentKeywordPopover: Bool + + var body: some View { + Button { + presentKeywordPopover.toggle() + } label: { + HStack { + SecondaryLabel(text: "Keywords") + Image(systemName: "chevron.right") + .foregroundStyle(Color.secondary) + .bold() + Spacer() + }.padding() + } + .buttonStyle(.plain) + .popover(isPresented: $presentKeywordPopover) { + if let keywords = getKeywords() { + ScrollView(showsIndicators: false) { + RecipeListSection(title: "Keywords", list: keywords) + .presentationCompactAdaptation(.popover) + } + } else { + Text(LocalizedStringKey("No keywords.")) + .foregroundStyle(Color.secondary) + .bold() + .padding() + .presentationCompactAdaptation(.popover) + } + + } + } + + func getKeywords() -> [String]? { + let keywords = recipeDetail.keywords.components(separatedBy: ",") + return keywords.isEmpty ? nil : keywords + } +} + + + fileprivate struct RecipeIngredientSection: View { @State var recipeDetail: RecipeDetail var body: some View { VStack(alignment: .leading) { - Divider() HStack { if recipeDetail.recipeYield == 0 { SecondaryLabel(text: LocalizedStringKey("Ingredients")) @@ -168,9 +260,9 @@ fileprivate struct IngredientListItem: View { if isSelected { Image(systemName: "checkmark.circle") } else { - //Text("\u{2022}") Image(systemName: "circle") } + Text("\(ingredient)") .multilineTextAlignment(.leading) .lineLimit(5) @@ -185,19 +277,20 @@ fileprivate struct IngredientListItem: View { -fileprivate struct RecipeToolSection: View { - @State var recipeDetail: RecipeDetail +fileprivate struct RecipeListSection: View { + @State var title: LocalizedStringKey + @State var list: [String] + var body: some View { VStack(alignment: .leading) { - Divider() HStack { - SecondaryLabel(text: LocalizedStringKey("Tools")) + SecondaryLabel(text: title) Spacer() } - ForEach(recipeDetail.tool, id: \.self) { tool in + ForEach(list, id: \.self) { item in HStack(alignment: .top) { Text("\u{2022}") - Text("\(tool)") + Text("\(item)") .multilineTextAlignment(.leading) } .padding(4) @@ -212,16 +305,12 @@ fileprivate struct RecipeInstructionSection: View { @State var recipeDetail: RecipeDetail var body: some View { VStack(alignment: .leading) { - Divider() HStack { SecondaryLabel(text: LocalizedStringKey("Instructions")) Spacer() } ForEach(0..