Add meal plan feature with cross-device sync and automatic stale data cleanup

Introduces weekly meal planning with a calendar-based tab view, per-recipe
date assignments synced via Nextcloud Cookbook custom metadata, and 30-day
automatic pruning of old entries on load, save, and sync merge.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 05:23:29 +01:00
parent 5890dbcad4
commit 8b23652f10
17 changed files with 1332 additions and 6 deletions

View File

@@ -51,8 +51,9 @@ struct RecipeDetail: Codable {
var recipeInstructions: [String]
var nutrition: [String:String]
var groceryState: GroceryState?
var mealPlanAssignment: MealPlanAssignment?
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) {
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, mealPlanAssignment: MealPlanAssignment? = nil) {
self.name = name
self.keywords = keywords
self.dateCreated = dateCreated
@@ -71,6 +72,7 @@ struct RecipeDetail: Codable {
self.recipeInstructions = recipeInstructions
self.nutrition = nutrition
self.groceryState = groceryState
self.mealPlanAssignment = mealPlanAssignment
}
init() {
@@ -92,12 +94,14 @@ struct RecipeDetail: Codable {
recipeInstructions = []
nutrition = [:]
groceryState = nil
mealPlanAssignment = 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"
case mealPlanAssignment = "_mealPlanAssignment"
}
init(from decoder: Decoder) throws {
@@ -138,6 +142,7 @@ struct RecipeDetail: Codable {
}
groceryState = try? container.decode(GroceryState.self, forKey: .groceryState)
mealPlanAssignment = try? container.decode(MealPlanAssignment.self, forKey: .mealPlanAssignment)
}
func encode(to encoder: Encoder) throws {
@@ -161,6 +166,7 @@ struct RecipeDetail: Codable {
try container.encode(recipeInstructions, forKey: .recipeInstructions)
try container.encode(nutrition, forKey: .nutrition)
try container.encodeIfPresent(groceryState, forKey: .groceryState)
try container.encodeIfPresent(mealPlanAssignment, forKey: .mealPlanAssignment)
}
}