Nextcloud Login refactoring
This commit is contained in:
@@ -7,55 +7,158 @@
|
||||
|
||||
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(CookbookState.self) var cookbookState
|
||||
|
||||
@Environment(\.modelContext) var modelContext
|
||||
@Query var groceryList: [RecipeGroceries] = []
|
||||
@State var newGroceries: String = ""
|
||||
@FocusState private var isFocused: Bool
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
if cookbookState.groceryList.groceryDict.isEmpty {
|
||||
EmptyGroceryListView()
|
||||
} else {
|
||||
List {
|
||||
ForEach(cookbookState.groceryList.groceryDict.keys.sorted(), id: \.self) { key in
|
||||
Section {
|
||||
ForEach(cookbookState.groceryList.groceryDict[key]!.items) { item in
|
||||
GroceryListItemView(item: item, toggleAction: {
|
||||
cookbookState.groceryList.toggleItemChecked(item)
|
||||
}, deleteAction: {
|
||||
withAnimation {
|
||||
cookbookState.groceryList.deleteItem(item.name, fromRecipe: key)
|
||||
}
|
||||
})
|
||||
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"))
|
||||
}
|
||||
} header: {
|
||||
HStack {
|
||||
Text(cookbookState.groceryList.groceryDict[key]!.name)
|
||||
}
|
||||
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)
|
||||
Spacer()
|
||||
Button {
|
||||
cookbookState.groceryList.deleteGroceryRecipe(key)
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.foregroundStyle(Color.nextcloudBlue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Grocery List")
|
||||
.toolbar {
|
||||
Button {
|
||||
cookbookState.groceryList.deleteAll()
|
||||
} label: {
|
||||
Text("Delete")
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,9 +166,8 @@ struct GroceryListTabView: View {
|
||||
|
||||
|
||||
fileprivate struct GroceryListItemView: View {
|
||||
let item: GroceryRecipeItem
|
||||
let toggleAction: () -> Void
|
||||
let deleteAction: () -> Void
|
||||
@Environment(\.modelContext) var modelContext
|
||||
@Bindable var item: GroceryItem
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
@@ -81,149 +183,13 @@ fileprivate struct GroceryListItemView: View {
|
||||
}
|
||||
.padding(5)
|
||||
.foregroundStyle(item.isChecked ? Color.secondary : Color.primary)
|
||||
.onTapGesture(perform: toggleAction)
|
||||
.onTapGesture(perform: { item.isChecked.toggle() })
|
||||
.animation(.easeInOut, value: item.isChecked)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button(action: deleteAction) {
|
||||
Button(action: { modelContext.delete(item) }) {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fileprivate struct EmptyGroceryListView: View {
|
||||
var body: some View {
|
||||
List {
|
||||
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("Your grocery list is stored locally and therefore not synchronized across your devices.")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.navigationTitle("Grocery List")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Grocery List Logic
|
||||
|
||||
|
||||
class GroceryRecipe: Identifiable, Codable {
|
||||
let name: String
|
||||
var items: [GroceryRecipeItem]
|
||||
|
||||
init(name: String, items: [GroceryRecipeItem]) {
|
||||
self.name = name
|
||||
self.items = items
|
||||
}
|
||||
|
||||
init(name: String, item: GroceryRecipeItem) {
|
||||
self.name = name
|
||||
self.items = [item]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class GroceryRecipeItem: Identifiable, Codable {
|
||||
let name: String
|
||||
var isChecked: Bool
|
||||
|
||||
init(_ name: String, isChecked: Bool = false) {
|
||||
self.name = name
|
||||
self.isChecked = isChecked
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Observable class GroceryList {
|
||||
let dataStore: DataStore = DataStore()
|
||||
var groceryDict: [String: GroceryRecipe] = [:]
|
||||
var sortBySimilarity: Bool = false
|
||||
|
||||
|
||||
func addItem(_ itemName: String, toRecipe recipeId: String, recipeName: String? = nil, saveGroceryDict: Bool = true) {
|
||||
print("Adding item of recipe \(String(describing: recipeName))")
|
||||
if self.groceryDict[recipeId] != nil {
|
||||
self.groceryDict[recipeId]?.items.append(GroceryRecipeItem(itemName))
|
||||
} else {
|
||||
let newRecipe = GroceryRecipe(name: recipeName ?? "-", items: [GroceryRecipeItem(itemName)])
|
||||
self.groceryDict[recipeId] = newRecipe
|
||||
}
|
||||
if saveGroceryDict {
|
||||
self.save()
|
||||
}
|
||||
}
|
||||
|
||||
func addItems(_ items: [String], toRecipe recipeId: String, recipeName: String? = nil) {
|
||||
for item in items {
|
||||
addItem(item, toRecipe: recipeId, recipeName: recipeName, saveGroceryDict: false)
|
||||
}
|
||||
save()
|
||||
}
|
||||
|
||||
func deleteItem(_ itemName: String, fromRecipe recipeId: String) {
|
||||
print("Deleting item \(itemName)")
|
||||
guard let recipe = groceryDict[recipeId] else { return }
|
||||
guard let itemIndex = groceryDict[recipeId]?.items.firstIndex(where: { $0.name == itemName }) else { return }
|
||||
groceryDict[recipeId]?.items.remove(at: itemIndex)
|
||||
if groceryDict[recipeId]!.items.isEmpty {
|
||||
groceryDict.removeValue(forKey: recipeId)
|
||||
}
|
||||
save()
|
||||
}
|
||||
|
||||
func deleteGroceryRecipe(_ recipeId: String) {
|
||||
print("Deleting grocery recipe with id \(recipeId)")
|
||||
groceryDict.removeValue(forKey: recipeId)
|
||||
save()
|
||||
}
|
||||
|
||||
func deleteAll() {
|
||||
print("Deleting all grocery items")
|
||||
groceryDict = [:]
|
||||
save()
|
||||
}
|
||||
|
||||
func toggleItemChecked(_ groceryItem: GroceryRecipeItem) {
|
||||
print("Item checked: \(groceryItem.name)")
|
||||
groceryItem.isChecked.toggle()
|
||||
save()
|
||||
}
|
||||
|
||||
func containsItem(at recipeId: String, item: String) -> Bool {
|
||||
guard let recipe = groceryDict[recipeId] else { return false }
|
||||
if recipe.items.contains(where: { $0.name == item }) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsRecipe(_ recipeId: String) -> Bool {
|
||||
return groceryDict[recipeId] != nil
|
||||
}
|
||||
|
||||
func save() {
|
||||
Task {
|
||||
await dataStore.save(data: groceryDict, toPath: "grocery_list.data")
|
||||
}
|
||||
}
|
||||
|
||||
func load() async {
|
||||
do {
|
||||
guard let groceryDict: [String: GroceryRecipe] = try await dataStore.load(
|
||||
fromPath: "grocery_list.data"
|
||||
) else { return }
|
||||
self.groceryDict = groceryDict
|
||||
} catch {
|
||||
print("Unable to load grocery list")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user