Files
Client/ios/TimetableWidgetExtension/TimetableWidgetExtension.swift
T
2026-05-13 16:09:43 +02:00

172 lines
5.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.")
// Only .systemLarge: iOS gives us no tall-portrait family, so the
// 162×162 .systemSmall can't fit a readable 11-period day grid,
// and .systemMedium is short-wide which crunches the labels.
// Single useful size beats three crippled ones.
.supportedFamilies([.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<TimetableEntry>) -> 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.")
// .systemMedium is dropped 5 day columns plus a time-label column
// in a 4×2 cell-strip yields per-column widths well below the
// subject-only threshold and the labels in the time sidebar overlap.
.supportedFamilies([.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<TimetableEntry>) -> 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<Content: View>: 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<C: View>(content: C, palette: WidgetPalette) -> some View {
if #available(iOS 17.0, *) {
content.containerBackground(palette.background, for: .widget)
} else {
content.background(palette.background)
}
}
}