Add cross-device grocery list sync via Nextcloud Cookbook API

Store a _groceryState JSON field on each recipe to track which
ingredients have been added, completed, or removed. Uses per-item
last-writer-wins conflict resolution with ISO 8601 timestamps.
Debounced push (2s) avoids excessive API calls; pull reconciles
on recipe open and app launch. Includes a settings toggle to
enable/disable sync.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 04:14:02 +01:00
parent 501434bd0e
commit 5890dbcad4
11 changed files with 323 additions and 10 deletions

View File

@@ -81,6 +81,10 @@ struct MainView: View {
}
}
await groceryList.load()
groceryList.configureSyncManager(appState: appState)
if UserSettings.shared.grocerySyncEnabled {
await groceryList.syncManager?.performInitialSync()
}
recipeViewModel.presentLoadingIndicator = false
}
}

View File

@@ -12,6 +12,7 @@ import SwiftUI
struct RecipeView: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var groceryList: GroceryListManager
@Environment(\.dismiss) private var dismiss
@StateObject var viewModel: ViewModel
@GestureState private var dragOffset = CGSize.zero
@@ -75,6 +76,15 @@ struct RecipeView: View {
fetchMode: UserSettings.shared.storeImages ? .preferLocal : .onlyServer
)
// Reconcile server grocery state with local data
if UserSettings.shared.grocerySyncEnabled {
groceryList.syncManager?.reconcileFromServer(
serverState: viewModel.recipeDetail.groceryState,
recipeId: String(viewModel.recipe.recipe_id),
recipeName: viewModel.recipeDetail.name
)
}
} else {
// Prepare view for a new recipe
if let preloaded = viewModel.preloadedRecipeDetail {

View File

@@ -93,10 +93,16 @@ struct SettingsView: View {
}
}
}
Toggle(isOn: $userSettings.grocerySyncEnabled) {
Text("Sync grocery list across devices")
}
} header: {
Text("Grocery List")
} footer: {
if userSettings.groceryListMode == GroceryListMode.appleReminders.rawValue {
if userSettings.grocerySyncEnabled {
Text("Grocery list state is synced via your Nextcloud server by storing it alongside recipe data.")
} else if userSettings.groceryListMode == GroceryListMode.appleReminders.rawValue {
Text("Grocery items will be saved to Apple Reminders. The Grocery List tab will be hidden since you can manage items directly in the Reminders app.")
} else {
Text("Grocery items are stored locally on this device.")