import SwiftUI import WidgetKit @main struct MarianumWidgetBundle: WidgetBundle { var body: some Widget { TimetableDayWidget() TimetableWeekWidget() } } // MARK: - Day widget struct TimetableDayWidget: Widget { let kind: String = "TimetableDayWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: TimetableDayProvider()) { entry in TimetableDayView(entry: entry).widgetSurface(entry: entry) } .configurationDisplayName("Marianum · Heute") .description("Stundenplan und Vertretungen für den anstehenden Schultag.") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) } } struct TimetableDayProvider: TimelineProvider { func placeholder(in context: Context) -> TimetableEntry { TimetableEntry.placeholder() } func getSnapshot(in context: Context, completion: @escaping (TimetableEntry) -> Void) { completion(TimetableEntry.current(variant: .day)) } func getTimeline( in context: Context, completion: @escaping (Timeline) -> Void ) { let entry = TimetableEntry.current(variant: .day) // 30 min mirrors the Dart workmanager cadence. iOS treats this as // advisory; the "Stand:" label tells the user when data is stale. let next = Calendar.current.date(byAdding: .minute, value: 30, to: Date()) ?? Date() completion(Timeline(entries: [entry], policy: .after(next))) } } // MARK: - Week widget struct TimetableWeekWidget: Widget { let kind: String = "TimetableWeekWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: TimetableWeekProvider()) { entry in TimetableWeekView(entry: entry).widgetSurface(entry: entry) } .configurationDisplayName("Marianum · Woche") .description("Stundenplan und Vertretungen für die ganze Schulwoche.") .supportedFamilies([.systemMedium, .systemLarge, .systemExtraLarge]) } } struct TimetableWeekProvider: TimelineProvider { func placeholder(in context: Context) -> TimetableEntry { TimetableEntry.placeholder() } func getSnapshot(in context: Context, completion: @escaping (TimetableEntry) -> Void) { completion(TimetableEntry.current(variant: .week)) } func getTimeline( in context: Context, completion: @escaping (Timeline) -> Void ) { let entry = TimetableEntry.current(variant: .week) let next = Calendar.current.date(byAdding: .minute, value: 30, to: Date()) ?? Date() completion(Timeline(entries: [entry], policy: .after(next))) } } // MARK: - Entry enum TimetableVariant { case day, week } struct TimetableEntry: TimelineEntry { let date: Date let variant: TimetableVariant let data: WidgetTimetableData? let isLoggedIn: Bool let themeMode: String static func placeholder() -> TimetableEntry { TimetableEntry( date: Date(), variant: .day, data: nil, isLoggedIn: true, themeMode: "system" ) } static func current(variant: TimetableVariant) -> TimetableEntry { let isLoggedIn = WidgetDataLoader.isLoggedIn() let data = isLoggedIn ? (variant == .day ? WidgetDataLoader.loadDay() : WidgetDataLoader.loadWeek()) : nil return TimetableEntry( date: Date(), variant: variant, data: data, isLoggedIn: isLoggedIn, themeMode: WidgetDataLoader.themeMode() ) } } extension View { /// Applies the user's chosen light/dark override on top of the system /// scheme so the widget honours the in-app theme setting. @ViewBuilder func widgetThemeOverride(_ mode: String) -> some View { switch mode { case "light": self.environment(\.colorScheme, .light) case "dark": self.environment(\.colorScheme, .dark) default: self } } /// Wraps the widget view in the Marianum palette + container background /// so all subviews can read `\.widgetPalette` and so the widget renders /// in our warm off-white / dark-clay instead of system grey. @ViewBuilder func widgetSurface(entry: TimetableEntry) -> some View { WidgetSurface(entry: entry) { self } } } private struct WidgetSurface: View { let entry: TimetableEntry @ViewBuilder let content: () -> Content @Environment(\.colorScheme) private var colorScheme var body: some View { let palette = WidgetPalette.resolve( themeMode: entry.themeMode, colorScheme: colorScheme ) return AnyView( background(content: content(), palette: palette) .environment(\.widgetPalette, palette) .widgetThemeOverride(entry.themeMode) ) } @ViewBuilder private func background(content: C, palette: WidgetPalette) -> some View { if #available(iOS 17.0, *) { content.containerBackground(palette.background, for: .widget) } else { content.background(palette.background) } } }