196 lines
6.9 KiB
Swift
196 lines
6.9 KiB
Swift
//
|
|
// GroceryListTabView.swift
|
|
// Nextcloud Cookbook iOS Client
|
|
//
|
|
// Created by Vincent Meilinger on 23.01.24.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
@Model class GroceryItem {
|
|
var name: String
|
|
var isChecked: Bool
|
|
|
|
init(name: String, isChecked: Bool) {
|
|
self.name = name
|
|
self.isChecked = isChecked
|
|
}
|
|
}
|
|
|
|
@Model class RecipeGroceries: Identifiable {
|
|
var id: String
|
|
var name: String
|
|
@Relationship(deleteRule: .cascade) var items: [GroceryItem]
|
|
var multiplier: Double
|
|
|
|
init(id: String, name: String, items: [GroceryItem], multiplier: Double) {
|
|
self.id = id
|
|
self.name = name
|
|
self.items = items
|
|
self.multiplier = multiplier
|
|
}
|
|
|
|
init(id: String, name: String) {
|
|
self.id = id
|
|
self.name = name
|
|
self.items = []
|
|
self.multiplier = 1
|
|
}
|
|
}
|
|
|
|
struct GroceryListTabView: View {
|
|
@Environment(\.modelContext) var modelContext
|
|
@Query var groceryList: [RecipeGroceries] = []
|
|
@State var newGroceries: String = ""
|
|
@FocusState private var isFocused: Bool
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
List {
|
|
HStack(alignment: .top) {
|
|
TextEditor(text: $newGroceries)
|
|
.padding(4)
|
|
.overlay(RoundedRectangle(cornerRadius: 8)
|
|
.stroke(Color.secondary).opacity(0.5))
|
|
.focused($isFocused)
|
|
Button {
|
|
if !newGroceries.isEmpty {
|
|
let items = newGroceries
|
|
.split(separator: "\n")
|
|
.compactMap { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
.filter { !$0.isEmpty }
|
|
Task {
|
|
await addGroceryItems(items, toCategory: "Other", named: String(localized: "Other"))
|
|
}
|
|
}
|
|
newGroceries = ""
|
|
|
|
} label: {
|
|
Text("Add")
|
|
}
|
|
.disabled(newGroceries.isEmpty)
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
|
|
|
|
ForEach(groceryList, id: \.name) { category in
|
|
Section {
|
|
ForEach(category.items, id: \.self) { item in
|
|
GroceryListItemView(item: item)
|
|
}
|
|
} header: {
|
|
HStack {
|
|
Text(category.name)
|
|
.foregroundStyle(Color.nextcloudBlue)
|
|
Spacer()
|
|
Button {
|
|
modelContext.delete(category)
|
|
} label: {
|
|
Image(systemName: "trash")
|
|
.foregroundStyle(Color.nextcloudBlue)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if groceryList.isEmpty {
|
|
Text("You're all set for cooking 🍓")
|
|
.font(.headline)
|
|
Text("Add groceries to this list by either using the button next to an ingredient list in a recipe, or by swiping right on individual ingredients of a recipe.")
|
|
.foregroundStyle(.secondary)
|
|
Text("To add grocieries manually, type them in the box below and press the button. To add multiple items at once, separate them by a new line.")
|
|
.foregroundStyle(.secondary)
|
|
Text("Your grocery list is stored locally and therefore not synchronized across your devices.")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
.listStyle(.plain)
|
|
.navigationTitle("Grocery List")
|
|
.toolbar {
|
|
Button {
|
|
do {
|
|
try modelContext.delete(model: RecipeGroceries.self)
|
|
} catch {
|
|
print("Failed to delete all GroceryCategory models.")
|
|
}
|
|
} label: {
|
|
Text("Delete")
|
|
.foregroundStyle(Color.nextcloudBlue)
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
private func addGroceryItems(_ itemNames: [String], toCategory categoryId: String, named name: String) async {
|
|
do {
|
|
// Find or create the target category
|
|
let categoryPredicate = #Predicate<RecipeGroceries> { $0.id == categoryId }
|
|
let fetchDescriptor = FetchDescriptor<RecipeGroceries>(predicate: categoryPredicate)
|
|
|
|
var targetCategory: RecipeGroceries?
|
|
if let existingCategory = try modelContext.fetch(fetchDescriptor).first {
|
|
targetCategory = existingCategory
|
|
} else {
|
|
// Create the category if it doesn't exist
|
|
let newCategory = RecipeGroceries(id: categoryId, name: name)
|
|
modelContext.insert(newCategory)
|
|
targetCategory = newCategory
|
|
}
|
|
|
|
guard let category = targetCategory else { return }
|
|
|
|
// Add new GroceryItems to the category
|
|
for itemName in itemNames {
|
|
let newItem = GroceryItem(name: itemName, isChecked: false)
|
|
category.items.append(newItem)
|
|
}
|
|
|
|
try modelContext.save()
|
|
} catch {
|
|
print("Error adding grocery items: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
private func deleteGroceryItems(at offsets: IndexSet, in category: RecipeGroceries) {
|
|
for index in offsets {
|
|
let itemToDelete = category.items[index]
|
|
modelContext.delete(itemToDelete)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fileprivate struct GroceryListItemView: View {
|
|
@Environment(\.modelContext) var modelContext
|
|
@Bindable var item: GroceryItem
|
|
|
|
var body: some View {
|
|
HStack(alignment: .top) {
|
|
if item.isChecked {
|
|
Image(systemName: "checkmark.circle")
|
|
} else {
|
|
Image(systemName: "circle")
|
|
}
|
|
|
|
Text("\(item.name)")
|
|
.multilineTextAlignment(.leading)
|
|
.lineLimit(5)
|
|
}
|
|
.padding(5)
|
|
.foregroundStyle(item.isChecked ? Color.secondary : Color.primary)
|
|
.onTapGesture(perform: { item.isChecked.toggle() })
|
|
.animation(.easeInOut, value: item.isChecked)
|
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
|
Button(action: { modelContext.delete(item) }) {
|
|
Label("Delete", systemImage: "trash")
|
|
}
|
|
.tint(.red)
|
|
}
|
|
}
|
|
}
|