Added recipe description, nutrition information and keywords to recipe detail view
This commit is contained in:
@@ -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,10 +111,29 @@ 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
|
||||||
@@ -115,6 +141,8 @@ struct RecipeImage {
|
|||||||
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
|
||||||
|
|||||||
@@ -832,6 +832,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"No keywords." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"No nutritional information." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"None" : {
|
"None" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -848,6 +854,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Nutrition" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Nutrition (%@)" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Ok" : {
|
"Ok" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user