129 lines
4.2 KiB
Swift
129 lines
4.2 KiB
Swift
import Foundation
|
|
|
|
/// Mirrors lib/widget_data/widget_data.dart. JSON keys must stay in sync —
|
|
/// the bridge is one-way: Dart writes, Swift reads.
|
|
enum WidgetLessonStatus: String, Codable {
|
|
case regular
|
|
case ongoing
|
|
case past
|
|
case cancelled
|
|
case irregular
|
|
case teacherChanged
|
|
case event
|
|
}
|
|
|
|
struct WidgetLesson: Codable {
|
|
let start: Date
|
|
let end: Date
|
|
let subjectShort: String
|
|
let subjectLong: String?
|
|
let room: String?
|
|
let teacher: String?
|
|
let originalTeacher: String?
|
|
let status: WidgetLessonStatus
|
|
let customColor: String?
|
|
let siblingCount: Int?
|
|
}
|
|
|
|
struct WidgetPeriod: Codable {
|
|
let name: String
|
|
let startMinutes: Int
|
|
let endMinutes: Int
|
|
let virtualStartMinutes: Int
|
|
let virtualEndMinutes: Int
|
|
}
|
|
|
|
struct WidgetTimetableData: Codable {
|
|
let fetchedAt: Date
|
|
let anchorDate: Date
|
|
let lessons: [WidgetLesson]
|
|
let periods: [WidgetPeriod]
|
|
let isHoliday: Bool
|
|
let holidayName: String?
|
|
}
|
|
|
|
enum WidgetDataKey {
|
|
static let appGroupId = "group.eu.mhsl.marianum.mobile.client.widget"
|
|
static let dayData = "widget_data_day_v1"
|
|
static let weekData = "widget_data_week_v1"
|
|
static let loggedIn = "widget_data_logged_in_v1"
|
|
static let themeMode = "widget_setting_theme_mode_v1"
|
|
}
|
|
|
|
enum WidgetDataLoader {
|
|
/// Dart's `DateTime.toIso8601String()` on a non-UTC DateTime drops the
|
|
/// trailing Z and ships local wall-clock time. ISO8601DateFormatter's
|
|
/// default treats that as UTC and shifts every lesson by the local TZ
|
|
/// offset — dispatch by suffix instead, mirroring WidgetDataParser.kt.
|
|
private static func parseDartDate(_ raw: String) -> Date? {
|
|
let hasTzSuffix = raw.hasSuffix("Z")
|
|
|| raw.range(of: #"[+-]\d{2}:?\d{2}$"#, options: .regularExpression) != nil
|
|
if hasTzSuffix {
|
|
let iso = ISO8601DateFormatter()
|
|
iso.formatOptions = [.withFullDate, .withFullTime, .withFractionalSeconds]
|
|
if let d = iso.date(from: raw) { return d }
|
|
iso.formatOptions = [.withFullDate, .withFullTime]
|
|
return iso.date(from: raw)
|
|
}
|
|
for pattern in [
|
|
"yyyy-MM-dd'T'HH:mm:ss.SSSSSS",
|
|
"yyyy-MM-dd'T'HH:mm:ss.SSS",
|
|
"yyyy-MM-dd'T'HH:mm:ss",
|
|
] {
|
|
let f = DateFormatter()
|
|
f.dateFormat = pattern
|
|
f.timeZone = TimeZone.current
|
|
f.locale = Locale(identifier: "en_US_POSIX")
|
|
if let d = f.date(from: raw) { return d }
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static func decoder() -> JSONDecoder {
|
|
let dec = JSONDecoder()
|
|
dec.dateDecodingStrategy = .custom { decoder in
|
|
let container = try decoder.singleValueContainer()
|
|
let raw = try container.decode(String.self)
|
|
if let d = parseDartDate(raw) { return d }
|
|
throw DecodingError.dataCorruptedError(
|
|
in: container,
|
|
debugDescription: "Unparseable date: \(raw)"
|
|
)
|
|
}
|
|
return dec
|
|
}
|
|
|
|
static func loadDay() -> WidgetTimetableData? {
|
|
load(key: WidgetDataKey.dayData)
|
|
}
|
|
|
|
static func loadWeek() -> WidgetTimetableData? {
|
|
load(key: WidgetDataKey.weekData)
|
|
}
|
|
|
|
static func isLoggedIn() -> Bool {
|
|
let defaults = UserDefaults(suiteName: WidgetDataKey.appGroupId)
|
|
return defaults?.bool(forKey: WidgetDataKey.loggedIn) ?? false
|
|
}
|
|
|
|
/// "light" / "dark" / "system". The view's `.environment(\.colorScheme)`
|
|
/// reads this so the App's theme choice wins over the OS-level setting.
|
|
static func themeMode() -> String {
|
|
let defaults = UserDefaults(suiteName: WidgetDataKey.appGroupId)
|
|
return defaults?.string(forKey: WidgetDataKey.themeMode) ?? "system"
|
|
}
|
|
|
|
private static func load(key: String) -> WidgetTimetableData? {
|
|
guard let defaults = UserDefaults(suiteName: WidgetDataKey.appGroupId),
|
|
let raw = defaults.string(forKey: key),
|
|
let data = raw.data(using: .utf8) else {
|
|
return nil
|
|
}
|
|
do {
|
|
return try decoder().decode(WidgetTimetableData.self, from: data)
|
|
} catch {
|
|
return nil
|
|
}
|
|
}
|
|
}
|