Files
Nextcloud-Cookbook-iOS/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeListView.swift
Hendrik Hogertz 1536174586 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>
2026-02-15 03:29:20 +01:00

123 lines
4.6 KiB
Swift

//
// CategoryDetailView.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 15.09.23.
//
import Foundation
import SwiftUI
struct RecipeListView: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var groceryList: GroceryListManager
@State var categoryName: String
@State var searchText: String = ""
var onCreateNew: () -> Void
var onImportFromURL: () -> Void
@State var selectedRecipe: Recipe? = nil
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 in this cookbook")
.font(.headline)
.foregroundStyle(.secondary)
Text("Recipes will appear here once they are added to this category.")
.font(.subheadline)
.foregroundStyle(.tertiary)
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
Button {
Task {
await appState.getCategories()
await appState.getCategory(named: categoryName, fetchMode: .preferServer)
}
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
.bold()
}
.buttonStyle(.bordered)
.tint(.nextcloudBlue)
}.padding()
}
}
.searchable(text: $searchText, prompt: "Search recipes/keywords")
.navigationTitle(categoryName == "*" ? String(localized: "Other") : categoryName)
.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 {
await appState.getCategory(
named: categoryName,
fetchMode: UserSettings.shared.storeRecipes ? .preferLocal : .onlyServer
)
}
.refreshable {
await appState.getCategory(
named: categoryName,
fetchMode: UserSettings.shared.storeRecipes ? .preferServer : .onlyServer
)
}
}
func recipesFiltered() -> [Recipe] {
guard let recipes = appState.recipes[categoryName] else { return [] }
guard searchText != "" else { return recipes }
return recipes.filter { recipe in
recipe.name.lowercased().contains(searchText.lowercased()) || // check name for occurence of search term
(recipe.keywords != nil && recipe.keywords!.lowercased().contains(searchText.lowercased())) // check keywords for search term
}
}
}