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>
225 lines
8.6 KiB
Swift
225 lines
8.6 KiB
Swift
//
|
|
// RecipeTabView.swift
|
|
// Nextcloud Cookbook iOS Client
|
|
//
|
|
// Created by Vincent Meilinger on 23.01.24.
|
|
//
|
|
|
|
import Foundation
|
|
import OSLog
|
|
import SwiftUI
|
|
|
|
|
|
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 {
|
|
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
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
}
|
|
.padding(.vertical)
|
|
}
|
|
.navigationTitle("Recipes")
|
|
.toolbar {
|
|
RecipeTabViewToolBar()
|
|
}
|
|
.navigationDestination(isPresented: $viewModel.presentSettingsView) {
|
|
SettingsView()
|
|
.environmentObject(appState)
|
|
}
|
|
.navigationDestination(isPresented: $viewModel.presentEditView) {
|
|
RecipeView(viewModel: RecipeView.ViewModel())
|
|
.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 {
|
|
RecipeListView(
|
|
categoryName: category.name,
|
|
showEditView: $viewModel.presentEditView
|
|
)
|
|
.id(category.id)
|
|
}
|
|
}
|
|
}
|
|
.tint(.nextcloudBlue)
|
|
.task {
|
|
let connection = await appState.checkServerConnection()
|
|
DispatchQueue.main.async {
|
|
viewModel.serverConnection = connection
|
|
}
|
|
}
|
|
.refreshable {
|
|
let connection = await appState.checkServerConnection()
|
|
DispatchQueue.main.async {
|
|
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
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fileprivate struct RecipeTabViewToolBar: ToolbarContent {
|
|
@EnvironmentObject var appState: AppState
|
|
@EnvironmentObject var viewModel: RecipeTabView.ViewModel
|
|
|
|
var body: some ToolbarContent {
|
|
// Top left menu toolbar item
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
Menu {
|
|
Button {
|
|
Task {
|
|
viewModel.presentLoadingIndicator = true
|
|
UserSettings.shared.lastUpdate = Date.distantPast
|
|
await appState.getCategories()
|
|
for category in appState.categories {
|
|
await appState.getCategory(named: category.name, fetchMode: .preferServer)
|
|
}
|
|
await appState.updateAllRecipeDetails()
|
|
viewModel.presentLoadingIndicator = false
|
|
}
|
|
} label: {
|
|
Text("Refresh all")
|
|
Image(systemName: "icloud.and.arrow.down")
|
|
}
|
|
|
|
Button {
|
|
viewModel.presentSettingsView = true
|
|
} label: {
|
|
Text("Settings")
|
|
Image(systemName: "gearshape")
|
|
}
|
|
} label: {
|
|
Image(systemName: "ellipsis.circle")
|
|
}
|
|
}
|
|
|
|
// Server connection indicator
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button {
|
|
Logger.view.debug("Check server connection")
|
|
viewModel.presentConnectionPopover = true
|
|
} label: {
|
|
if viewModel.presentLoadingIndicator {
|
|
ProgressView()
|
|
} else if viewModel.serverConnection {
|
|
Image(systemName: "checkmark.icloud")
|
|
} else {
|
|
Image(systemName: "xmark.icloud")
|
|
}
|
|
}.popover(isPresented: $viewModel.presentConnectionPopover) {
|
|
VStack(alignment: .leading) {
|
|
Text(viewModel.serverConnection ? LocalizedStringKey("Connected to server.") : LocalizedStringKey("Unable to connect to server."))
|
|
.bold()
|
|
|
|
Text("Last updated: \(DateFormatter.utcToString(date: UserSettings.shared.lastUpdate))")
|
|
.font(.caption)
|
|
.foregroundStyle(Color.secondary)
|
|
}
|
|
.padding()
|
|
.presentationCompactAdaptation(.popover)
|
|
}
|
|
}
|
|
|
|
// Create new recipes
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button {
|
|
Logger.view.debug("Add new recipe")
|
|
viewModel.presentEditView = true
|
|
} label: {
|
|
Image(systemName: "plus.circle.fill")
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|