bc684a099c
Dos cosas que faltaban, las dos visibles en domingo con el fin de semana desactivado: El envio al reloj solo llevaba la semana en curso, asi que no habia forma de ver las semanas ya planificadas por delante. Ahora viajan cuatro (la anterior, la actual y las dos siguientes) y la vista de semana se desliza entre ellas, arrancando en la actual. Y un dia fuera del plan dejaba al reloj sin nada que enseñar: el domingo no existe en el payload si no planificas fines de semana, asi que la complicacion salia vacia y la app decia "abre MealMood en el iPhone", como si fuera un problema de sincronizacion. Ahora se distingue: si hoy no se planifica se dice, y se enseña el proximo dia que si tiene comidas — que es lo util en el reloj. De paso, las semanas se comparan con el calendario alineado a lunes. Con el del sistema, en las regiones donde la semana empieza en domingo, un domingo caia en otra semana que su propio lunes y un envio recien hecho parecia caducado. Refs #36, #37 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
204 lines
7.3 KiB
Swift
204 lines
7.3 KiB
Swift
import SwiftUI
|
|
|
|
/// Today page — Weather-app style: a bold colored header with the day, then
|
|
/// one vivid gradient card per meal.
|
|
struct TodayView: View {
|
|
@EnvironmentObject private var store: WatchWeekStore
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
if let payload = store.payload {
|
|
switch payload.todayState() {
|
|
case .planned(let today):
|
|
dayHeader(today.title)
|
|
if today.isEmpty {
|
|
EmptyDayView(text: payload.emptyDayText)
|
|
} else {
|
|
ForEach(Array(today.meals.enumerated()), id: \.offset) { _, meal in
|
|
MealCard(meal: meal, emptyText: payload.emptyMealText)
|
|
}
|
|
}
|
|
|
|
case .notPlannedToday(let next):
|
|
// A weekend with weekends turned off isn't a sync
|
|
// problem — say so, and point at what does come next.
|
|
NotPlannedTodayView(text: payload.notPlannedTodayText)
|
|
if let next {
|
|
if let label = payload.nextUpLabel {
|
|
Text(label.uppercased())
|
|
.font(.system(size: 10, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.secondary)
|
|
.padding(.top, 2)
|
|
}
|
|
dayHeader(next.title)
|
|
ForEach(Array(next.meals.enumerated()), id: \.offset) { _, meal in
|
|
MealCard(meal: meal, emptyText: payload.emptyMealText)
|
|
}
|
|
}
|
|
|
|
case .outOfDate:
|
|
StaleView(text: payload.staleText)
|
|
}
|
|
|
|
// When the watch is holding an old snapshot, this is what
|
|
// makes it obvious instead of guessing why meals look wrong.
|
|
LastUpdatedFooter(payload: payload)
|
|
} else {
|
|
EmptySyncView()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func dayHeader(_ title: String) -> some View {
|
|
HStack(alignment: .firstTextBaseline) {
|
|
Text(title)
|
|
.font(.system(.title3, design: .rounded, weight: .bold))
|
|
.foregroundStyle(WatchTheme.coral)
|
|
Spacer()
|
|
Image(systemName: "fork.knife")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(.horizontal, 2)
|
|
}
|
|
}
|
|
|
|
/// Today is deliberately outside the plan (weekends off, typically).
|
|
struct NotPlannedTodayView: View {
|
|
var text: String?
|
|
|
|
var body: some View {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "calendar.badge.minus")
|
|
.font(.system(size: 13))
|
|
.foregroundStyle(WatchTheme.coral)
|
|
if let text {
|
|
Text(text)
|
|
.font(.system(size: 12, weight: .semibold, design: .rounded))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
|
|
private struct LastUpdatedFooter: View {
|
|
let payload: WatchWeekPayload
|
|
|
|
var body: some View {
|
|
HStack(spacing: 3) {
|
|
Image(systemName: "arrow.clockwise")
|
|
// Label comes localized from the iPhone; the date formats itself
|
|
// with the watch's own locale.
|
|
Text("\(payload.updatedLabel ?? "") \(payload.updatedAt.formatted(date: .abbreviated, time: .shortened))")
|
|
}
|
|
.font(.system(size: 9, weight: .medium, design: .rounded))
|
|
.foregroundStyle(.secondary)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.top, 6)
|
|
}
|
|
}
|
|
|
|
struct MealCard: View {
|
|
let meal: WatchWeekPayload.Meal
|
|
/// Localized "nothing planned" coming from the iPhone — the watch bundle
|
|
/// has no strings of its own.
|
|
var emptyText: String?
|
|
|
|
private var accent: Color { WatchTheme.accent(for: meal.type) }
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
HStack(spacing: 5) {
|
|
Image(systemName: WatchWeekPayload.icon(for: meal.type))
|
|
.font(.system(size: 11, weight: .bold))
|
|
Text(meal.label.uppercased())
|
|
.font(.system(size: 11, weight: .bold, design: .rounded))
|
|
.tracking(0.5)
|
|
}
|
|
.foregroundStyle(.white.opacity(0.85))
|
|
|
|
Text(meal.name ?? emptyText ?? "—")
|
|
.font(.system(.body, design: .rounded, weight: .semibold))
|
|
.foregroundStyle(.white.opacity(meal.name == nil ? 0.7 : 1))
|
|
.lineLimit(2)
|
|
.minimumScaleFactor(0.8)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 9)
|
|
.background(
|
|
LinearGradient(
|
|
colors: [accent, accent.opacity(0.65)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
),
|
|
in: RoundedRectangle(cornerRadius: 12)
|
|
)
|
|
}
|
|
}
|
|
|
|
/// The day exists in the snapshot but has no meals planned.
|
|
struct EmptyDayView: View {
|
|
var text: String?
|
|
|
|
var body: some View {
|
|
VStack(spacing: 6) {
|
|
Image(systemName: "moon.zzz.fill")
|
|
.font(.title3)
|
|
.foregroundStyle(WatchTheme.coral)
|
|
if let text {
|
|
Text(text)
|
|
.font(.system(.caption2, design: .rounded, weight: .semibold))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.top, 18)
|
|
}
|
|
}
|
|
|
|
/// A snapshot from another week: the watch has data, but not for today.
|
|
struct StaleView: View {
|
|
var text: String?
|
|
|
|
var body: some View {
|
|
VStack(spacing: 6) {
|
|
Image(systemName: "iphone.gen3.radiowaves.left.and.right")
|
|
.font(.title3)
|
|
.foregroundStyle(WatchTheme.coral)
|
|
if let text {
|
|
Text(text)
|
|
.font(.system(.caption2, design: .rounded, weight: .semibold))
|
|
.multilineTextAlignment(.center)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.top, 18)
|
|
}
|
|
}
|
|
|
|
struct EmptySyncView: View {
|
|
var body: some View {
|
|
VStack(spacing: 8) {
|
|
Image(systemName: "applewatch.radiowaves.left.and.right")
|
|
.font(.title3)
|
|
.foregroundStyle(WatchTheme.coral)
|
|
// Payload arrives fully localized from the iPhone; the watch
|
|
// bundle carries no strings of its own, so this stays neutral.
|
|
Text(verbatim: "MealMood")
|
|
.font(.system(.caption2, design: .rounded, weight: .semibold))
|
|
Image(systemName: "iphone.gen3")
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.top, 24)
|
|
}
|
|
}
|