From 543254337faeeac9bb7ca896e0335bab1a18bf44 Mon Sep 17 00:00:00 2001 From: alexandrev-tibco Date: Fri, 24 Jul 2026 16:29:22 +0200 Subject: [PATCH] 2.0: banner export descartable + slider calendario/platos en iPad MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - El banner "Your week is ready to export" ahora tiene botón de cerrar (X) que lo oculta para esa semana (persistido en AppStorage). - iPad/Mac: handle arrastrable entre calendario y platos para rebalancear el alto (fracción persistida, clamp 0.28–0.72); el calendario y la lista se recalculan al arrastrar. Sustituye la altura fija anterior. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01A3HaWmmtTQ1vdTERtSYU6p --- .../Resources/de.lproj/Localizable.strings | 1 + .../Resources/en.lproj/Localizable.strings | 1 + .../Resources/es.lproj/Localizable.strings | 1 + .../Resources/fr.lproj/Localizable.strings | 1 + .../Resources/it.lproj/Localizable.strings | 1 + .../Resources/pt-BR.lproj/Localizable.strings | 1 + MealMood/Views/Home/HomeView.swift | 85 ++++++++++++++++--- 7 files changed, 81 insertions(+), 10 deletions(-) diff --git a/MealMood/Resources/de.lproj/Localizable.strings b/MealMood/Resources/de.lproj/Localizable.strings index 85e6434..09dad96 100644 --- a/MealMood/Resources/de.lproj/Localizable.strings +++ b/MealMood/Resources/de.lproj/Localizable.strings @@ -407,6 +407,7 @@ "dish_ingredient_placeholder" = "z. B. 200 g Spaghetti"; "dish_ingredients_add" = "Zutat hinzufügen"; "dish_dictate" = "Diktieren"; +"calendar_resize_hint" = "Ziehen, um Kalender und Gerichte anzupassen"; "dish_ingredients_dictate" = "Diktieren"; "dictation_listening" = "Höre zu…"; "dictation_stop" = "Fertig"; diff --git a/MealMood/Resources/en.lproj/Localizable.strings b/MealMood/Resources/en.lproj/Localizable.strings index 25f3814..41937be 100644 --- a/MealMood/Resources/en.lproj/Localizable.strings +++ b/MealMood/Resources/en.lproj/Localizable.strings @@ -407,6 +407,7 @@ "dish_ingredient_placeholder" = "e.g. 200 g spaghetti"; "dish_ingredients_add" = "Add ingredient"; "dish_dictate" = "Dictate"; +"calendar_resize_hint" = "Drag to resize calendar and dishes"; "dish_ingredients_dictate" = "Dictate"; "dictation_listening" = "Listening…"; "dictation_stop" = "Done"; diff --git a/MealMood/Resources/es.lproj/Localizable.strings b/MealMood/Resources/es.lproj/Localizable.strings index 1bc1a21..fab271f 100644 --- a/MealMood/Resources/es.lproj/Localizable.strings +++ b/MealMood/Resources/es.lproj/Localizable.strings @@ -407,6 +407,7 @@ "dish_ingredient_placeholder" = "p. ej. 200 g de espaguetis"; "dish_ingredients_add" = "Añadir ingrediente"; "dish_dictate" = "Dictar"; +"calendar_resize_hint" = "Arrastra para ajustar calendario y platos"; "dish_ingredients_dictate" = "Dictar"; "dictation_listening" = "Escuchando…"; "dictation_stop" = "Listo"; diff --git a/MealMood/Resources/fr.lproj/Localizable.strings b/MealMood/Resources/fr.lproj/Localizable.strings index da00be8..f8aac05 100644 --- a/MealMood/Resources/fr.lproj/Localizable.strings +++ b/MealMood/Resources/fr.lproj/Localizable.strings @@ -407,6 +407,7 @@ "dish_ingredient_placeholder" = "ex. 200 g de spaghettis"; "dish_ingredients_add" = "Ajouter un ingrédient"; "dish_dictate" = "Dicter"; +"calendar_resize_hint" = "Faites glisser pour ajuster le calendrier et les plats"; "dish_ingredients_dictate" = "Dicter"; "dictation_listening" = "Écoute…"; "dictation_stop" = "Terminé"; diff --git a/MealMood/Resources/it.lproj/Localizable.strings b/MealMood/Resources/it.lproj/Localizable.strings index 12a9ce4..01a46e3 100644 --- a/MealMood/Resources/it.lproj/Localizable.strings +++ b/MealMood/Resources/it.lproj/Localizable.strings @@ -407,6 +407,7 @@ "dish_ingredient_placeholder" = "es. 200 g di spaghetti"; "dish_ingredients_add" = "Aggiungi ingrediente"; "dish_dictate" = "Detta"; +"calendar_resize_hint" = "Trascina per ridimensionare calendario e piatti"; "dish_ingredients_dictate" = "Detta"; "dictation_listening" = "In ascolto…"; "dictation_stop" = "Fatto"; diff --git a/MealMood/Resources/pt-BR.lproj/Localizable.strings b/MealMood/Resources/pt-BR.lproj/Localizable.strings index c893fa9..652b208 100644 --- a/MealMood/Resources/pt-BR.lproj/Localizable.strings +++ b/MealMood/Resources/pt-BR.lproj/Localizable.strings @@ -407,6 +407,7 @@ "dish_ingredient_placeholder" = "ex.: 200 g de espaguete"; "dish_ingredients_add" = "Adicionar ingrediente"; "dish_dictate" = "Ditar"; +"calendar_resize_hint" = "Arraste para ajustar calendário e pratos"; "dish_ingredients_dictate" = "Ditar"; "dictation_listening" = "Ouvindo…"; "dictation_stop" = "Pronto"; diff --git a/MealMood/Views/Home/HomeView.swift b/MealMood/Views/Home/HomeView.swift index e7d6b7c..266454b 100644 --- a/MealMood/Views/Home/HomeView.swift +++ b/MealMood/Views/Home/HomeView.swift @@ -44,14 +44,63 @@ struct HomeView: View { private var isRunningOnMac: Bool { ProcessInfo.processInfo.isiOSAppOnMac } private var contentHorizontalPadding: CGFloat { (isRunningOnMac || horizontalSizeClass == .regular) ? 14 : 0 } - /// On iPad/Mac (regular width) let the calendar claim roughly half of the - /// available height so it feels like the centerpiece instead of a strip at - /// the top. On iPhone (compact) return nil to keep the intrinsic sizing. + /// User-adjustable split (iPad/Mac) between the calendar and the dish + /// drawer, dragged via the resize handle. 0.5 = half each. + @AppStorage("calendarHeightFraction") private var calendarFraction: Double = 0.5 + @State private var dragStartFraction: Double? + + /// Weeks whose export banner the user has dismissed (comma-joined week keys). + @AppStorage("dismissedExportWeeks") private var dismissedExportWeeksRaw: String = "" + + private func exportWeekKey(_ plan: WeekPlan) -> String { + String(Int(plan.weekStartDate.timeIntervalSince1970)) + } + private func isExportCalloutDismissed(_ plan: WeekPlan) -> Bool { + dismissedExportWeeksRaw.split(separator: ",").contains(Substring(exportWeekKey(plan))) + } + private func dismissExportCallout(_ plan: WeekPlan) { + var keys = Set(dismissedExportWeeksRaw.split(separator: ",").map(String.init)) + keys.insert(exportWeekKey(plan)) + // Cap the stored history so it can't grow unbounded. + dismissedExportWeeksRaw = keys.sorted().suffix(24).joined(separator: ",") + } + + private static let minCalendarFraction: Double = 0.28 + private static let maxCalendarFraction: Double = 0.72 + + /// On iPad/Mac (regular width) the calendar takes `calendarFraction` of the + /// available height (draggable), keeping a floor for the dish drawer. On + /// iPhone (compact) return nil to keep the intrinsic sizing. private func calendarHeight(availableHeight: CGFloat) -> CGFloat? { guard horizontalSizeClass == .regular, availableHeight > 0 else { return nil } - let target = availableHeight * 0.5 - // Keep at least ~260pt for the dish drawer below. - return min(max(target, 300), availableHeight - 260) + let target = availableHeight * calendarFraction + // Keep a sensible floor for both the calendar and the dish drawer below. + return min(max(target, 200), availableHeight - 200) + } + + /// Draggable handle (iPad/Mac only) to rebalance calendar vs. dishes. + private func calendarResizeHandle(availableHeight: CGFloat) -> some View { + VStack(spacing: 3) { + RoundedRectangle(cornerRadius: 2.5) + .fill(Color.mealMoodTextSecondary.opacity(0.35)) + .frame(width: 40, height: 5) + } + .frame(maxWidth: .infinity) + .frame(height: 20) + .contentShape(Rectangle()) + .gesture( + DragGesture() + .onChanged { value in + guard availableHeight > 0 else { return } + let base = dragStartFraction ?? calendarFraction + if dragStartFraction == nil { dragStartFraction = base } + let delta = value.translation.height / availableHeight + calendarFraction = min(Self.maxCalendarFraction, + max(Self.minCalendarFraction, base + delta)) + } + .onEnded { _ in dragStartFraction = nil } + ) + .accessibilityLabel(Text("calendar_resize_hint")) } var body: some View { @@ -276,8 +325,12 @@ struct HomeView: View { ) .frame(height: calendarHeight(availableHeight: contentGeo.size.height)) + if horizontalSizeClass == .regular { + calendarResizeHandle(availableHeight: contentGeo.size.height) + } + let filledCount = plan.slotList.filter { $0.dishId != nil || $0.isEatingOut }.count - if filledCount > 0 { + if filledCount > 0 && !isExportCalloutDismissed(plan) { exportCallout(plan: plan, settings: settings) } @@ -872,9 +925,21 @@ struct HomeView: View { @ViewBuilder private func exportCallout(plan: WeekPlan, settings: AppSettings) -> some View { VStack(alignment: .leading, spacing: 10) { - Text("share_week_callout_title") - .font(.mealMoodBodyBold) - .foregroundColor(.mealMoodTextPrimary) + HStack(alignment: .top) { + Text("share_week_callout_title") + .font(.mealMoodBodyBold) + .foregroundColor(.mealMoodTextPrimary) + Spacer() + Button { + withAnimation { dismissExportCallout(plan) } + } label: { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 18)) + .foregroundColor(.mealMoodTextSecondary.opacity(0.7)) + } + .buttonStyle(.plain) + .accessibilityLabel(Text("dish_cancel")) + } Text("share_week_callout_subtitle") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary)