Added recipe description, nutrition information and keywords to recipe detail view

This commit is contained in:
Vicnet
2023-10-24 19:09:21 +02:00
parent 8f32946e27
commit 04980b64c7
5 changed files with 192 additions and 43 deletions

View File

@@ -31,6 +31,8 @@ extension Recipe: Identifiable, Hashable {
var id: String { name } var id: String { name }
} }
struct RecipeDetail: Codable { struct RecipeDetail: Codable {
var name: String var name: String
var keywords: String var keywords: String
@@ -48,8 +50,9 @@ struct RecipeDetail: Codable {
var tool: [String] var tool: [String]
var recipeIngredient: [String] var recipeIngredient: [String]
var recipeInstructions: [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.name = name
self.keywords = keywords self.keywords = keywords
self.dateCreated = dateCreated self.dateCreated = dateCreated
@@ -66,6 +69,7 @@ struct RecipeDetail: Codable {
self.tool = tool self.tool = tool
self.recipeIngredient = recipeIngredient self.recipeIngredient = recipeIngredient
self.recipeInstructions = recipeInstructions self.recipeInstructions = recipeInstructions
self.nutrition = nutrition
} }
init() { init() {
@@ -85,9 +89,12 @@ struct RecipeDetail: Codable {
tool = [] tool = []
recipeIngredient = [] recipeIngredient = []
recipeInstructions = [] recipeInstructions = []
nutrition = [:]
} }
}
static func error() -> RecipeDetail { extension RecipeDetail {
static var error: RecipeDetail {
return RecipeDetail( return RecipeDetail(
name: "Error: Unable to load recipe.", name: "Error: Unable to load recipe.",
keywords: "", keywords: "",
@@ -104,17 +111,38 @@ struct RecipeDetail: Codable {
recipeCategory: "", recipeCategory: "",
tool: [], tool: [],
recipeIngredient: [], recipeIngredient: [],
recipeInstructions: [] 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 { struct RecipeImage {
var imageExists: Bool = true var imageExists: Bool = true
var thumb: UIImage? var thumb: UIImage?
var full: UIImage? var full: UIImage?
} }
struct RecipeKeyword: Codable { struct RecipeKeyword: Codable {
let name: String let name: String
let recipe_count: Int let recipe_count: Int

View File

@@ -832,6 +832,12 @@
} }
} }
} }
},
"No keywords." : {
},
"No nutritional information." : {
}, },
"None" : { "None" : {
"localizations" : { "localizations" : {
@@ -848,6 +854,12 @@
} }
} }
} }
},
"Nutrition" : {
},
"Nutrition (%@)" : {
}, },
"Ok" : { "Ok" : {
"localizations" : { "localizations" : {

View File

@@ -96,7 +96,7 @@ import SwiftUI
recipeDetails[recipeId] = recipeDetail recipeDetails[recipeId] = recipeDetail
return recipeDetail return recipeDetail
} }
return RecipeDetail.error() return RecipeDetail.error
} }
func downloadAllRecipes() async { func downloadAllRecipes() async {

View File

@@ -51,7 +51,6 @@ struct MainView: View {
} }
} }
} }
.navigationTitle("Cookbooks") .navigationTitle("Cookbooks")
.navigationDestination(isPresented: $showSettingsView) { .navigationDestination(isPresented: $showSettingsView) {
SettingsView(userSettings: userSettings, viewModel: viewModel) SettingsView(userSettings: userSettings, viewModel: viewModel)

View File

@@ -17,6 +17,8 @@ struct RecipeDetailView: View {
@State var showTitle: Bool = false @State var showTitle: Bool = false
@State var isDownloaded: Bool? = nil @State var isDownloaded: Bool? = nil
@State private var presentEditView: Bool = false @State private var presentEditView: Bool = false
@State private var presentNutritionPopover: Bool = false
@State private var presentKeywordPopover: Bool = false
var body: some View { var body: some View {
ScrollView(showsIndicators: false) { ScrollView(showsIndicators: false) {
@@ -31,7 +33,6 @@ struct RecipeDetailView: View {
if let recipeDetail = recipeDetail { if let recipeDetail = recipeDetail {
LazyVStack (alignment: .leading) { LazyVStack (alignment: .leading) {
Divider()
HStack { HStack {
Text(recipeDetail.name) Text(recipeDetail.name)
.font(.title) .font(.title)
@@ -51,18 +52,28 @@ struct RecipeDetailView: View {
.padding() .padding()
} }
} }
if recipeDetail.description != "" {
Text(recipeDetail.description)
.padding([.bottom, .horizontal])
}
Divider() Divider()
RecipeDurationSection(recipeDetail: recipeDetail) RecipeDurationSection(recipeDetail: recipeDetail)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) { LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
if(!recipeDetail.recipeIngredient.isEmpty) { if(!recipeDetail.recipeIngredient.isEmpty) {
RecipeIngredientSection(recipeDetail: recipeDetail) RecipeIngredientSection(recipeDetail: recipeDetail)
} }
if(!recipeDetail.tool.isEmpty) { if(!recipeDetail.tool.isEmpty) {
RecipeToolSection(recipeDetail: recipeDetail) RecipeListSection(title: "Tools", list: recipeDetail.tool)
} }
if(!recipeDetail.recipeInstructions.isEmpty) { if(!recipeDetail.recipeInstructions.isEmpty) {
RecipeInstructionSection(recipeDetail: recipeDetail) RecipeInstructionSection(recipeDetail: recipeDetail)
} }
RecipeNutritionSection(recipeDetail: recipeDetail, presentNutritionPopover: $presentNutritionPopover)
RecipeKeywordSection(recipeDetail: recipeDetail, presentKeywordPopover: $presentKeywordPopover)
} }
}.padding(.horizontal, 5) }.padding(.horizontal, 5)
@@ -104,9 +115,9 @@ fileprivate struct RecipeDurationSection: View {
@State var recipeDetail: RecipeDetail @State var recipeDetail: RecipeDetail
var body: some View { var body: some View {
HStack(alignment: .center) { LazyVGrid(columns: [GridItem(.adaptive(minimum: 150), alignment: .leading)]) {
if let prepTime = recipeDetail.prepTime { if let prepTime = recipeDetail.prepTime {
VStack { VStack(alignment: .leading) {
SecondaryLabel(text: LocalizedStringKey("Preparation")) SecondaryLabel(text: LocalizedStringKey("Preparation"))
Text(DateFormatter.formatDate(duration: prepTime)) Text(DateFormatter.formatDate(duration: prepTime))
.lineLimit(1) .lineLimit(1)
@@ -114,7 +125,7 @@ fileprivate struct RecipeDurationSection: View {
} }
if let cookTime = recipeDetail.cookTime { if let cookTime = recipeDetail.cookTime {
VStack { VStack(alignment: .leading) {
SecondaryLabel(text: LocalizedStringKey("Cooking")) SecondaryLabel(text: LocalizedStringKey("Cooking"))
Text(DateFormatter.formatDate(duration: cookTime)) Text(DateFormatter.formatDate(duration: cookTime))
.lineLimit(1) .lineLimit(1)
@@ -122,7 +133,7 @@ fileprivate struct RecipeDurationSection: View {
} }
if let totalTime = recipeDetail.totalTime { if let totalTime = recipeDetail.totalTime {
VStack { VStack(alignment: .leading) {
SecondaryLabel(text: LocalizedStringKey("Total time")) SecondaryLabel(text: LocalizedStringKey("Total time"))
Text(DateFormatter.formatDate(duration: totalTime)) Text(DateFormatter.formatDate(duration: totalTime))
.lineLimit(1) .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 { fileprivate struct RecipeIngredientSection: View {
@State var recipeDetail: RecipeDetail @State var recipeDetail: RecipeDetail
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Divider()
HStack { HStack {
if recipeDetail.recipeYield == 0 { if recipeDetail.recipeYield == 0 {
SecondaryLabel(text: LocalizedStringKey("Ingredients")) SecondaryLabel(text: LocalizedStringKey("Ingredients"))
@@ -168,9 +260,9 @@ fileprivate struct IngredientListItem: View {
if isSelected { if isSelected {
Image(systemName: "checkmark.circle") Image(systemName: "checkmark.circle")
} else { } else {
//Text("\u{2022}")
Image(systemName: "circle") Image(systemName: "circle")
} }
Text("\(ingredient)") Text("\(ingredient)")
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.lineLimit(5) .lineLimit(5)
@@ -185,19 +277,20 @@ fileprivate struct IngredientListItem: View {
fileprivate struct RecipeToolSection: View { fileprivate struct RecipeListSection: View {
@State var recipeDetail: RecipeDetail @State var title: LocalizedStringKey
@State var list: [String]
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Divider()
HStack { HStack {
SecondaryLabel(text: LocalizedStringKey("Tools")) SecondaryLabel(text: title)
Spacer() Spacer()
} }
ForEach(recipeDetail.tool, id: \.self) { tool in ForEach(list, id: \.self) { item in
HStack(alignment: .top) { HStack(alignment: .top) {
Text("\u{2022}") Text("\u{2022}")
Text("\(tool)") Text("\(item)")
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
} }
.padding(4) .padding(4)
@@ -212,16 +305,12 @@ fileprivate struct RecipeInstructionSection: View {
@State var recipeDetail: RecipeDetail @State var recipeDetail: RecipeDetail
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Divider()
HStack { HStack {
SecondaryLabel(text: LocalizedStringKey("Instructions")) SecondaryLabel(text: LocalizedStringKey("Instructions"))
Spacer() Spacer()
} }
ForEach(0..<recipeDetail.recipeInstructions.count) { ix in ForEach(0..<recipeDetail.recipeInstructions.count) { ix in
HStack(alignment: .top) { RecipeInstructionListItem(instruction: recipeDetail.recipeInstructions[ix], index: ix+1)
Text("\(ix+1).")
Text("\(recipeDetail.recipeInstructions[ix])")
}.padding(4)
} }
}.padding() }.padding()
} }
@@ -229,6 +318,27 @@ fileprivate struct RecipeInstructionSection: View {
fileprivate struct RecipeInstructionListItem: View {
@State var instruction: String
@State var index: Int
@State var isSelected: Bool = false
var body: some View {
HStack(alignment: .top) {
Text("\(index)")
.monospaced()
Text(instruction)
}.padding(4)
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
.onTapGesture {
isSelected.toggle()
}
.animation(.easeInOut, value: isSelected)
}
}
fileprivate struct SecondaryLabel: View { fileprivate struct SecondaryLabel: View {
let text: LocalizedStringKey let text: LocalizedStringKey
var body: some View { var body: some View {