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:
@@ -13,6 +13,7 @@ import SwiftUI
|
||||
struct RecipeView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var groceryList: GroceryListManager
|
||||
@EnvironmentObject var mealPlan: MealPlanManager
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@StateObject var viewModel: ViewModel
|
||||
@GestureState private var dragOffset = CGSize.zero
|
||||
@@ -50,6 +51,15 @@ struct RecipeView: View {
|
||||
.sheet(isPresented: $viewModel.presentKeywordSheet) {
|
||||
KeywordPickerView(title: "Keywords", searchSuggestions: appState.allKeywords, selection: $viewModel.observableRecipeDetail.keywords)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.presentMealPlanSheet) {
|
||||
AddToMealPlanSheet(
|
||||
recipeId: String(viewModel.recipe.recipe_id),
|
||||
recipeName: viewModel.observableRecipeDetail.name,
|
||||
prepTime: viewModel.recipeDetail.prepTime,
|
||||
recipeImage: viewModel.recipeImage
|
||||
)
|
||||
.environmentObject(mealPlan)
|
||||
}
|
||||
.task {
|
||||
// Load recipe detail
|
||||
if !viewModel.newRecipe {
|
||||
@@ -85,6 +95,15 @@ struct RecipeView: View {
|
||||
)
|
||||
}
|
||||
|
||||
// Reconcile server meal plan state with local data
|
||||
if UserSettings.shared.mealPlanSyncEnabled {
|
||||
mealPlan.syncManager?.reconcileFromServer(
|
||||
serverAssignment: viewModel.recipeDetail.mealPlanAssignment,
|
||||
recipeId: String(viewModel.recipe.recipe_id),
|
||||
recipeName: viewModel.recipeDetail.name
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
// Prepare view for a new recipe
|
||||
if let preloaded = viewModel.preloadedRecipeDetail {
|
||||
@@ -196,6 +215,22 @@ struct RecipeView: View {
|
||||
|
||||
RecipeDurationSection(viewModel: viewModel)
|
||||
|
||||
Button {
|
||||
viewModel.presentMealPlanSheet = true
|
||||
} label: {
|
||||
Label("Plan recipe", systemImage: "calendar.badge.plus")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.foregroundStyle(Color.nextcloudBlue)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.nextcloudBlue.opacity(0.1))
|
||||
)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
Divider()
|
||||
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
|
||||
@@ -279,6 +314,7 @@ struct RecipeView: View {
|
||||
|
||||
@Published var presentShareSheet: Bool = false
|
||||
@Published var presentKeywordSheet: Bool = false
|
||||
@Published var presentMealPlanSheet: Bool = false
|
||||
|
||||
var recipe: Recipe
|
||||
var sharedURL: URL? = nil
|
||||
@@ -328,6 +364,7 @@ struct RecipeView: View {
|
||||
|
||||
struct RecipeViewToolBar: ToolbarContent {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var mealPlan: MealPlanManager
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@ObservedObject var viewModel: RecipeView.ViewModel
|
||||
|
||||
@@ -474,6 +511,7 @@ struct RecipeViewToolBar: ToolbarContent {
|
||||
}
|
||||
await appState.getCategories()
|
||||
await appState.getCategory(named: category, fetchMode: .preferServer)
|
||||
mealPlan.removeAllAssignments(forRecipeId: String(id))
|
||||
viewModel.presentAlert(RecipeAlert.DELETE_SUCCESS)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user