// // GroceryListManager.swift // Nextcloud Cookbook iOS Client // import EventKit import Foundation import OSLog import SwiftUI @MainActor class GroceryListManager: ObservableObject { @Published var groceryDict: [String: GroceryRecipe] = [:] let localStore = GroceryList() let remindersStore = RemindersGroceryStore() var syncManager: GroceryStateSyncManager? /// Recipe IDs modified by our own CRUD — skip these in the onDataChanged callback /// to avoid duplicate syncs. private var recentlyModifiedByUs: Set = [] private var mode: GroceryListMode { GroceryListMode(rawValue: UserSettings.shared.groceryListMode) ?? .inApp } init() { remindersStore.onDataChanged = { [weak self] in guard let self else { return } if self.mode == .appleReminders { let previousDict = self.groceryDict self.groceryDict = self.remindersStore.groceryDict // Only sync recipes that changed externally (e.g. checked off in Reminders app), // not ones we just modified ourselves. for recipeId in self.remindersStore.groceryDict.keys { guard !self.recentlyModifiedByUs.contains(recipeId) else { continue } // Detect if item count changed (external add/remove/complete) let oldCount = previousDict[recipeId]?.items.count ?? 0 let newCount = self.remindersStore.groceryDict[recipeId]?.items.count ?? 0 if oldCount != newCount { self.syncManager?.scheduleSync(forRecipeId: recipeId) } } self.recentlyModifiedByUs.removeAll() } } } func configureSyncManager(appState: AppState) { syncManager = GroceryStateSyncManager(appState: appState, groceryManager: self) } // MARK: - Grocery Operations func addItem(_ itemName: String, toRecipe recipeId: String, recipeName: String? = nil) { switch mode { case .inApp: localStore.addItem(itemName, toRecipe: recipeId, recipeName: recipeName) groceryDict = localStore.groceryDict case .appleReminders: recentlyModifiedByUs.insert(recipeId) remindersStore.addItem(itemName, toRecipe: recipeId, recipeName: recipeName) groceryDict = remindersStore.groceryDict } syncManager?.scheduleSync(forRecipeId: recipeId) } func addItems(_ items: [String], toRecipe recipeId: String, recipeName: String? = nil) { switch mode { case .inApp: localStore.addItems(items, toRecipe: recipeId, recipeName: recipeName) groceryDict = localStore.groceryDict case .appleReminders: recentlyModifiedByUs.insert(recipeId) remindersStore.addItems(items, toRecipe: recipeId, recipeName: recipeName) groceryDict = remindersStore.groceryDict } syncManager?.scheduleSync(forRecipeId: recipeId) } func deleteItem(_ itemName: String, fromRecipe recipeId: String) { switch mode { case .inApp: localStore.deleteItem(itemName, fromRecipe: recipeId) groceryDict = localStore.groceryDict case .appleReminders: recentlyModifiedByUs.insert(recipeId) remindersStore.deleteItem(itemName, fromRecipe: recipeId) } syncManager?.scheduleSync(forRecipeId: recipeId) } func deleteGroceryRecipe(_ recipeId: String) { switch mode { case .inApp: localStore.deleteGroceryRecipe(recipeId) groceryDict = localStore.groceryDict case .appleReminders: recentlyModifiedByUs.insert(recipeId) remindersStore.deleteGroceryRecipe(recipeId) } syncManager?.scheduleSync(forRecipeId: recipeId) } func deleteAll() { let recipeIds = Array(groceryDict.keys) switch mode { case .inApp: localStore.deleteAll() groceryDict = localStore.groceryDict case .appleReminders: recentlyModifiedByUs.formUnion(recipeIds) remindersStore.deleteAll() } for recipeId in recipeIds { syncManager?.scheduleSync(forRecipeId: recipeId) } } func toggleItemChecked(_ groceryItem: GroceryRecipeItem) { switch mode { case .inApp: localStore.toggleItemChecked(groceryItem) case .appleReminders: // Reminders don't support checked state in our model break } } func containsItem(at recipeId: String, item: String) -> Bool { switch mode { case .inApp: return localStore.containsItem(at: recipeId, item: item) case .appleReminders: return remindersStore.containsItem(at: recipeId, item: item) } } func containsRecipe(_ recipeId: String) -> Bool { switch mode { case .inApp: return localStore.containsRecipe(recipeId) case .appleReminders: return remindersStore.containsRecipe(recipeId) } } func load() async { switch mode { case .inApp: await localStore.load() groceryDict = localStore.groceryDict case .appleReminders: await remindersStore.load() groceryDict = remindersStore.groceryDict } } func save() { if mode == .inApp { localStore.save() } } // MARK: - Reminders Helpers (for Settings UI) var remindersPermissionStatus: EKAuthorizationStatus { remindersStore.checkPermissionStatus() } func requestRemindersAccess() async -> Bool { await remindersStore.requestAccess() } func availableReminderLists() -> [EKCalendar] { remindersStore.availableReminderLists() } }