Creating and updating recipes works in the new edit view

This commit is contained in:
VincentMeilinger
2024-03-04 11:58:16 +01:00
parent aa45bbdbd8
commit 597477544d
7 changed files with 132 additions and 131 deletions

View File

@@ -46,6 +46,7 @@
} }
}, },
":" : { ":" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -273,6 +274,7 @@
} }
}, },
"00" : { "00" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -3069,6 +3071,9 @@
} }
} }
} }
},
"Total" : {
}, },
"Total duration:" : { "Total duration:" : {
"localizations" : { "localizations" : {

View File

@@ -10,64 +10,42 @@ import SwiftUI
class DurationComponents: ObservableObject { class DurationComponents: ObservableObject {
@Published var secondComponent: String = "00" { @Published var secondComponent: Int = 0 {
didSet { didSet {
if secondComponent.count > 2 { if secondComponent > 59 {
secondComponent = oldValue secondComponent = 59
} else if secondComponent.count == 1 { } else if secondComponent < 0 {
secondComponent = "0\(secondComponent)" secondComponent = 0
} else if secondComponent.count == 0 {
secondComponent = "00"
} }
let filtered = secondComponent.filter { $0.isNumber } }
if secondComponent != filtered { }
secondComponent = filtered @Published var minuteComponent: Int = 0 {
didSet {
if minuteComponent > 59 {
minuteComponent = 59
} else if minuteComponent < 0 {
minuteComponent = 0
} }
} }
} }
@Published var minuteComponent: String = "00" { @Published var hourComponent: Int = 0 {
didSet { didSet {
if minuteComponent.count > 2 { if hourComponent < 0 {
minuteComponent = oldValue hourComponent = 0
} else if minuteComponent.count == 1 {
minuteComponent = "0\(minuteComponent)"
} else if minuteComponent.count == 0 {
minuteComponent = "00"
}
let filtered = minuteComponent.filter { $0.isNumber }
if minuteComponent != filtered {
minuteComponent = filtered
} }
} }
} }
@Published var hourComponent: String = "00" {
didSet {
if hourComponent.count > 2 {
hourComponent = oldValue
} else if hourComponent.count == 1 {
hourComponent = "0\(hourComponent)"
} else if hourComponent.count == 0 {
hourComponent = "00"
}
let filtered = hourComponent.filter { $0.isNumber }
if hourComponent != filtered {
hourComponent = filtered
}
}
}
var displayString: String { var displayString: String {
let intHour = Int(hourComponent) ?? 0 if hourComponent != 0 && minuteComponent != 0 {
let intMinute = Int(minuteComponent) ?? 0 return "\(hourComponent) h \(minuteComponent) min"
} else if hourComponent == 0 && minuteComponent != 0 {
if intHour != 0 && intMinute != 0 { return "\(minuteComponent) min"
return "\(intHour) h \(intMinute) min" } else if hourComponent != 0 && minuteComponent == 0 {
} else if intHour == 0 && intMinute != 0 { return "\(hourComponent) h"
return "\(intMinute) min"
} else if intHour != 0 && intMinute == 0 {
return "\(intHour) h"
} else { } else {
return "-" return "-"
} }
@@ -78,10 +56,10 @@ class DurationComponents: ObservableObject {
let hourRegex = /([0-9]{1,2})H/ let hourRegex = /([0-9]{1,2})H/
let minuteRegex = /([0-9]{1,2})M/ let minuteRegex = /([0-9]{1,2})M/
if let match = PTRepresentation.firstMatch(of: hourRegex) { if let match = PTRepresentation.firstMatch(of: hourRegex) {
duration.hourComponent = String(match.1) duration.hourComponent = Int(match.1) ?? 0
} }
if let match = PTRepresentation.firstMatch(of: minuteRegex) { if let match = PTRepresentation.firstMatch(of: minuteRegex) {
duration.minuteComponent = String(match.1) duration.minuteComponent = Int(match.1) ?? 0
} }
return duration return duration
} }
@@ -90,41 +68,47 @@ class DurationComponents: ObservableObject {
let hourRegex = /([0-9]{1,2})H/ let hourRegex = /([0-9]{1,2})H/
let minuteRegex = /([0-9]{1,2})M/ let minuteRegex = /([0-9]{1,2})M/
if let match = PTRepresentation.firstMatch(of: hourRegex) { if let match = PTRepresentation.firstMatch(of: hourRegex) {
self.hourComponent = String(match.1) self.hourComponent = Int(match.1) ?? 0
} }
if let match = PTRepresentation.firstMatch(of: minuteRegex) { if let match = PTRepresentation.firstMatch(of: minuteRegex) {
self.minuteComponent = String(match.1) self.minuteComponent = Int(match.1) ?? 0
} }
} }
private func stringFormatComponents() -> (String, String, String) {
let sec = secondComponent < 10 ? "0\(secondComponent)" : "\(secondComponent)"
let min = minuteComponent < 10 ? "0\(minuteComponent)" : "\(minuteComponent)"
let hr = hourComponent < 10 ? "0\(hourComponent)" : "\(hourComponent)"
return (hr, min, sec)
}
func toPTString() -> String { func toPTString() -> String {
return "PT\(hourComponent)H\(minuteComponent)M00S" let (hr, min, sec) = stringFormatComponents()
return "PT\(hr)H\(min)M\(sec)S"
} }
func toTimerText() -> String { func toTimerText() -> String {
var timeString = "" var timeString = ""
if hourComponent != "00" { let (hr, min, sec) = stringFormatComponents()
timeString.append("\(hourComponent):") if hourComponent != 0 {
timeString.append("\(hr):")
} }
timeString.append("\(minuteComponent):") timeString.append("\(min):")
timeString.append("\(secondComponent)") timeString.append(sec)
return timeString return timeString
} }
func toSeconds() -> Double { func toSeconds() -> Double {
guard let hours = Double(hourComponent) else { return 0 } return Double(hourComponent) * 3600 + Double(minuteComponent) * 60 + Double(secondComponent)
guard let minutes = Double(minuteComponent) else { return 0 }
guard let seconds = Double(secondComponent) else { return 0 }
return hours * 3600 + minutes * 60 + seconds
} }
func fromSeconds(_ totalSeconds: Int) { func fromSeconds(_ totalSeconds: Int) {
let hours = totalSeconds / 3600 let hours = totalSeconds / 3600
let minutes = (totalSeconds % 3600) / 60 let minutes = (totalSeconds % 3600) / 60
let seconds = totalSeconds % 60 let seconds = totalSeconds % 60
self.hourComponent = String(hours) self.hourComponent = Int(hours)
self.minuteComponent = String(minutes) self.minuteComponent = Int(minutes)
self.secondComponent = String(seconds) self.secondComponent = Int(seconds)
} }
static func ptToText(_ ptString: String) -> String? { static func ptToText(_ ptString: String) -> String? {

View File

@@ -267,17 +267,7 @@ fileprivate struct DurationPicker: View {
var body: some View { var body: some View {
HStack { HStack {
Text(title) Text(title)
Spacer()
TextField("00", text: $duration.hourComponent)
.keyboardType(.decimalPad)
.textFieldStyle(.roundedBorder)
.multilineTextAlignment(.trailing)
.frame(maxWidth: 40)
Text(":")
TextField("00", text: $duration.minuteComponent)
.keyboardType(.decimalPad)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 40)
} }
.frame(maxHeight: 40) .frame(maxHeight: 40)
.clipped() .clipped()

View File

@@ -102,20 +102,34 @@ struct RecipeView: View {
.navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "") .navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "")
.toolbar { .toolbar {
if viewModel.editMode { if viewModel.editMode {
// Cancel Button
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Button("Cancel") { Button("Cancel") {
viewModel.editMode = false viewModel.editMode = false
} }
} }
// Upload Button
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Button { Button {
// TODO: POST edited recipe // TODO: POST edited recipe
Task {
if viewModel.newRecipe { if viewModel.newRecipe {
if let res = await uploadNewRecipe() {
viewModel.alertType = res
viewModel.presentAlert = true
} else {
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
}
} else {
if let res = await uploadEditedRecipe() {
viewModel.alertType = res
viewModel.presentAlert = true
} else { } else {
viewModel.editMode = false viewModel.editMode = false
} }
}
}
} label: { } label: {
if viewModel.newRecipe { if viewModel.newRecipe {
Text("Upload Recipe") Text("Upload Recipe")
@@ -124,6 +138,8 @@ struct RecipeView: View {
} }
} }
} }
// Delete Button
if !viewModel.newRecipe { if !viewModel.newRecipe {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Menu { Menu {

View File

@@ -12,21 +12,43 @@ import SwiftUI
struct RecipeDurationSection: View { struct RecipeDurationSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel @ObservedObject var viewModel: RecipeView.ViewModel
@State var presentPopover: Bool = false
var body: some View { var body: some View {
if viewModel.editMode { if !viewModel.editMode {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) {
EditableDurationView(time: viewModel.observableRecipeDetail.prepTime, title: LocalizedStringKey("Preparation"))
EditableDurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking"))
EditableDurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time"))
}
} else {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) { LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) {
DurationView(time: viewModel.observableRecipeDetail.prepTime, title: LocalizedStringKey("Preparation")) DurationView(time: viewModel.observableRecipeDetail.prepTime, title: LocalizedStringKey("Preparation"))
DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking")) DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking"))
DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time")) DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time"))
} }
} else {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) {
Button {
presentPopover.toggle()
} label: {
DurationView(time: viewModel.observableRecipeDetail.prepTime, title: LocalizedStringKey("Preparation"))
} }
Button {
presentPopover.toggle()
} label: {
DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking"))
}
Button {
presentPopover.toggle()
} label: {
DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time"))
}
}
.popover(isPresented: $presentPopover) {
EditableDurationView(
prepTime: viewModel.observableRecipeDetail.prepTime,
cookTime: viewModel.observableRecipeDetail.cookTime,
totalTime: viewModel.observableRecipeDetail.totalTime
)
}
}
} }
} }
@@ -52,48 +74,30 @@ fileprivate struct DurationView: View {
} }
fileprivate struct EditableDurationView: View { fileprivate struct EditableDurationView: View {
@ObservedObject var time: DurationComponents @ObservedObject var prepTime: DurationComponents
@State var title: LocalizedStringKey @ObservedObject var cookTime: DurationComponents
@State var presentPopoverView: Bool = false @ObservedObject var totalTime: DurationComponents
@State var hour: Int = 0
@State var minute: Int = 0
var body: some View { var body: some View {
ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { HStack {
SecondaryLabel(text: title) SecondaryLabel(text: "Preparation")
Spacer() Spacer()
} }
Button { TimePickerView(selectedHour: $prepTime.hourComponent, selectedMinute: $prepTime.minuteComponent)
presentPopoverView.toggle() SecondaryLabel(text: "Cooking")
} label: { TimePickerView(selectedHour: $cookTime.hourComponent, selectedMinute: $cookTime.minuteComponent)
HStack { SecondaryLabel(text: "Total")
Image(systemName: "clock") TimePickerView(selectedHour: $totalTime.hourComponent, selectedMinute: $totalTime.minuteComponent)
.foregroundStyle(.secondary)
Text(time.displayString)
.lineLimit(1)
}
}
} }
.padding() .padding()
.popover(isPresented: $presentPopoverView) {
TimePickerPopoverView(selectedHour: $hour, selectedMinute: $minute)
}
.onChange(of: presentPopoverView) { presentPopover in
if !presentPopover {
time.hourComponent = String(hour)
time.minuteComponent = String(minute)
}
}
.onAppear {
minute = Int(time.minuteComponent) ?? 0
hour = Int(time.hourComponent) ?? 0
} }
} }
} }
fileprivate struct TimePickerPopoverView: View { fileprivate struct TimePickerView: View {
@Binding var selectedHour: Int @Binding var selectedHour: Int
@Binding var selectedMinute: Int @Binding var selectedMinute: Int

View File

@@ -14,14 +14,19 @@ struct RecipeMetadataSection: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@ObservedObject var viewModel: RecipeView.ViewModel @ObservedObject var viewModel: RecipeView.ViewModel
@State var categories: [String] = []
@State var keywords: [RecipeKeyword] = [] @State var keywords: [RecipeKeyword] = []
var categories: [String] {
appState.categories.map({ category in category.name })
}
@State var presentKeywordSheet: Bool = false @State var presentKeywordSheet: Bool = false
@State var presentServingsPopover: Bool = false @State var presentServingsPopover: Bool = false
@State var presentCategoryPopover: Bool = false @State var presentCategoryPopover: Bool = false
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
// Category
//CategoryPickerView(items: $categories, input: $viewModel.observableRecipeDetail.recipeCategory, titleKey: "Category") //CategoryPickerView(items: $categories, input: $viewModel.observableRecipeDetail.recipeCategory, titleKey: "Category")
SecondaryLabel(text: "Category") SecondaryLabel(text: "Category")
HStack { HStack {
@@ -29,13 +34,16 @@ struct RecipeMetadataSection: View {
.lineLimit(1) .lineLimit(1)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
Button { Picker("Choose", selection: $viewModel.observableRecipeDetail.recipeCategory) {
presentCategoryPopover.toggle() Text("").tag("")
} label: { ForEach(categories, id: \.self) { item in
Text("Choose") Text(item)
} }
} }
.pickerStyle(.menu)
}
// Keywords
SecondaryLabel(text: "Keywords") SecondaryLabel(text: "Keywords")
if !viewModel.observableRecipeDetail.keywords.isEmpty { if !viewModel.observableRecipeDetail.keywords.isEmpty {
@@ -54,7 +62,7 @@ struct RecipeMetadataSection: View {
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
} }
// Servings / Yield
VStack(alignment: .leading) { VStack(alignment: .leading) {
SecondaryLabel(text: "Servings") SecondaryLabel(text: "Servings")
Button { Button {
@@ -63,22 +71,16 @@ struct RecipeMetadataSection: View {
Text("\(viewModel.observableRecipeDetail.recipeYield) serving(s)") Text("\(viewModel.observableRecipeDetail.recipeYield) serving(s)")
.lineLimit(1) .lineLimit(1)
} }
.popover(isPresented: $presentServingsPopover) {
PickerPopoverView(value: $viewModel.observableRecipeDetail.recipeYield, items: 0..<99, titleKey: "Servings")
}
} }
} }
.padding() .padding()
.background(Rectangle().foregroundStyle(Color.white.opacity(0.1))) .background(Rectangle().foregroundStyle(Color.white.opacity(0.1)))
.task {
categories = appState.categories.map({ category in category.name })
}
.sheet(isPresented: $presentKeywordSheet) { .sheet(isPresented: $presentKeywordSheet) {
KeywordPickerView(title: "Keywords", searchSuggestions: appState.allKeywords, selection: $viewModel.observableRecipeDetail.keywords) KeywordPickerView(title: "Keywords", searchSuggestions: appState.allKeywords, selection: $viewModel.observableRecipeDetail.keywords)
} }
.popover(isPresented: $presentServingsPopover) {
PickerPopoverView(value: $viewModel.observableRecipeDetail.recipeYield, items: 0..<99, titleKey: "Servings")
}
.popover(isPresented: $presentCategoryPopover) {
PickerPopoverView(value: $viewModel.observableRecipeDetail.recipeCategory, items: categories, titleKey: "Category")
}
} }
} }
@@ -95,7 +97,7 @@ fileprivate struct PickerPopoverView<Item: Hashable & CustomStringConvertible, C
} }
} }
.pickerStyle(WheelPickerStyle()) .pickerStyle(WheelPickerStyle())
.frame(width: 100, height: 150) .frame(width: 150, height: 150)
.clipped() .clipped()
} }
.padding() .padding()