2.0: beta feedback — edit dish from shopping list + dish photos in planner

- Shopping list: dishes under "No ingredients yet" now open the dish editor on
  tap (pencil affordance); list reconciles on dismiss. This was a dead end on
  devices without Apple Intelligence (no Generate button, e.g. iPhone 14).
- Planner: dish photo as a scrimmed card background in the week calendar,
  behind a new Settings toggle (settings_show_dish_photos, default on). New
  optional AppSettings.showDishPhotosInPlanner (CloudKit/migration safe).
- FilledSlotView: inlines the meal-card chrome to support the photo layer.
- Toggle localized in all 6 languages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkanrydYtrme8wipTzWssG
This commit is contained in:
alexandrev-tibco
2026-07-14 22:06:11 +02:00
parent 9118cc6e86
commit 29cba08678
11 changed files with 83 additions and 6 deletions
+23 -1
View File
@@ -6,6 +6,7 @@ struct FilledSlotView: View {
let tags: [(name: String, color: String)] let tags: [(name: String, color: String)]
let mealType: MealType let mealType: MealType
var showsRuleWarning: Bool = false var showsRuleWarning: Bool = false
var photoData: Data? = nil
var onRemove: (() -> Void)? var onRemove: (() -> Void)?
var body: some View { var body: some View {
@@ -52,6 +53,27 @@ struct FilledSlotView: View {
} }
.padding(10) .padding(10)
.frame(maxWidth: .infinity, minHeight: 80, alignment: .topLeading) .frame(maxWidth: .infinity, minHeight: 80, alignment: .topLeading)
.mealCardStyle() .background(cardBackground)
.cornerRadius(16)
.shadow(color: Color.black.opacity(0.05), radius: 8, y: 4)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color(hex: "#F0F0F0"), lineWidth: 1)
)
}
/// Dish photo as a subtle card background, scrimmed so text stays legible.
@ViewBuilder
private var cardBackground: some View {
if let photoData, let image = UIImage(data: photoData) {
ZStack {
Image(uiImage: image)
.resizable()
.scaledToFill()
Color.mealMoodSurface.opacity(0.75)
}
} else {
Color.mealMoodSurface
}
} }
} }
+8
View File
@@ -24,6 +24,9 @@ final class AppSettings {
var reminderMinutesBefore: Int? var reminderMinutesBefore: Int?
var iCloudSyncEnabled: Bool? var iCloudSyncEnabled: Bool?
var weekExportStyle: String? var weekExportStyle: String?
/// Show dish photos as planner card backgrounds (2.0). Optional for
/// CloudKit/migration compatibility; resolves to true.
var showDishPhotosInPlanner: Bool?
var isPremium: Bool = false var isPremium: Bool = false
var onboardingCompleted: Bool = false var onboardingCompleted: Bool = false
@@ -74,6 +77,11 @@ final class AppSettings {
set { iCloudSyncEnabled = newValue } set { iCloudSyncEnabled = newValue }
} }
var showDishPhotosInPlannerResolved: Bool {
get { showDishPhotosInPlanner ?? true }
set { showDishPhotosInPlanner = newValue }
}
var weekExportStyleEnum: WeekExportStyle { var weekExportStyleEnum: WeekExportStyle {
get { WeekExportStyle(rawValue: weekExportStyle ?? "") ?? .defaultStyle } get { WeekExportStyle(rawValue: weekExportStyle ?? "") ?? .defaultStyle }
set { weekExportStyle = newValue.rawValue } set { weekExportStyle = newValue.rawValue }
@@ -410,3 +410,5 @@
// Meal types (2.0) // Meal types (2.0)
"breakfast" = "Frühstück"; "breakfast" = "Frühstück";
"snack" = "Snack"; "snack" = "Snack";
"settings_show_dish_photos" = "Gerichtfotos im Planer anzeigen";
@@ -410,3 +410,5 @@
// Meal types (2.0) // Meal types (2.0)
"breakfast" = "Breakfast"; "breakfast" = "Breakfast";
"snack" = "Snack"; "snack" = "Snack";
"settings_show_dish_photos" = "Show dish photos in planner";
@@ -410,3 +410,5 @@
// Meal types (2.0) // Meal types (2.0)
"breakfast" = "Desayuno"; "breakfast" = "Desayuno";
"snack" = "Merienda"; "snack" = "Merienda";
"settings_show_dish_photos" = "Mostrar fotos de platos en el planificador";
@@ -410,3 +410,5 @@
// Meal types (2.0) // Meal types (2.0)
"breakfast" = "Petit-déjeuner"; "breakfast" = "Petit-déjeuner";
"snack" = "Goûter"; "snack" = "Goûter";
"settings_show_dish_photos" = "Afficher les photos des plats dans le planning";
@@ -410,3 +410,5 @@
// Meal types (2.0) // Meal types (2.0)
"breakfast" = "Colazione"; "breakfast" = "Colazione";
"snack" = "Merenda"; "snack" = "Merenda";
"settings_show_dish_photos" = "Mostra le foto dei piatti nel planner";
@@ -410,3 +410,5 @@
// Meal types (2.0) // Meal types (2.0)
"breakfast" = "Café da manhã"; "breakfast" = "Café da manhã";
"snack" = "Lanche"; "snack" = "Lanche";
"settings_show_dish_photos" = "Mostrar fotos dos pratos no planejador";
+7 -3
View File
@@ -271,6 +271,7 @@ struct WeekCalendarView: View {
tags: dishTags.map { (name: $0.localizedName(language: settings.languageEnum.resolved()), color: $0.color) }, tags: dishTags.map { (name: $0.localizedName(language: settings.languageEnum.resolved()), color: $0.color) },
mealType: mealType, mealType: mealType,
showsRuleWarning: slot.isRuleOverridden, showsRuleWarning: slot.isRuleOverridden,
photoData: settings.showDishPhotosInPlannerResolved ? dishData.photoData : nil,
onRemove: viewModel.canEditCurrentWeek ? { onRemove: viewModel.canEditCurrentWeek ? {
guard let live = liveSlot(for: slot) else { return } guard let live = liveSlot(for: slot) else { return }
viewModel.removeDish(from: live, plan: plan, settings: settings) viewModel.removeDish(from: live, plan: plan, settings: settings)
@@ -313,6 +314,7 @@ struct WeekCalendarView: View {
let name: String let name: String
let descriptionText: String? let descriptionText: String?
let tagIds: [UUID] let tagIds: [UUID]
let photoData: Data?
} }
private struct DisplaySlot { private struct DisplaySlot {
@@ -326,7 +328,7 @@ struct WeekCalendarView: View {
private var dishesSnapshotKey: String { private var dishesSnapshotKey: String {
dishes dishes
.map { "\($0.id.uuidString)|\($0.name)|\($0.tagIds.count)|\($0.descriptionText ?? "")" } .map { "\($0.id.uuidString)|\($0.name)|\($0.tagIds.count)|\($0.descriptionText ?? "")|\($0.photoData?.count ?? 0)" }
.joined(separator: ";") .joined(separator: ";")
} }
@@ -335,7 +337,8 @@ struct WeekCalendarView: View {
return DishSnapshot( return DishSnapshot(
name: live.name, name: live.name,
descriptionText: live.descriptionText, descriptionText: live.descriptionText,
tagIds: live.tagIds tagIds: live.tagIds,
photoData: live.photoData
) )
} }
return dishSnapshotsById[dishId] return dishSnapshotsById[dishId]
@@ -348,7 +351,8 @@ struct WeekCalendarView: View {
DishSnapshot( DishSnapshot(
name: dish.name, name: dish.name,
descriptionText: dish.descriptionText, descriptionText: dish.descriptionText,
tagIds: dish.tagIds tagIds: dish.tagIds,
photoData: dish.photoData
) )
) )
}) })
@@ -89,6 +89,12 @@ struct SettingsView: View {
)) ))
.tint(.mealMoodCoral) .tint(.mealMoodCoral)
Toggle("settings_show_dish_photos", isOn: Binding(
get: { settings.showDishPhotosInPlannerResolved },
set: { settings.showDishPhotosInPlannerResolved = $0 }
))
.tint(.mealMoodCoral)
Picker("settings_export_style", selection: Binding( Picker("settings_export_style", selection: Binding(
get: { settings.weekExportStyleEnum }, get: { settings.weekExportStyleEnum },
set: { settings.weekExportStyleEnum = $0 } set: { settings.weekExportStyleEnum = $0 }
@@ -18,6 +18,7 @@ struct ShoppingListView: View {
@State private var newItemText: String = "" @State private var newItemText: String = ""
@State private var generatingDishId: UUID? @State private var generatingDishId: UUID?
@State private var showPaywall = false @State private var showPaywall = false
@State private var editingDish: Dish?
private var items: [ShoppingItem] { private var items: [ShoppingItem] {
allItems allItems
@@ -131,6 +132,16 @@ struct ShoppingListView: View {
.sheet(isPresented: $showPaywall) { .sheet(isPresented: $showPaywall) {
PremiumView(settings: settings, source: "ingredient_generation") PremiumView(settings: settings, source: "ingredient_generation")
} }
.sheet(item: $editingDish, onDismiss: {
// Pick up ingredients the user just added in the editor.
ShoppingListService.reconcile(
context: context,
weekStartDate: weekStartDate,
plannedDishes: plannedDishes
)
}) { dish in
DishFormView(dish: dish)
}
} }
} }
@@ -179,9 +190,23 @@ struct ShoppingListView: View {
@ViewBuilder @ViewBuilder
private func missingIngredientsRow(_ dish: Dish) -> some View { private func missingIngredientsRow(_ dish: Dish) -> some View {
HStack { HStack {
// Tapping the dish opens its editor the only path to add
// ingredients on devices without on-device generation.
Button {
editingDish = dish
} label: {
HStack(spacing: 6) {
Text(dish.name) Text(dish.name)
.foregroundColor(.mealMoodTextPrimary) .foregroundColor(.mealMoodTextPrimary)
Image(systemName: "square.and.pencil")
.font(.system(size: 13))
.foregroundColor(.mealMoodTextSecondary)
}
}
.buttonStyle(.borderless)
Spacer() Spacer()
if IngredientGenerator.isAvailable { if IngredientGenerator.isAvailable {
if generatingDishId == dish.id { if generatingDishId == dish.id {
ProgressView() ProgressView()