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

@@ -12,52 +12,64 @@ struct RecipeCardView: View {
@EnvironmentObject var appState: AppState
@State var recipe: Recipe
@State var recipeThumb: UIImage?
@State var isDownloaded: Bool? = nil
private var keywordsText: String? {
guard let keywords = recipe.keywords, !keywords.isEmpty else { return nil }
let items = keywords.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty }
guard !items.isEmpty else { return nil }
return items.prefix(3).joined(separator: " \u{00B7} ")
}
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 0) {
// Thumbnail
if let recipeThumb = recipeThumb {
Image(uiImage: recipeThumb)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 80, height: 80)
.clipShape(RoundedRectangle(cornerRadius: 17))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 120, maxHeight: 120)
.clipped()
} else {
Image(systemName: "square.text.square")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundStyle(Color.white)
.padding(10)
.background(Color("ncblue"))
.frame(width: 80, height: 80)
.clipShape(RoundedRectangle(cornerRadius: 17))
}
Text(recipe.name)
.font(.headline)
.padding(.leading, 4)
Spacer()
if let isDownloaded = isDownloaded {
VStack {
Image(systemName: isDownloaded ? "checkmark.circle" : "icloud.and.arrow.down")
.foregroundColor(.secondary)
.padding()
Spacer()
LinearGradient(
gradient: Gradient(colors: [.ncGradientDark, .ncGradientLight]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 120, maxHeight: 120)
.overlay {
Image(systemName: "fork.knife")
.font(.title2)
.foregroundStyle(.white.opacity(0.7))
}
}
// Text content
VStack(alignment: .leading, spacing: 3) {
Text(recipe.name)
.font(.subheadline)
.fontWeight(.medium)
.lineLimit(2)
.multilineTextAlignment(.leading)
if let keywordsText {
Text(keywordsText)
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
}
.background(Color.backgroundHighlight)
.clipShape(RoundedRectangle(cornerRadius: 17))
.clipShape(RoundedRectangle(cornerRadius: 14))
.shadow(color: .black.opacity(0.08), radius: 4, y: 2)
.task {
recipeThumb = await appState.getImage(
id: recipe.recipe_id,
size: .THUMB,
fetchMode: UserSettings.shared.storeThumb ? .preferLocal : .onlyServer
)
if recipe.storedLocally == nil {
recipe.storedLocally = appState.recipeDetailExists(recipeId: recipe.recipe_id)
}
isDownloaded = recipe.storedLocally
}
.refreshable {
recipeThumb = await appState.getImage(
@@ -66,6 +78,5 @@ struct RecipeCardView: View {
fetchMode: UserSettings.shared.storeThumb ? .preferServer : .onlyServer
)
}
.frame(height: 80)
}
}