// // 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") } } } }