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:
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user