Redesign recipe creation and edit view with Form-based layout and URL import

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>
This commit is contained in:
2026-02-15 03:29:20 +01:00
parent 98c82dc537
commit 1536174586
13 changed files with 1085 additions and 444 deletions

View File

@@ -21,23 +21,70 @@ struct RecipeDurationSection: View {
DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking"))
DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time"))
}
if viewModel.editMode {
Button {
presentPopover.toggle()
} label: {
Text("Edit")
}
.buttonStyle(.borderedProminent)
.padding(.top, 5)
}
}
.padding()
.popover(isPresented: $presentPopover) {
EditableDurationView(
prepTime: viewModel.observableRecipeDetail.prepTime,
cookTime: viewModel.observableRecipeDetail.cookTime,
totalTime: viewModel.observableRecipeDetail.totalTime
)
}
}
// 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()
}
}
}
}