Files
Nextcloud-Cookbook-iOS/Nextcloud Cookbook iOS Client/Views/Recipes/AllRecipesCategoryCardView.swift
Hendrik Hogertz 6824dbea6b Fix settings page dismissing immediately by replacing multiple isPresented navigation destinations with value-based NavigationPath
The settings view was being popped immediately after push because multiple
navigationDestination(isPresented:) modifiers on the same view caused SwiftUI
to reset bindings when appState published changes. Replaced with a single
navigationDestination(for: SidebarDestination.self) using an explicit
NavigationStack(path:). Also fixed @ObservedObject -> @StateObject on
SettingsView.ViewModel, added AllRecipesListView/AllRecipesCategoryCardView,
and added translations for new strings.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 02:20:38 +01:00

153 lines
5.1 KiB
Swift

//
// AllRecipesCategoryCardView.swift
// Nextcloud Cookbook iOS Client
//
import SwiftUI
struct AllRecipesCategoryCardView: View {
@EnvironmentObject var appState: AppState
@State private var mosaicImages: [UIImage] = []
private var totalRecipeCount: Int {
appState.categories.reduce(0) { $0 + $1.recipe_count }
}
var body: some View {
ZStack(alignment: .bottomLeading) {
// 2x2 image mosaic or gradient fallback
if mosaicImages.count >= 4 {
mosaicGrid
} else {
gradientFallback
}
// Bottom scrim with text
VStack(alignment: .leading, spacing: 2) {
Spacer()
LinearGradient(
colors: [.clear, .black.opacity(0.95)],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 60)
.overlay(alignment: .bottomLeading) {
VStack(alignment: .leading, spacing: 2) {
Text("All Recipes")
.font(.system(size: 16, weight: .medium))
.foregroundStyle(.white)
.lineLimit(1)
Text("\(totalRecipeCount) recipes")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.white.opacity(0.85))
}
.padding(.horizontal, 10)
.padding(.bottom, 8)
}
}
}
.frame(height: 140)
.clipShape(RoundedRectangle(cornerRadius: 17))
.shadow(color: .black.opacity(0.1), radius: 4, y: 2)
.task {
await loadMosaicImages()
}
}
private func loadMosaicImages() async {
// Ensure recipes are loaded for each category (they may not be yet)
for category in appState.categories {
if appState.recipes[category.name] == nil || appState.recipes[category.name]!.isEmpty {
await appState.getCategory(named: category.name, fetchMode: .preferLocal)
}
}
// Collect all recipes across categories, shuffled for variety
var allRecipes: [Recipe] = []
for category in appState.categories {
if let recipes = appState.recipes[category.name] {
allRecipes.append(contentsOf: recipes)
}
}
allRecipes.shuffle()
// Filter to recipes that have an image URL, then pick 4
var candidates: [Recipe] = []
var seenIds: Set<Int> = []
for recipe in allRecipes {
guard let url = recipe.imageUrl, !url.isEmpty else { continue }
guard !seenIds.contains(recipe.recipe_id) else { continue }
seenIds.insert(recipe.recipe_id)
candidates.append(recipe)
if candidates.count >= 4 { break }
}
var images: [UIImage] = []
for recipe in candidates {
if let image = await appState.getImage(
id: recipe.recipe_id,
size: .THUMB,
fetchMode: UserSettings.shared.storeThumb ? .preferLocal : .onlyServer
) {
images.append(image)
}
}
guard !images.isEmpty else { return }
// Cycle to fill 4 slots if fewer than 4 unique images
var filled: [UIImage] = []
for i in 0..<4 {
filled.append(images[i % images.count])
}
mosaicImages = filled
}
private var mosaicGrid: some View {
VStack(spacing: 1) {
HStack(spacing: 1) {
imageCell(mosaicImages[safe: 0])
imageCell(mosaicImages[safe: 1])
}
HStack(spacing: 1) {
imageCell(mosaicImages[safe: 2])
imageCell(mosaicImages[safe: 3])
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 140, maxHeight: 140)
.clipped()
}
private func imageCell(_ image: UIImage?) -> some View {
Group {
if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.clipped()
} else {
Color.gray
}
}
}
private var gradientFallback: some View {
LinearGradient(
gradient: Gradient(colors: [.ncGradientDark, .ncGradientLight]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 140, maxHeight: 140)
.overlay(alignment: .center) {
Image(systemName: "square.grid.2x2.fill")
.font(.system(size: 36))
.foregroundStyle(.white.opacity(0.5))
}
}
}
private extension Array {
subscript(safe index: Int) -> Element? {
indices.contains(index) ? self[index] : nil
}
}