293 lines
11 KiB
Swift
293 lines
11 KiB
Swift
//
|
|
// RecipeIngredientSection.swift
|
|
// Nextcloud Cookbook iOS Client
|
|
//
|
|
// Created by Vincent Meilinger on 01.03.24.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
// MARK: - RecipeView Ingredients Section
|
|
|
|
struct RecipeIngredientSection: View {
|
|
@Environment(\.modelContext) var modelContext
|
|
@Bindable var recipe: Recipe
|
|
@Binding var editMode: Bool
|
|
@Binding var presentIngredientEditView: Bool
|
|
@State var recipeGroceries: RecipeGroceries? = nil
|
|
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading) {
|
|
HStack {
|
|
Button {
|
|
withAnimation {
|
|
/*
|
|
if cookbookState.groceryList.containsRecipe(viewModel.recipe.id) {
|
|
cookbookState.groceryList.deleteGroceryRecipe(viewModel.recipe.id)
|
|
} else {
|
|
cookbookState.groceryList.addItems(
|
|
viewModel.recipe.recipeIngredient,
|
|
toRecipe: viewModel.recipe.id,
|
|
recipeName: viewModel.recipe.name
|
|
)
|
|
}
|
|
*/
|
|
}
|
|
} label: {
|
|
if #available(iOS 17.0, *) {
|
|
Image(systemName: "storefront")
|
|
} else {
|
|
Image(systemName: "heart.text.square")
|
|
}
|
|
}.disabled(editMode)
|
|
|
|
SecondaryLabel(text: LocalizedStringKey("Ingredients"))
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "person.2")
|
|
.foregroundStyle(.secondary)
|
|
.bold()
|
|
|
|
ServingPickerView(selectedServingSize: $recipe.ingredientMultiplier)
|
|
}
|
|
|
|
|
|
|
|
ForEach(0..<recipe.ingredients.count, id: \.self) { ix in
|
|
/*IngredientListItem(
|
|
ingredient: $recipe.recipeIngredient[ix],
|
|
servings: $recipe.ingredientMultiplier,
|
|
recipeYield: Double(recipe.recipeYield),
|
|
recipeId: recipe.id
|
|
) {
|
|
/*
|
|
cookbookState.groceryList.addItem(
|
|
recipe.recipeIngredient[ix],
|
|
toRecipe: recipe.id,
|
|
recipeName: recipe.name
|
|
)*/
|
|
}
|
|
.padding(4)*/
|
|
Text(recipe.ingredients[ix])
|
|
}
|
|
|
|
if recipe.ingredientMultiplier != Double(recipe.yield) {
|
|
HStack() {
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
.foregroundStyle(.secondary)
|
|
Text("Marked ingredients could not be adjusted!")
|
|
.foregroundStyle(.secondary)
|
|
}.padding(.top)
|
|
}
|
|
|
|
if editMode {
|
|
Button {
|
|
presentIngredientEditView.toggle()
|
|
} label: {
|
|
Text("Edit")
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
}
|
|
.padding()
|
|
.animation(.easeInOut, value: recipe.ingredientMultiplier)
|
|
}
|
|
|
|
func toggleAllGroceryItems(_ itemNames: [String], inCategory categoryId: String, named name: String) {
|
|
do {
|
|
// Find or create the target category
|
|
let categoryPredicate = #Predicate<RecipeGroceries> { $0.id == categoryId }
|
|
let fetchDescriptor = FetchDescriptor<RecipeGroceries>(predicate: categoryPredicate)
|
|
|
|
if let existingCategory = try modelContext.fetch(fetchDescriptor).first {
|
|
// Delete category if it exists
|
|
modelContext.delete(existingCategory)
|
|
} else {
|
|
// Create the category if it doesn't exist
|
|
let newCategory = RecipeGroceries(id: categoryId, name: name)
|
|
modelContext.insert(newCategory)
|
|
|
|
// Add new GroceryItems to the category
|
|
for itemName in itemNames {
|
|
let newItem = GroceryItem(name: itemName, isChecked: false)
|
|
newCategory.items.append(newItem)
|
|
}
|
|
|
|
try modelContext.save()
|
|
}
|
|
} catch {
|
|
print("Error adding grocery items: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
func toggleGroceryItem(_ itemName: String, inCategory categoryId: String, named name: String) {
|
|
do {
|
|
// Find or create the target category
|
|
let categoryPredicate = #Predicate<RecipeGroceries> { $0.id == categoryId }
|
|
let fetchDescriptor = FetchDescriptor<RecipeGroceries>(predicate: categoryPredicate)
|
|
|
|
if let existingCategory = try modelContext.fetch(fetchDescriptor).first {
|
|
// Delete item if it exists
|
|
if existingCategory.items.contains(where: { $0.name == itemName }) {
|
|
existingCategory.items.removeAll { $0.name == itemName }
|
|
|
|
// Delete category if empty
|
|
if existingCategory.items.isEmpty {
|
|
modelContext.delete(existingCategory)
|
|
}
|
|
} else {
|
|
existingCategory.items.append(GroceryItem(name: itemName, isChecked: false))
|
|
}
|
|
} else {
|
|
// Add the category if it doesn't exist
|
|
let newCategory = RecipeGroceries(id: categoryId, name: name)
|
|
modelContext.insert(newCategory)
|
|
|
|
// Add the item to the new category
|
|
newCategory.items.append(GroceryItem(name: itemName, isChecked: false))
|
|
}
|
|
|
|
try modelContext.save()
|
|
} catch {
|
|
print("Error adding grocery items: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - RecipeIngredientSection List Item
|
|
/*
|
|
fileprivate struct IngredientListItem: View {
|
|
@Environment(\.modelContext) var modelContext
|
|
@Bindable var recipeGroceries: RecipeGroceries
|
|
@Binding var ingredient: String
|
|
@Binding var servings: Double
|
|
@State var recipeYield: Double
|
|
@State var recipeId: String
|
|
|
|
|
|
@State var modifiedIngredient: AttributedString = ""
|
|
@State var isSelected: Bool = false
|
|
var unmodified: Bool {
|
|
servings == Double(recipeYield) || servings == 0
|
|
}
|
|
|
|
// Drag animation
|
|
@State private var dragOffset: CGFloat = 0
|
|
@State private var animationStartOffset: CGFloat = 0
|
|
let maxDragDistance = 50.0
|
|
|
|
var body: some View {
|
|
HStack(alignment: .top) {
|
|
if recipeGroceries.items.contains(ingredient) {
|
|
if #available(iOS 17.0, *) {
|
|
Image(systemName: "storefront")
|
|
.foregroundStyle(Color.green)
|
|
} else {
|
|
Image(systemName: "heart.text.square")
|
|
.foregroundStyle(Color.green)
|
|
}
|
|
|
|
} else if isSelected {
|
|
Image(systemName: "checkmark.circle")
|
|
} else {
|
|
Image(systemName: "circle")
|
|
}
|
|
if !unmodified && String(modifiedIngredient.characters) == ingredient {
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
.foregroundStyle(.red)
|
|
}
|
|
if unmodified {
|
|
Text(ingredient)
|
|
.multilineTextAlignment(.leading)
|
|
.lineLimit(5)
|
|
} else {
|
|
Text(modifiedIngredient)
|
|
.multilineTextAlignment(.leading)
|
|
.lineLimit(5)
|
|
//.foregroundStyle(String(modifiedIngredient.characters) == ingredient ? .red : .primary)
|
|
}
|
|
Spacer()
|
|
}
|
|
.onChange(of: servings) { _, newServings in
|
|
if recipeYield == 0 {
|
|
modifiedIngredient = Recipe.adjustIngredient(ingredient, by: newServings)
|
|
} else {
|
|
modifiedIngredient = Recipe.adjustIngredient(ingredient, by: newServings/recipeYield)
|
|
}
|
|
}
|
|
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
|
|
.onTapGesture {
|
|
isSelected.toggle()
|
|
}
|
|
.offset(x: dragOffset, y: 0)
|
|
.animation(.easeInOut, value: isSelected)
|
|
|
|
.gesture(
|
|
DragGesture()
|
|
.onChanged { gesture in
|
|
// Update drag offset as the user drags
|
|
if animationStartOffset == 0 {
|
|
animationStartOffset = gesture.translation.width
|
|
}
|
|
let dragAmount = gesture.translation.width
|
|
let offset = min(dragAmount, maxDragDistance + pow(dragAmount - maxDragDistance, 0.7)) - animationStartOffset
|
|
self.dragOffset = max(0, offset)
|
|
}
|
|
.onEnded { gesture in
|
|
withAnimation {
|
|
if dragOffset > maxDragDistance * 0.3 { // Swipe threshold
|
|
if recipeGroceries.items.contains(ingredient) {
|
|
cookbookState.groceryList.deleteItem(ingredient, fromRecipe: recipeId)
|
|
} else {
|
|
addToGroceryListAction()
|
|
}
|
|
}
|
|
// Animate back to original position
|
|
self.dragOffset = 0
|
|
self.animationStartOffset = 0
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
*/
|
|
|
|
|
|
struct ServingPickerView: View {
|
|
@Binding var selectedServingSize: Double
|
|
|
|
// Computed property to handle the text field input and update the selectedServingSize
|
|
var body: some View {
|
|
HStack {
|
|
Button {
|
|
selectedServingSize -= 1
|
|
} label: {
|
|
Image(systemName: "minus.square.fill")
|
|
.bold()
|
|
}
|
|
TextField("", value: $selectedServingSize, formatter: numberFormatter)
|
|
.keyboardType(.numbersAndPunctuation)
|
|
.lineLimit(1)
|
|
.multilineTextAlignment(.center)
|
|
.frame(width: 40)
|
|
Button {
|
|
selectedServingSize += 1
|
|
} label: {
|
|
Image(systemName: "plus.square.fill")
|
|
.bold()
|
|
}
|
|
}
|
|
.onChange(of: selectedServingSize) { _, newValue in
|
|
if newValue < 0 { selectedServingSize = 0 }
|
|
else if newValue > 100 { selectedServingSize = 100 }
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|