diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate index deea4fc..01af19e 100644 Binary files a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate and b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Nextcloud Cookbook iOS Client/Localizable.xcstrings b/Nextcloud Cookbook iOS Client/Localizable.xcstrings index bfd241a..b7e583f 100644 --- a/Nextcloud Cookbook iOS Client/Localizable.xcstrings +++ b/Nextcloud Cookbook iOS Client/Localizable.xcstrings @@ -46,6 +46,7 @@ } }, ":" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -273,6 +274,7 @@ } }, "00" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -3069,6 +3071,9 @@ } } } + }, + "Total" : { + }, "Total duration:" : { "localizations" : { diff --git a/Nextcloud Cookbook iOS Client/Util/DurationComponents.swift b/Nextcloud Cookbook iOS Client/Util/DurationComponents.swift index d991708..3eb2fc5 100644 --- a/Nextcloud Cookbook iOS Client/Util/DurationComponents.swift +++ b/Nextcloud Cookbook iOS Client/Util/DurationComponents.swift @@ -10,64 +10,42 @@ import SwiftUI class DurationComponents: ObservableObject { - @Published var secondComponent: String = "00" { + @Published var secondComponent: Int = 0 { didSet { - if secondComponent.count > 2 { - secondComponent = oldValue - } else if secondComponent.count == 1 { - secondComponent = "0\(secondComponent)" - } else if secondComponent.count == 0 { - secondComponent = "00" + if secondComponent > 59 { + secondComponent = 59 + } else if secondComponent < 0 { + secondComponent = 0 } - 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 { - if minuteComponent.count > 2 { - minuteComponent = oldValue - } 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 + if hourComponent < 0 { + hourComponent = 0 } } } - @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 { - let intHour = Int(hourComponent) ?? 0 - let intMinute = Int(minuteComponent) ?? 0 - - if intHour != 0 && intMinute != 0 { - return "\(intHour) h \(intMinute) min" - } else if intHour == 0 && intMinute != 0 { - return "\(intMinute) min" - } else if intHour != 0 && intMinute == 0 { - return "\(intHour) h" + if hourComponent != 0 && minuteComponent != 0 { + return "\(hourComponent) h \(minuteComponent) min" + } else if hourComponent == 0 && minuteComponent != 0 { + return "\(minuteComponent) min" + } else if hourComponent != 0 && minuteComponent == 0 { + return "\(hourComponent) h" } else { return "-" } @@ -78,10 +56,10 @@ class DurationComponents: ObservableObject { let hourRegex = /([0-9]{1,2})H/ let minuteRegex = /([0-9]{1,2})M/ 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) { - duration.minuteComponent = String(match.1) + duration.minuteComponent = Int(match.1) ?? 0 } return duration } @@ -90,41 +68,47 @@ class DurationComponents: ObservableObject { let hourRegex = /([0-9]{1,2})H/ let minuteRegex = /([0-9]{1,2})M/ 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) { - 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 { - return "PT\(hourComponent)H\(minuteComponent)M00S" + let (hr, min, sec) = stringFormatComponents() + return "PT\(hr)H\(min)M\(sec)S" } func toTimerText() -> String { var timeString = "" - if hourComponent != "00" { - timeString.append("\(hourComponent):") + let (hr, min, sec) = stringFormatComponents() + if hourComponent != 0 { + timeString.append("\(hr):") } - timeString.append("\(minuteComponent):") - timeString.append("\(secondComponent)") + timeString.append("\(min):") + timeString.append(sec) return timeString } func toSeconds() -> Double { - guard let hours = Double(hourComponent) else { return 0 } - guard let minutes = Double(minuteComponent) else { return 0 } - guard let seconds = Double(secondComponent) else { return 0 } - return hours * 3600 + minutes * 60 + seconds + return Double(hourComponent) * 3600 + Double(minuteComponent) * 60 + Double(secondComponent) } func fromSeconds(_ totalSeconds: Int) { let hours = totalSeconds / 3600 let minutes = (totalSeconds % 3600) / 60 let seconds = totalSeconds % 60 - self.hourComponent = String(hours) - self.minuteComponent = String(minutes) - self.secondComponent = String(seconds) + self.hourComponent = Int(hours) + self.minuteComponent = Int(minutes) + self.secondComponent = Int(seconds) } static func ptToText(_ ptString: String) -> String? { diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeEditing/RecipeEditView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeEditing/RecipeEditView.swift index d91c6e2..8698e76 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeEditing/RecipeEditView.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeEditing/RecipeEditView.swift @@ -267,17 +267,7 @@ fileprivate struct DurationPicker: View { var body: some View { HStack { 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) .clipped() diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift index 6225e86..532b550 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift @@ -102,19 +102,33 @@ struct RecipeView: View { .navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "") .toolbar { if viewModel.editMode { + // Cancel Button ToolbarItem(placement: .topBarLeading) { Button("Cancel") { viewModel.editMode = false } } + // Upload Button ToolbarItem(placement: .topBarTrailing) { Button { // TODO: POST edited recipe - if viewModel.newRecipe { - presentationMode.wrappedValue.dismiss() - } else { - viewModel.editMode = false + Task { + if viewModel.newRecipe { + if let res = await uploadNewRecipe() { + viewModel.alertType = res + viewModel.presentAlert = true + } else { + presentationMode.wrappedValue.dismiss() + } + } else { + if let res = await uploadEditedRecipe() { + viewModel.alertType = res + viewModel.presentAlert = true + } else { + viewModel.editMode = false + } + } } } label: { if viewModel.newRecipe { @@ -124,6 +138,8 @@ struct RecipeView: View { } } } + + // Delete Button if !viewModel.newRecipe { ToolbarItem(placement: .topBarTrailing) { Menu { diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeDurationSection.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeDurationSection.swift index 30275f2..48172d8 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeDurationSection.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeDurationSection.swift @@ -12,21 +12,43 @@ import SwiftUI struct RecipeDurationSection: View { @ObservedObject var viewModel: RecipeView.ViewModel + @State var presentPopover: Bool = false var body: some View { - 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 { + if !viewModel.editMode { LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) { DurationView(time: viewModel.observableRecipeDetail.prepTime, title: LocalizedStringKey("Preparation")) DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking")) 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 { - @ObservedObject var time: DurationComponents - @State var title: LocalizedStringKey - @State var presentPopoverView: Bool = false - @State var hour: Int = 0 - @State var minute: Int = 0 + @ObservedObject var prepTime: DurationComponents + @ObservedObject var cookTime: DurationComponents + @ObservedObject var totalTime: DurationComponents var body: some View { - VStack(alignment: .leading) { - HStack { - SecondaryLabel(text: title) - Spacer() - } - Button { - presentPopoverView.toggle() - } label: { + ScrollView { + VStack(alignment: .leading) { HStack { - Image(systemName: "clock") - .foregroundStyle(.secondary) - Text(time.displayString) - .lineLimit(1) + SecondaryLabel(text: "Preparation") + Spacer() } + TimePickerView(selectedHour: $prepTime.hourComponent, selectedMinute: $prepTime.minuteComponent) + SecondaryLabel(text: "Cooking") + TimePickerView(selectedHour: $cookTime.hourComponent, selectedMinute: $cookTime.minuteComponent) + SecondaryLabel(text: "Total") + TimePickerView(selectedHour: $totalTime.hourComponent, selectedMinute: $totalTime.minuteComponent) } - } - .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 + .padding() } } } -fileprivate struct TimePickerPopoverView: View { +fileprivate struct TimePickerView: View { @Binding var selectedHour: Int @Binding var selectedMinute: Int diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift index ac883a7..a2bad4e 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeMetadataSection.swift @@ -14,14 +14,19 @@ struct RecipeMetadataSection: View { @EnvironmentObject var appState: AppState @ObservedObject var viewModel: RecipeView.ViewModel - @State var categories: [String] = [] @State var keywords: [RecipeKeyword] = [] + var categories: [String] { + appState.categories.map({ category in category.name }) + } + + @State var presentKeywordSheet: Bool = false @State var presentServingsPopover: Bool = false @State var presentCategoryPopover: Bool = false var body: some View { VStack(alignment: .leading) { + // Category //CategoryPickerView(items: $categories, input: $viewModel.observableRecipeDetail.recipeCategory, titleKey: "Category") SecondaryLabel(text: "Category") HStack { @@ -29,13 +34,16 @@ struct RecipeMetadataSection: View { .lineLimit(1) .textFieldStyle(.roundedBorder) - Button { - presentCategoryPopover.toggle() - } label: { - Text("Choose") + Picker("Choose", selection: $viewModel.observableRecipeDetail.recipeCategory) { + Text("").tag("") + ForEach(categories, id: \.self) { item in + Text(item) + } } + .pickerStyle(.menu) } + // Keywords SecondaryLabel(text: "Keywords") if !viewModel.observableRecipeDetail.keywords.isEmpty { @@ -54,7 +62,7 @@ struct RecipeMetadataSection: View { Image(systemName: "chevron.right") } - + // Servings / Yield VStack(alignment: .leading) { SecondaryLabel(text: "Servings") Button { @@ -63,22 +71,16 @@ struct RecipeMetadataSection: View { Text("\(viewModel.observableRecipeDetail.recipeYield) serving(s)") .lineLimit(1) } + .popover(isPresented: $presentServingsPopover) { + PickerPopoverView(value: $viewModel.observableRecipeDetail.recipeYield, items: 0..<99, titleKey: "Servings") + } } } .padding() .background(Rectangle().foregroundStyle(Color.white.opacity(0.1))) - .task { - categories = appState.categories.map({ category in category.name }) - } .sheet(isPresented: $presentKeywordSheet) { 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