Redesign search tab, add category cards, recent recipes, and complete German translations

Overhaul SearchTabView with search history, empty/no-results states, and dynamic
navigation title. Extract CategoryCardView and RecentRecipesSection into standalone
views. Update RecipeTabView, RecipeListView, RecipeCardView, and MainView for the
modernized UI. Add all 12 missing German translations in Localizable.xcstrings.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 01:47:16 +01:00
parent 7c824b492e
commit c8ddb098d1
11 changed files with 792 additions and 121 deletions

View File

@@ -14,44 +14,66 @@ struct RecipeTabView: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var groceryList: GroceryList
@EnvironmentObject var viewModel: RecipeTabView.ViewModel
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
private let gridColumns = [GridItem(.adaptive(minimum: 160), spacing: 12)]
var body: some View {
NavigationSplitView {
List(selection: $viewModel.selectedCategory) {
// Categories
ForEach(appState.categories) { category in
NavigationLink(value: category) {
HStack(alignment: .center) {
if viewModel.selectedCategory != nil &&
category.name == viewModel.selectedCategory!.name {
Image(systemName: "book")
} else {
Image(systemName: "book.closed.fill")
}
if category.name == "*" {
Text("Other")
.font(.system(size: 20, weight: .medium, design: .default))
} else {
Text(category.name)
.font(.system(size: 20, weight: .medium, design: .default))
}
Spacer()
Text("\(category.recipe_count)")
.font(.system(size: 15, weight: .bold, design: .default))
.foregroundStyle(Color.background)
.frame(width: 25, height: 25, alignment: .center)
.minimumScaleFactor(0.5)
.background {
Circle()
.foregroundStyle(Color.secondary)
ScrollView {
VStack(alignment: .leading, spacing: 20) {
// Recently Viewed
if !appState.recentRecipes.isEmpty {
RecentRecipesSection()
}
// Categories header
if !appState.categories.isEmpty {
Text("Categories")
.font(.title2)
.bold()
.padding(.horizontal)
}
// Category grid
if appState.categories.isEmpty {
VStack(spacing: 12) {
Image(systemName: "book.closed")
.font(.system(size: 48))
.foregroundStyle(.secondary)
Text("No cookbooks found")
.font(.headline)
.foregroundStyle(.secondary)
Text("Pull to refresh or check your server connection.")
.font(.subheadline)
.foregroundStyle(.tertiary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding(.top, 40)
} else {
LazyVGrid(columns: gridColumns, spacing: 12) {
ForEach(appState.categories) { category in
Button {
viewModel.selectedCategory = category
if horizontalSizeClass == .compact {
viewModel.navigateToCategory = true
}
} label: {
CategoryCardView(
category: category,
isSelected: viewModel.selectedCategory?.name == category.name
)
}
}.padding(7)
.buttonStyle(.plain)
}
}
.padding(.horizontal)
}
}
.padding(.vertical)
}
.navigationTitle("Cookbooks")
.navigationTitle("Recipes")
.toolbar {
RecipeTabViewToolBar()
}
@@ -64,6 +86,22 @@ struct RecipeTabView: View {
.environmentObject(appState)
.environmentObject(groceryList)
}
.navigationDestination(for: Recipe.self) { recipe in
RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe))
.environmentObject(appState)
.environmentObject(groceryList)
}
.navigationDestination(isPresented: $viewModel.navigateToCategory) {
if let category = viewModel.selectedCategory {
RecipeListView(
categoryName: category.name,
showEditView: $viewModel.presentEditView
)
.id(category.id)
.environmentObject(appState)
.environmentObject(groceryList)
}
}
} detail: {
NavigationStack {
if let category = viewModel.selectedCategory {
@@ -71,9 +109,8 @@ struct RecipeTabView: View {
categoryName: category.name,
showEditView: $viewModel.presentEditView
)
.id(category.id) // Workaround: This is needed to update the detail view when the selection changes
.id(category.id)
}
}
}
.tint(.nextcloudBlue)
@@ -89,17 +126,22 @@ struct RecipeTabView: View {
viewModel.serverConnection = connection
}
await appState.getCategories()
for category in appState.categories {
await appState.getCategory(named: category.name, fetchMode: .preferServer)
await appState.getCategoryImage(for: category.name)
}
}
}
class ViewModel: ObservableObject {
@Published var presentEditView: Bool = false
@Published var presentSettingsView: Bool = false
@Published var navigateToCategory: Bool = false
@Published var presentLoadingIndicator: Bool = false
@Published var presentConnectionPopover: Bool = false
@Published var serverConnection: Bool = false
@Published var selectedCategory: Category? = nil
}
}