Replace the single "+" button with a 2-option menu (Create New Recipe / Import from URL) across RecipeTabView, RecipeListView, and AllRecipesListView. Add ImportURLSheet for server-side recipe import with loading and error states. Completely redesign edit mode to use a native Form layout with inline editing for all sections (metadata, duration, ingredients, instructions, tools, nutrition) instead of the previous sheet-based EditableListView approach. Move delete action from edit toolbar to view mode context menu. Add recipe image display to the edit form. Refactor RecipeListView and AllRecipesListView to use closure-based callbacks instead of Binding<Bool> for the create/import actions. Add preloadedRecipeDetail support to RecipeView.ViewModel for imported recipes. Add DE/ES/FR translations for all new UI strings. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
105 lines
3.6 KiB
Swift
105 lines
3.6 KiB
Swift
//
|
|
// RecipeNutritionSection.swift
|
|
// Nextcloud Cookbook iOS Client
|
|
//
|
|
// Created by Vincent Meilinger on 01.03.24.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
|
|
// MARK: - RecipeView Nutrition Section
|
|
|
|
struct RecipeNutritionSection: View {
|
|
@ObservedObject var viewModel: RecipeView.ViewModel
|
|
|
|
var body: some View {
|
|
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandNutritionSection) {
|
|
VStack(alignment: .leading) {
|
|
if viewModel.editMode {
|
|
ForEach(Nutrition.allCases, id: \.self) { nutrition in
|
|
HStack {
|
|
Text(nutrition.localizedDescription)
|
|
TextField("", text: binding(for: nutrition.dictKey), axis: .horizontal)
|
|
.textFieldStyle(.roundedBorder)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
} else if !nutritionEmpty() {
|
|
VStack(alignment: .leading) {
|
|
ForEach(Nutrition.allCases, id: \.self) { nutrition in
|
|
if let value = viewModel.observableRecipeDetail.nutrition[nutrition.dictKey], nutrition.dictKey != Nutrition.servingSize.dictKey {
|
|
HStack(alignment: .top) {
|
|
Text("\(nutrition.localizedDescription): \(value)")
|
|
.multilineTextAlignment(.leading)
|
|
}
|
|
.padding(4)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Text(LocalizedStringKey("No nutritional information."))
|
|
}
|
|
}
|
|
} title: {
|
|
HStack {
|
|
if let servingSize = viewModel.observableRecipeDetail.nutrition["servingSize"] {
|
|
SecondaryLabel(text: "Nutrition (\(servingSize))")
|
|
} else {
|
|
SecondaryLabel(text: LocalizedStringKey("Nutrition"))
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
|
|
func binding(for key: String) -> Binding<String> {
|
|
Binding(
|
|
get: { viewModel.observableRecipeDetail.nutrition[key, default: ""] },
|
|
set: { viewModel.observableRecipeDetail.nutrition[key] = $0 }
|
|
)
|
|
}
|
|
|
|
func nutritionEmpty() -> Bool {
|
|
for nutrition in Nutrition.allCases {
|
|
if let _ = viewModel.observableRecipeDetail.nutrition[nutrition.dictKey] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
// MARK: - Recipe Edit Nutrition Section (Form-based)
|
|
|
|
struct RecipeEditNutritionSection: View {
|
|
@Binding var nutrition: [String: String]
|
|
|
|
@State private var isExpanded: Bool = false
|
|
|
|
var body: some View {
|
|
Section {
|
|
DisclosureGroup("Nutrition Information", isExpanded: $isExpanded) {
|
|
ForEach(Nutrition.allCases, id: \.self) { item in
|
|
HStack {
|
|
Text(item.localizedDescription)
|
|
.lineLimit(1)
|
|
Spacer()
|
|
TextField("", text: nutritionBinding(for: item.dictKey))
|
|
.multilineTextAlignment(.trailing)
|
|
.frame(maxWidth: 150)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func nutritionBinding(for key: String) -> Binding<String> {
|
|
Binding(
|
|
get: { nutrition[key, default: ""] },
|
|
set: { nutrition[key] = $0 }
|
|
)
|
|
}
|
|
}
|