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>
103 lines
3.7 KiB
Swift
103 lines
3.7 KiB
Swift
//
|
|
// AllRecipesListView.swift
|
|
// Nextcloud Cookbook iOS Client
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct AllRecipesListView: View {
|
|
@EnvironmentObject var appState: AppState
|
|
@EnvironmentObject var groceryList: GroceryListManager
|
|
var onCreateNew: () -> Void
|
|
var onImportFromURL: () -> Void
|
|
@State private var allRecipes: [Recipe] = []
|
|
@State private var searchText: String = ""
|
|
|
|
private let gridColumns = [GridItem(.adaptive(minimum: 160), spacing: 12)]
|
|
|
|
var body: some View {
|
|
Group {
|
|
let recipes = recipesFiltered()
|
|
if !recipes.isEmpty {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("\(recipes.count) recipes")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.padding(.horizontal)
|
|
|
|
LazyVGrid(columns: gridColumns, spacing: 12) {
|
|
ForEach(recipes, id: \.recipe_id) { recipe in
|
|
NavigationLink(value: recipe) {
|
|
RecipeCardView(recipe: recipe)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
.padding(.vertical)
|
|
}
|
|
} else {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "fork.knife")
|
|
.font(.system(size: 48))
|
|
.foregroundStyle(.secondary)
|
|
Text("No recipes found")
|
|
.font(.headline)
|
|
.foregroundStyle(.secondary)
|
|
Button {
|
|
Task {
|
|
allRecipes = await appState.getRecipes()
|
|
}
|
|
} label: {
|
|
Label("Refresh", systemImage: "arrow.clockwise")
|
|
.bold()
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.tint(.nextcloudBlue)
|
|
}.padding()
|
|
}
|
|
}
|
|
.searchable(text: $searchText, prompt: "Search recipes/keywords")
|
|
.navigationTitle(String(localized: "All Recipes"))
|
|
.navigationDestination(for: Recipe.self) { recipe in
|
|
RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe))
|
|
.environmentObject(appState)
|
|
.environmentObject(groceryList)
|
|
}
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Menu {
|
|
Button {
|
|
onCreateNew()
|
|
} label: {
|
|
Label("Create New Recipe", systemImage: "square.and.pencil")
|
|
}
|
|
Button {
|
|
onImportFromURL()
|
|
} label: {
|
|
Label("Import from URL", systemImage: "link")
|
|
}
|
|
} label: {
|
|
Image(systemName: "plus.circle.fill")
|
|
}
|
|
}
|
|
}
|
|
.task {
|
|
allRecipes = await appState.getRecipes()
|
|
}
|
|
.refreshable {
|
|
allRecipes = await appState.getRecipes()
|
|
}
|
|
}
|
|
|
|
private func recipesFiltered() -> [Recipe] {
|
|
guard !searchText.isEmpty else { return allRecipes }
|
|
return allRecipes.filter { recipe in
|
|
recipe.name.lowercased().contains(searchText.lowercased()) ||
|
|
(recipe.keywords != nil && recipe.keywords!.lowercased().contains(searchText.lowercased()))
|
|
}
|
|
}
|
|
}
|