diff --git a/MealMoodWatchWidget/Info.plist b/MealMoodWatchWidget/Info.plist index cdf3c3e..8d20719 100644 --- a/MealMoodWatchWidget/Info.plist +++ b/MealMoodWatchWidget/Info.plist @@ -17,7 +17,7 @@ CFBundleShortVersionString 2.1.0 CFBundleVersion - 81 + 82 NSExtension NSExtensionPointIdentifier diff --git a/MealMoodWatchWidget/MealMoodWatchWidget.swift b/MealMoodWatchWidget/MealMoodWatchWidget.swift index 6e27d86..4fd58b8 100644 --- a/MealMoodWatchWidget/MealMoodWatchWidget.swift +++ b/MealMoodWatchWidget/MealMoodWatchWidget.swift @@ -8,6 +8,7 @@ import SwiftUI struct MealMoodWatchWidgetBundle: WidgetBundle { var body: some Widget { MealMoodWatchWidget() + MealMoodWatchDaysWidget() } } @@ -83,16 +84,21 @@ struct TodayComplicationView: View { default: VStack(alignment: .leading, spacing: 2) { if let day = entry.day { - ForEach(Array(day.meals.prefix(3).enumerated()), id: \.offset) { _, meal in + // The rectangular slot fits ~3 lines: with fewer meals the + // text scales up so the space isn't wasted. + let meals = Array(day.meals.prefix(3)) + let nameSize: CGFloat = meals.count <= 1 ? 17 : (meals.count == 2 ? 15 : 12) + ForEach(Array(meals.enumerated()), id: \.offset) { _, meal in HStack(spacing: 4) { Image(systemName: WatchWeekPayload.icon(for: meal.type)) - .font(.system(size: 9, weight: .bold)) + .font(.system(size: nameSize - 3, weight: .bold)) .foregroundStyle(accent(for: meal.type)) .widgetAccentable() - .frame(width: 11) + .frame(width: nameSize) Text(meal.name ?? "—") - .font(.system(size: 12, weight: .semibold, design: .rounded)) - .lineLimit(1) + .font(.system(size: nameSize, weight: .semibold, design: .rounded)) + .lineLimit(meals.count <= 1 ? 3 : (meals.count == 2 ? 2 : 1)) + .minimumScaleFactor(0.75) } } } else { @@ -108,6 +114,10 @@ struct TodayComplicationView: View { } private func accent(for mealType: String) -> Color { + Self.accentColor(for: mealType) + } + + static func accentColor(for mealType: String) -> Color { switch mealType { case "breakfast": return Color(red: 0.95, green: 0.68, blue: 0.25) case "lunch": return Color(red: 0.98, green: 0.55, blue: 0.30) @@ -125,3 +135,102 @@ struct TodayComplicationView: View { return meals.first(where: { $0.name != nil }) ?? meals.first } } + +// MARK: - Next days complication + +struct DaysEntry: TimelineEntry { + let date: Date + let days: [WatchWeekPayload.Day] +} + +struct DaysProvider: TimelineProvider { + func placeholder(in context: Context) -> DaysEntry { + DaysEntry(date: Date(), days: sampleDays) + } + + func getSnapshot(in context: Context, completion: @escaping (DaysEntry) -> Void) { + let days = upcomingDays + completion(DaysEntry(date: Date(), days: days.isEmpty ? sampleDays : days)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + let entry = DaysEntry(date: Date(), days: upcomingDays) + let nextMidnight = Calendar.current.startOfDay(for: Date()).addingTimeInterval(86_400 + 300) + completion(Timeline(entries: [entry], policy: .after(nextMidnight))) + } + + /// Today and the next two planned days (falls back to the week's first + /// days when today is outside the planned range, e.g. weekends off). + private var upcomingDays: [WatchWeekPayload.Day] { + guard let payload = WatchWeekPayload.stored() else { return [] } + let todayOffset = (Calendar.current.component(.weekday, from: Date()) + 5) % 7 + let fromToday = payload.days.filter { $0.dayOfWeek >= todayOffset } + let source = fromToday.isEmpty ? payload.days : fromToday + return Array(source.prefix(3)) + } + + private var sampleDays: [WatchWeekPayload.Day] { + [ + .init(dayOfWeek: 0, title: "Lun 8", isToday: true, meals: [ + .init(type: "lunch", label: "Comida", name: "Ensalada"), + .init(type: "dinner", label: "Cena", name: "Salmón")]), + .init(dayOfWeek: 1, title: "Mar 9", isToday: false, meals: [ + .init(type: "dinner", label: "Cena", name: "Lentejas")]), + .init(dayOfWeek: 2, title: "Mié 10", isToday: false, meals: [ + .init(type: "dinner", label: "Cena", name: "Pizza casera")]) + ] + } +} + +struct MealMoodWatchDaysWidget: Widget { + var body: some WidgetConfiguration { + StaticConfiguration(kind: "MealMoodWatchDays", provider: DaysProvider()) { entry in + DaysComplicationView(entry: entry) + .containerBackground(.clear, for: .widget) + } + .configurationDisplayName("MealMood — 3 días") + .supportedFamilies([.accessoryRectangular]) + } +} + +struct DaysComplicationView: View { + let entry: DaysEntry + + var body: some View { + VStack(alignment: .leading, spacing: 1) { + if entry.days.isEmpty { + Text(verbatim: "MealMood") + .font(.system(size: 12, weight: .semibold, design: .rounded)) + Image(systemName: "iphone.and.arrow.forward") + .font(.system(size: 10)) + .foregroundStyle(.secondary) + } else { + ForEach(entry.days, id: \.dayOfWeek) { day in + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(dayAbbrev(day)) + .font(.system(size: 11, weight: .heavy, design: .rounded)) + .foregroundStyle(day.isToday + ? Color(red: 1.0, green: 0.45, blue: 0.35) + : .secondary) + .widgetAccentable() + .frame(width: 28, alignment: .leading) + Text(mealsLine(day)) + .font(.system(size: 12, weight: .medium, design: .rounded)) + .lineLimit(1) + .minimumScaleFactor(0.7) + } + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private func dayAbbrev(_ day: WatchWeekPayload.Day) -> String { + String(day.title.split(separator: " ").first ?? "").uppercased() + } + + private func mealsLine(_ day: WatchWeekPayload.Day) -> String { + let names = day.meals.compactMap(\.name) + return names.isEmpty ? "—" : names.joined(separator: " · ") + } +} diff --git a/MealMoodWidget/Info.plist b/MealMoodWidget/Info.plist index cdf3c3e..8d20719 100644 --- a/MealMoodWidget/Info.plist +++ b/MealMoodWidget/Info.plist @@ -17,7 +17,7 @@ CFBundleShortVersionString 2.1.0 CFBundleVersion - 81 + 82 NSExtension NSExtensionPointIdentifier