173 lines
6.5 KiB
Dart
173 lines
6.5 KiB
Dart
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:<id>'), 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<void> setSubject(TimetableSubject subject) async {
|
|
await ensureInitialized();
|
|
await HomeWidget.saveWidgetData<String>(subjectKey, encodeSubject(subject));
|
|
}
|
|
|
|
static Future<TimetableSubject?> getSubject() async {
|
|
await ensureInitialized();
|
|
return decodeSubject(await HomeWidget.getWidgetData<String>(subjectKey));
|
|
}
|
|
|
|
static Future<void> ensureInitialized() async {
|
|
if (_initialised) return;
|
|
await HomeWidget.setAppGroupId(iosAppGroupId);
|
|
_initialised = true;
|
|
}
|
|
|
|
static Future<void> writeDayData(WidgetTimetableData data) =>
|
|
_writeData(dayDataKey, data);
|
|
|
|
static Future<void> writeWeekData(WidgetTimetableData data) =>
|
|
_writeData(weekDataKey, data);
|
|
|
|
static Future<void> _writeData(String key, WidgetTimetableData data) async {
|
|
await ensureInitialized();
|
|
await HomeWidget.saveWidgetData<String>(key, jsonEncode(data.toJson()));
|
|
await HomeWidget.saveWidgetData<String>(
|
|
fetchedAtKey,
|
|
data.fetchedAt.toIso8601String(),
|
|
);
|
|
}
|
|
|
|
static Future<void> setLoggedIn(bool loggedIn) =>
|
|
_setBool(loggedInKey, loggedIn);
|
|
|
|
static Future<void> setConnectDoubleLessons(bool value) =>
|
|
_setBool(connectDoubleLessonsKey, value);
|
|
|
|
/// Default `true` matches `default_settings.dart` — fresh install behaves
|
|
/// like the in-app calendar.
|
|
static Future<bool> getConnectDoubleLessons() =>
|
|
_getBool(connectDoubleLessonsKey, defaultValue: true);
|
|
|
|
static Future<void> setShowClassInsteadOfTeacher(bool value) =>
|
|
_setBool(showClassInsteadOfTeacherKey, value);
|
|
|
|
static Future<bool> getShowClassInsteadOfTeacher() =>
|
|
_getBool(showClassInsteadOfTeacherKey, defaultValue: false);
|
|
|
|
static Future<void> _setBool(String key, bool value) async {
|
|
await ensureInitialized();
|
|
await HomeWidget.saveWidgetData<bool>(key, value);
|
|
}
|
|
|
|
static Future<bool> _getBool(String key, {required bool defaultValue}) async {
|
|
await ensureInitialized();
|
|
return await HomeWidget.getWidgetData<bool>(key) ?? defaultValue;
|
|
}
|
|
|
|
static Future<void> setThemeMode(String mode) async {
|
|
await ensureInitialized();
|
|
await HomeWidget.saveWidgetData<String>(themeModeKey, mode);
|
|
}
|
|
|
|
static Future<void> setMarianumConnectBaseUrl(String url) async {
|
|
await ensureInitialized();
|
|
await HomeWidget.saveWidgetData<String>(marianumConnectBaseUrlKey, url);
|
|
}
|
|
|
|
static Future<String?> getMarianumConnectBaseUrl() async {
|
|
await ensureInitialized();
|
|
return HomeWidget.getWidgetData<String>(marianumConnectBaseUrlKey);
|
|
}
|
|
|
|
static Future<DateTime?> getFetchedAt() async {
|
|
await ensureInitialized();
|
|
final raw = await HomeWidget.getWidgetData<String>(fetchedAtKey);
|
|
return raw == null ? null : DateTime.tryParse(raw);
|
|
}
|
|
|
|
static Future<void> clear() async {
|
|
await ensureInitialized();
|
|
await HomeWidget.saveWidgetData<String>(dayDataKey, null);
|
|
await HomeWidget.saveWidgetData<String>(weekDataKey, null);
|
|
await HomeWidget.saveWidgetData<String>(fetchedAtKey, null);
|
|
await HomeWidget.saveWidgetData<bool>(loggedInKey, false);
|
|
await HomeWidget.saveWidgetData<String>(subjectKey, null);
|
|
}
|
|
|
|
static Future<void> 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');
|
|
}
|
|
}
|
|
}
|