Add Apple Reminders integration for grocery list with local mapping persistence
Introduce a GroceryListManager facade that delegates to either the existing in-app GroceryList or a new RemindersGroceryStore backed by EventKit. Users choose the mode in Settings; when Reminders mode is active the Grocery List tab is hidden. Recipe-to-reminder grouping uses a local mapping file (reminder_mappings.data) instead of polluting the reminder's notes field, with automatic pruning when reminders are deleted externally. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
144
Nextcloud Cookbook iOS Client/Data/GroceryListManager.swift
Normal file
144
Nextcloud Cookbook iOS Client/Data/GroceryListManager.swift
Normal file
@@ -0,0 +1,144 @@
|
||||
//
|
||||
// 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()
|
||||
|
||||
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 {
|
||||
self.groceryDict = self.remindersStore.groceryDict
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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:
|
||||
remindersStore.addItem(itemName, toRecipe: recipeId, recipeName: recipeName)
|
||||
groceryDict = remindersStore.groceryDict
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
remindersStore.addItems(items, toRecipe: recipeId, recipeName: recipeName)
|
||||
groceryDict = remindersStore.groceryDict
|
||||
}
|
||||
}
|
||||
|
||||
func deleteItem(_ itemName: String, fromRecipe recipeId: String) {
|
||||
switch mode {
|
||||
case .inApp:
|
||||
localStore.deleteItem(itemName, fromRecipe: recipeId)
|
||||
groceryDict = localStore.groceryDict
|
||||
case .appleReminders:
|
||||
remindersStore.deleteItem(itemName, fromRecipe: recipeId)
|
||||
// Cache update happens async in RemindersGroceryStore via onDataChanged
|
||||
}
|
||||
}
|
||||
|
||||
func deleteGroceryRecipe(_ recipeId: String) {
|
||||
switch mode {
|
||||
case .inApp:
|
||||
localStore.deleteGroceryRecipe(recipeId)
|
||||
groceryDict = localStore.groceryDict
|
||||
case .appleReminders:
|
||||
remindersStore.deleteGroceryRecipe(recipeId)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAll() {
|
||||
switch mode {
|
||||
case .inApp:
|
||||
localStore.deleteAll()
|
||||
groceryDict = localStore.groceryDict
|
||||
case .appleReminders:
|
||||
remindersStore.deleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user