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

@@ -50,8 +50,9 @@ struct RecipeDetail: Codable {
var recipeIngredient: [String]
var recipeInstructions: [String]
var nutrition: [String:String]
init(name: String, keywords: String, dateCreated: String, dateModified: String, imageUrl: String, id: String, prepTime: String? = nil, cookTime: String? = nil, totalTime: String? = nil, description: String, url: String, recipeYield: Int, recipeCategory: String, tool: [String], recipeIngredient: [String], recipeInstructions: [String], nutrition: [String:String]) {
var groceryState: GroceryState?
init(name: String, keywords: String, dateCreated: String, dateModified: String, imageUrl: String, id: String, prepTime: String? = nil, cookTime: String? = nil, totalTime: String? = nil, description: String, url: String, recipeYield: Int, recipeCategory: String, tool: [String], recipeIngredient: [String], recipeInstructions: [String], nutrition: [String:String], groceryState: GroceryState? = nil) {
self.name = name
self.keywords = keywords
self.dateCreated = dateCreated
@@ -69,8 +70,9 @@ struct RecipeDetail: Codable {
self.recipeIngredient = recipeIngredient
self.recipeInstructions = recipeInstructions
self.nutrition = nutrition
self.groceryState = groceryState
}
init() {
name = ""
keywords = ""
@@ -89,11 +91,13 @@ struct RecipeDetail: Codable {
recipeIngredient = []
recipeInstructions = []
nutrition = [:]
groceryState = nil
}
// Custom decoder to handle value type ambiguity
private enum CodingKeys: String, CodingKey {
case name, keywords, dateCreated, dateModified, image, imageUrl, id, prepTime, cookTime, totalTime, description, url, recipeYield, recipeCategory, tool, recipeIngredient, recipeInstructions, nutrition
case groceryState = "_groceryState"
}
init(from decoder: Decoder) throws {
@@ -132,6 +136,8 @@ struct RecipeDetail: Codable {
} else {
nutrition = [:]
}
groceryState = try? container.decode(GroceryState.self, forKey: .groceryState)
}
func encode(to encoder: Encoder) throws {
@@ -154,6 +160,7 @@ struct RecipeDetail: Codable {
try container.encode(recipeIngredient, forKey: .recipeIngredient)
try container.encode(recipeInstructions, forKey: .recipeInstructions)
try container.encode(nutrition, forKey: .nutrition)
try container.encodeIfPresent(groceryState, forKey: .groceryState)
}
}