2.0: banner export descartable + slider calendario/platos en iPad

- 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3HaWmmtTQ1vdTERtSYU6p
This commit is contained in:
alexandrev-tibco
2026-07-24 16:29:22 +02:00
parent b70140191a
commit 543254337f
7 changed files with 81 additions and 10 deletions
@@ -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";
@@ -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";
@@ -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";
@@ -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é";
@@ -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";
@@ -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";
+75 -10
View File
@@ -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)