Replace the single "+" button with a 2-option menu (Create New Recipe / Import from URL) across RecipeTabView, RecipeListView, and AllRecipesListView. Add ImportURLSheet for server-side recipe import with loading and error states. Completely redesign edit mode to use a native Form layout with inline editing for all sections (metadata, duration, ingredients, instructions, tools, nutrition) instead of the previous sheet-based EditableListView approach. Move delete action from edit toolbar to view mode context menu. Add recipe image display to the edit form. Refactor RecipeListView and AllRecipesListView to use closure-based callbacks instead of Binding<Bool> for the create/import actions. Add preloadedRecipeDetail support to RecipeView.ViewModel for imported recipes. Add DE/ES/FR translations for all new UI strings. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
192 lines
6.4 KiB
Swift
192 lines
6.4 KiB
Swift
//
|
|
// RecipeDurationSection.swift
|
|
// Nextcloud Cookbook iOS Client
|
|
//
|
|
// Created by Vincent Meilinger on 01.03.24.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
|
|
// MARK: - RecipeView Duration Section
|
|
|
|
struct RecipeDurationSection: View {
|
|
@ObservedObject var viewModel: RecipeView.ViewModel
|
|
@State var presentPopover: Bool = false
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading) {
|
|
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"))
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
|
|
// MARK: - Recipe Edit Duration Section (Form-based)
|
|
|
|
struct RecipeEditDurationSection: View {
|
|
@ObservedObject var prepTime: DurationComponents
|
|
@ObservedObject var cookTime: DurationComponents
|
|
@ObservedObject var totalTime: DurationComponents
|
|
|
|
var body: some View {
|
|
Section("Duration") {
|
|
DurationPickerRow(label: "Preparation", time: prepTime)
|
|
DurationPickerRow(label: "Cooking", time: cookTime)
|
|
DurationPickerRow(label: "Total time", time: totalTime)
|
|
}
|
|
.onChange(of: prepTime.hourComponent) { _ in updateTotalTime() }
|
|
.onChange(of: prepTime.minuteComponent) { _ in updateTotalTime() }
|
|
.onChange(of: cookTime.hourComponent) { _ in updateTotalTime() }
|
|
.onChange(of: cookTime.minuteComponent) { _ in updateTotalTime() }
|
|
}
|
|
|
|
private func updateTotalTime() {
|
|
var hourComponent = prepTime.hourComponent + cookTime.hourComponent
|
|
var minuteComponent = prepTime.minuteComponent + cookTime.minuteComponent
|
|
if minuteComponent >= 60 {
|
|
hourComponent += minuteComponent / 60
|
|
minuteComponent %= 60
|
|
}
|
|
totalTime.hourComponent = hourComponent
|
|
totalTime.minuteComponent = minuteComponent
|
|
}
|
|
}
|
|
|
|
fileprivate struct DurationPickerRow: View {
|
|
let label: LocalizedStringKey
|
|
@ObservedObject var time: DurationComponents
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Text(label)
|
|
Spacer()
|
|
Menu {
|
|
ForEach(0..<25, id: \.self) { hour in
|
|
Button("\(hour) h") {
|
|
time.hourComponent = hour
|
|
}
|
|
}
|
|
} label: {
|
|
Text("\(time.hourComponent) h")
|
|
.monospacedDigit()
|
|
}
|
|
Menu {
|
|
ForEach(0..<60, id: \.self) { minute in
|
|
Button("\(minute) min") {
|
|
time.minuteComponent = minute
|
|
}
|
|
}
|
|
} label: {
|
|
Text("\(time.minuteComponent) min")
|
|
.monospacedDigit()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate struct DurationView: View {
|
|
@ObservedObject var time: DurationComponents
|
|
@State var title: LocalizedStringKey
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading) {
|
|
HStack {
|
|
SecondaryLabel(text: title)
|
|
Spacer()
|
|
}
|
|
HStack {
|
|
Image(systemName: "clock")
|
|
.bold()
|
|
.foregroundStyle(.secondary)
|
|
Text(time.displayString)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate struct EditableDurationView: View {
|
|
@Environment(\.presentationMode) var presentationMode
|
|
@ObservedObject var prepTime: DurationComponents
|
|
@ObservedObject var cookTime: DurationComponents
|
|
@ObservedObject var totalTime: DurationComponents
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .center) {
|
|
HStack {
|
|
SecondaryLabel(text: "Preparation")
|
|
Spacer()
|
|
Button("Done") {
|
|
presentationMode.wrappedValue.dismiss()
|
|
}
|
|
}
|
|
TimePickerView(selectedHour: $prepTime.hourComponent, selectedMinute: $prepTime.minuteComponent)
|
|
|
|
HStack {
|
|
SecondaryLabel(text: "Cooking")
|
|
Spacer()
|
|
}
|
|
TimePickerView(selectedHour: $cookTime.hourComponent, selectedMinute: $cookTime.minuteComponent)
|
|
|
|
HStack {
|
|
SecondaryLabel(text: "Total time")
|
|
Spacer()
|
|
}
|
|
TimePickerView(selectedHour: $totalTime.hourComponent, selectedMinute: $totalTime.minuteComponent)
|
|
}
|
|
.padding()
|
|
.onChange(of: prepTime.hourComponent) { _ in updateTotalTime() }
|
|
.onChange(of: prepTime.minuteComponent) { _ in updateTotalTime() }
|
|
.onChange(of: cookTime.hourComponent) { _ in updateTotalTime() }
|
|
.onChange(of: cookTime.minuteComponent) { _ in updateTotalTime() }
|
|
}
|
|
}
|
|
|
|
private func updateTotalTime() {
|
|
var hourComponent = prepTime.hourComponent + cookTime.hourComponent
|
|
var minuteComponent = prepTime.minuteComponent + cookTime.minuteComponent
|
|
// Handle potential overflow from minutes to hours
|
|
if minuteComponent >= 60 {
|
|
hourComponent += minuteComponent / 60
|
|
minuteComponent %= 60
|
|
}
|
|
totalTime.hourComponent = hourComponent
|
|
totalTime.minuteComponent = minuteComponent
|
|
}
|
|
}
|
|
|
|
|
|
fileprivate struct TimePickerView: View {
|
|
@Binding var selectedHour: Int
|
|
@Binding var selectedMinute: Int
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Picker(selection: $selectedHour, label: Text("Hours")) {
|
|
ForEach(0..<99, id: \.self) { hour in
|
|
Text("\(hour) h").tag(hour)
|
|
}
|
|
}
|
|
.pickerStyle(WheelPickerStyle())
|
|
.frame(width: 100, height: 150)
|
|
.clipped()
|
|
|
|
Picker(selection: $selectedMinute, label: Text("Minutes")) {
|
|
ForEach(0..<60, id: \.self) { minute in
|
|
Text("\(minute) min").tag(minute)
|
|
}
|
|
}
|
|
.pickerStyle(WheelPickerStyle())
|
|
.frame(width: 100, height: 150)
|
|
.clipped()
|
|
}
|
|
.padding()
|
|
}
|
|
}
|