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).widgetContainerBackground() } .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).widgetContainerBackground() } .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 { @ViewBuilder func widgetThemeOverride(_ mode: String) -> some View { switch mode { case "light": self.environment(\.colorScheme, .light) case "dark": self.environment(\.colorScheme, .dark) default: self } } /// `.containerBackground(_:for:)` is iOS 17+. Older iOS uses the /// implicit `.background(...)` model and renders fine without it. @ViewBuilder func widgetContainerBackground() -> some View { if #available(iOS 17.0, *) { self.containerBackground(.fill.tertiary, for: .widget) } else { self } } }