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

@@ -18,13 +18,6 @@ struct RecipeTabView: View {
private let gridColumns = [GridItem(.adaptive(minimum: 160), spacing: 12)]
private var showEditViewBinding: Binding<Bool> {
Binding(
get: { false },
set: { if $0 { viewModel.navigateToNewRecipe() } }
)
}
private var nonEmptyCategories: [Category] {
appState.categories.filter { $0.recipe_count > 0 }
}
@@ -112,21 +105,34 @@ struct RecipeTabView: View {
.environmentObject(appState)
.environmentObject(groceryList)
case .newRecipe:
RecipeView(viewModel: RecipeView.ViewModel())
.environmentObject(appState)
.environmentObject(groceryList)
RecipeView(viewModel: {
let vm = RecipeView.ViewModel()
if let imported = viewModel.importedRecipeDetail {
vm.preloadedRecipeDetail = imported
}
return vm
}())
.environmentObject(appState)
.environmentObject(groceryList)
.onAppear {
viewModel.importedRecipeDetail = nil
}
case .category(let category):
RecipeListView(
categoryName: category.name,
showEditView: showEditViewBinding
onCreateNew: { viewModel.navigateToNewRecipe() },
onImportFromURL: { viewModel.showImportURLSheet = true }
)
.id(category.id)
.environmentObject(appState)
.environmentObject(groceryList)
case .allRecipes:
AllRecipesListView(showEditView: showEditViewBinding)
.environmentObject(appState)
.environmentObject(groceryList)
AllRecipesListView(
onCreateNew: { viewModel.navigateToNewRecipe() },
onImportFromURL: { viewModel.showImportURLSheet = true }
)
.environmentObject(appState)
.environmentObject(groceryList)
}
}
.navigationDestination(for: Recipe.self) { recipe in
@@ -138,17 +144,27 @@ struct RecipeTabView: View {
} detail: {
NavigationStack {
if viewModel.showAllRecipesInDetail {
AllRecipesListView(showEditView: showEditViewBinding)
AllRecipesListView(
onCreateNew: { viewModel.navigateToNewRecipe() },
onImportFromURL: { viewModel.showImportURLSheet = true }
)
} else if let category = viewModel.selectedCategory {
RecipeListView(
categoryName: category.name,
showEditView: showEditViewBinding
onCreateNew: { viewModel.navigateToNewRecipe() },
onImportFromURL: { viewModel.showImportURLSheet = true }
)
.id(category.id)
}
}
}
.tint(.nextcloudBlue)
.sheet(isPresented: $viewModel.showImportURLSheet) {
ImportURLSheet { recipeDetail in
viewModel.navigateToImportedRecipe(recipeDetail: recipeDetail)
}
.environmentObject(appState)
}
.task {
let connection = await appState.checkServerConnection()
DispatchQueue.main.async {
@@ -185,6 +201,9 @@ struct RecipeTabView: View {
@Published var selectedCategory: Category? = nil
@Published var showAllRecipesInDetail: Bool = false
@Published var showImportURLSheet: Bool = false
@Published var importedRecipeDetail: RecipeDetail? = nil
func navigateToSettings() {
sidebarPath.append(SidebarDestination.settings)
}
@@ -193,6 +212,11 @@ struct RecipeTabView: View {
sidebarPath.append(SidebarDestination.newRecipe)
}
func navigateToImportedRecipe(recipeDetail: RecipeDetail) {
importedRecipeDetail = recipeDetail
sidebarPath.append(SidebarDestination.newRecipe)
}
func navigateToCategory(_ category: Category) {
selectedCategory = category
showAllRecipesInDetail = false
@@ -273,9 +297,18 @@ fileprivate struct RecipeTabViewToolBar: ToolbarContent {
// Create new recipes
ToolbarItem(placement: .topBarTrailing) {
Button {
Logger.view.debug("Add new recipe")
viewModel.navigateToNewRecipe()
Menu {
Button {
Logger.view.debug("Add new recipe")
viewModel.navigateToNewRecipe()
} label: {
Label("Create New Recipe", systemImage: "square.and.pencil")
}
Button {
viewModel.showImportURLSheet = true
} label: {
Label("Import from URL", systemImage: "link")
}
} label: {
Image(systemName: "plus.circle.fill")
}