import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'package:home_widget/home_widget.dart'; import '../state/app/modules/timetable/subject/timetable_subject.dart'; import 'widget_data.dart'; /// Bridge to the native widget host. All keys/names live here so the Kotlin /// and Swift sides stay in sync. class WidgetSync { static const String iosAppGroupId = 'group.eu.mhsl.marianum.mobile.client.widget'; // Must match the WidgetKit `kind` strings declared in // TimetableWidgetExtension.swift — a mismatch makes reloadTimelines a no-op. static const String iosDayWidgetKind = 'TimetableDayWidget'; static const String iosWeekWidgetKind = 'TimetableWeekWidget'; static const String androidDayProvider = 'TimetableDayWidget'; static const String androidWeekProvider = 'TimetableWeekWidget'; // Version suffix lets a schema change invalidate stale snapshots by // bumping the key instead of risking a parse crash. static const String dayDataKey = 'widget_data_day_v1'; // v2: 14-day window + per-day `days` holiday info. static const String weekDataKey = 'widget_data_week_v2'; static const String fetchedAtKey = 'widget_data_fetched_at_v1'; static const String loggedInKey = 'widget_data_logged_in_v1'; // Mirrored into widget storage so the background isolate can read it // without reopening HydratedBloc storage. static const String connectDoubleLessonsKey = 'widget_setting_connect_double_lessons_v1'; static const String themeModeKey = 'widget_setting_theme_mode_v1'; // Mirrors the resolved TimetablePolicy flag so the background isolate // renders tiles like the app does, without bloc storage. The key predates // the policy (it used to mirror "is teacher") and keeps its name so // existing installs stay consistent. static const String showClassInsteadOfTeacherKey = 'widget_setting_is_teacher_v1'; // Mirrored so the background isolate hits the same Marianum-Connect base // URL the in-app settings cubit currently has selected. static const String marianumConnectBaseUrlKey = 'widget_setting_mc_base_url_v1'; // Which plan the widget shows ('own' or 'child:'), so the background // isolate fetches the same subject as the app. static const String subjectKey = 'widget_setting_subject_v1'; static bool _initialised = false; /// Only the primary subjects can back the widget; foreign plans cannot. static String? encodeSubject(TimetableSubject subject) => switch (subject) { OwnTimetable() => 'own', ChildTimetable(:final childId) => 'child:$childId', ElementTimetable() || NoTimetable() => null, }; /// A missing value means an install from before guardian accounts, which /// always showed the own plan. static TimetableSubject? decodeSubject(String? value) { if (value == null || value == 'own') return const OwnTimetable(); if (value.startsWith('child:') && value.length > 'child:'.length) { return ChildTimetable(value.substring('child:'.length)); } return null; } static Future setSubject(TimetableSubject subject) async { await ensureInitialized(); await HomeWidget.saveWidgetData(subjectKey, encodeSubject(subject)); } static Future getSubject() async { await ensureInitialized(); return decodeSubject(await HomeWidget.getWidgetData(subjectKey)); } static Future ensureInitialized() async { if (_initialised) return; await HomeWidget.setAppGroupId(iosAppGroupId); _initialised = true; } static Future writeDayData(WidgetTimetableData data) => _writeData(dayDataKey, data); static Future writeWeekData(WidgetTimetableData data) => _writeData(weekDataKey, data); static Future _writeData(String key, WidgetTimetableData data) async { await ensureInitialized(); await HomeWidget.saveWidgetData(key, jsonEncode(data.toJson())); await HomeWidget.saveWidgetData( fetchedAtKey, data.fetchedAt.toIso8601String(), ); } static Future setLoggedIn(bool loggedIn) => _setBool(loggedInKey, loggedIn); static Future setConnectDoubleLessons(bool value) => _setBool(connectDoubleLessonsKey, value); /// Default `true` matches `default_settings.dart` — fresh install behaves /// like the in-app calendar. static Future getConnectDoubleLessons() => _getBool(connectDoubleLessonsKey, defaultValue: true); static Future setShowClassInsteadOfTeacher(bool value) => _setBool(showClassInsteadOfTeacherKey, value); static Future getShowClassInsteadOfTeacher() => _getBool(showClassInsteadOfTeacherKey, defaultValue: false); static Future _setBool(String key, bool value) async { await ensureInitialized(); await HomeWidget.saveWidgetData(key, value); } static Future _getBool(String key, {required bool defaultValue}) async { await ensureInitialized(); return await HomeWidget.getWidgetData(key) ?? defaultValue; } static Future setThemeMode(String mode) async { await ensureInitialized(); await HomeWidget.saveWidgetData(themeModeKey, mode); } static Future setMarianumConnectBaseUrl(String url) async { await ensureInitialized(); await HomeWidget.saveWidgetData(marianumConnectBaseUrlKey, url); } static Future getMarianumConnectBaseUrl() async { await ensureInitialized(); return HomeWidget.getWidgetData(marianumConnectBaseUrlKey); } static Future getFetchedAt() async { await ensureInitialized(); final raw = await HomeWidget.getWidgetData(fetchedAtKey); return raw == null ? null : DateTime.tryParse(raw); } static Future clear() async { await ensureInitialized(); await HomeWidget.saveWidgetData(dayDataKey, null); await HomeWidget.saveWidgetData(weekDataKey, null); await HomeWidget.saveWidgetData(fetchedAtKey, null); await HomeWidget.saveWidgetData(loggedInKey, false); await HomeWidget.saveWidgetData(subjectKey, null); } static Future triggerUpdate() async { await ensureInitialized(); try { await HomeWidget.updateWidget( androidName: androidDayProvider, iOSName: iosDayWidgetKind, ); await HomeWidget.updateWidget( androidName: androidWeekProvider, iOSName: iosWeekWidgetKind, ); } on Exception catch (e) { log('WidgetSync.triggerUpdate failed: $e'); } } }