import WidgetKit import SwiftUI import Foundation // MARK: - Shared data model (mirrors WidgetDataStore in main app) private struct TodayMealData: Codable { let lunch: String? let dinner: String? let lunchLabel: String let dinnerLabel: String let weekdayName: String let updatedAt: Date } private enum WidgetStore { static let appGroupID = "group.com.alexandrevazquez.mealmood" static let key = "mealmood.today_meals" static func read() -> TodayMealData? { guard let defaults = UserDefaults(suiteName: appGroupID), let data = defaults.data(forKey: key) else { return nil } return try? JSONDecoder().decode(TodayMealData.self, from: data) } } // MARK: - Timeline struct TodayMealEntry: TimelineEntry { let date: Date let lunch: String? let dinner: String? let lunchLabel: String let dinnerLabel: String let weekdayName: String } struct Provider: TimelineProvider { func placeholder(in context: Context) -> TodayMealEntry { TodayMealEntry(date: Date(), lunch: "Pasta al pesto", dinner: "Salmón al horno", lunchLabel: "Comida", dinnerLabel: "Cena", weekdayName: "Lunes") } func getSnapshot(in context: Context, completion: @escaping (TodayMealEntry) -> Void) { let data = WidgetStore.read() completion(entry(from: data)) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let data = WidgetStore.read() let nextMidnight = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!) completion(Timeline(entries: [entry(from: data)], policy: .after(nextMidnight))) } private func entry(from data: TodayMealData?) -> TodayMealEntry { TodayMealEntry( date: Date(), lunch: data?.lunch, dinner: data?.dinner, lunchLabel: data?.lunchLabel ?? "Lunch", dinnerLabel: data?.dinnerLabel ?? "Dinner", weekdayName: data?.weekdayName ?? "" ) } } // MARK: - Colors private extension Color { static let mmCoral = Color(red: 0.93, green: 0.42, blue: 0.31) static let mmBgWarm = Color(red: 1.00, green: 0.94, blue: 0.91) static let mmBgMint = Color(red: 0.93, green: 0.98, blue: 0.95) static let mmText = Color(red: 0.13, green: 0.13, blue: 0.13) static let mmSubtext = Color(red: 0.55, green: 0.55, blue: 0.55) static let mmLunchOrange = Color(red: 0.90, green: 0.55, blue: 0.18) static let mmDinnerBlue = Color(red: 0.36, green: 0.48, blue: 0.78) } // MARK: - Small widget private struct SmallWidgetView: View { let entry: TodayMealEntry var body: some View { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 4) { Image(systemName: "fork.knife") .font(.system(size: 10, weight: .bold)) .foregroundColor(.mmCoral) Text("MealMood") .font(.system(size: 11, weight: .bold)) .foregroundColor(.mmCoral) Spacer() } Text(entry.weekdayName) .font(.system(size: 10, weight: .medium)) .foregroundColor(.mmSubtext) .lineLimit(1) .padding(.top, 2) .padding(.bottom, 10) Spacer() mealRow(icon: "sun.max.fill", label: entry.lunchLabel, name: entry.lunch, color: .mmLunchOrange) .padding(.bottom, 8) mealRow(icon: "moon.stars.fill", label: entry.dinnerLabel, name: entry.dinner, color: .mmDinnerBlue) } .padding(12) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .background( LinearGradient(colors: [.mmBgWarm, .mmBgMint], startPoint: .topLeading, endPoint: .bottomTrailing) ) } private func mealRow(icon: String, label: String, name: String?, color: Color) -> some View { HStack(spacing: 8) { Image(systemName: icon) .font(.system(size: 13)) .foregroundColor(color) .frame(width: 18) VStack(alignment: .leading, spacing: 1) { Text(label.uppercased()) .font(.system(size: 8, weight: .semibold)) .foregroundColor(.mmSubtext) Text(name ?? "—") .font(.system(size: 13, weight: .semibold)) .foregroundColor(.mmText) .lineLimit(2) .minimumScaleFactor(0.75) } } } } // MARK: - Medium widget private struct MediumWidgetView: View { let entry: TodayMealEntry var body: some View { HStack(spacing: 0) { VStack(alignment: .leading, spacing: 6) { Image(systemName: "fork.knife") .font(.system(size: 18, weight: .bold)) .foregroundColor(.mmCoral) Spacer() Text(entry.weekdayName) .font(.system(size: 16, weight: .bold)) .foregroundColor(.mmText) .lineLimit(1) .minimumScaleFactor(0.7) Text(Date(), style: .date) .font(.system(size: 11, weight: .medium)) .foregroundColor(.mmSubtext) } .padding(14) .frame(width: 112) .frame(maxHeight: .infinity, alignment: .leading) .background(Color.mmBgWarm) Rectangle() .fill(Color.mmCoral.opacity(0.2)) .frame(width: 1) VStack(alignment: .leading, spacing: 14) { mediumRow(icon: "sun.max.fill", label: entry.lunchLabel, name: entry.lunch, color: .mmLunchOrange) Divider() mediumRow(icon: "moon.stars.fill", label: entry.dinnerLabel, name: entry.dinner, color: .mmDinnerBlue) } .padding(14) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .background(Color.white) } } private func mediumRow(icon: String, label: String, name: String?, color: Color) -> some View { HStack(spacing: 10) { Image(systemName: icon) .font(.system(size: 16)) .foregroundColor(color) .frame(width: 22) VStack(alignment: .leading, spacing: 2) { Text(label.uppercased()) .font(.system(size: 9, weight: .semibold)) .foregroundColor(.mmSubtext) Text(name ?? "—") .font(.system(size: 15, weight: .semibold)) .foregroundColor(.mmText) .lineLimit(2) } } } } // MARK: - Widget entry view struct MealMoodWidgetView: View { var entry: Provider.Entry @Environment(\.widgetFamily) var family var body: some View { switch family { case .systemMedium: MediumWidgetView(entry: entry) default: SmallWidgetView(entry: entry) } } } // MARK: - Widget definition @main struct MealMoodWidget: Widget { let kind = "MealMoodWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in MealMoodWidgetView(entry: entry) .containerBackground(for: .widget) { Color.white } } .configurationDisplayName("MealMood") .description("Today's lunch and dinner at a glance.") .supportedFamilies([.systemSmall, .systemMedium]) } }