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