merged develop

This commit is contained in:
2026-09-27 14:58:07 +02:00
188 changed files with 7474 additions and 2558 deletions
+80 -6
View File
@@ -1,8 +1,10 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../session/session_manager.dart';
import '../state/app/modules/timetable/bloc/timetable_state.dart';
import '../state/app/modules/timetable/subject/timetable_subject.dart';
import '../storage/settings.dart';
@@ -20,23 +22,75 @@ class WidgetPublisher {
static DateTime widgetNow() =>
kDebugMode ? DateTime.now().add(debugTimeShift) : DateTime.now();
/// Identical snapshots are still re-written after this long, so the stored
/// `fetchedAt` (freshness for the background refresh) doesn't go stale.
static const Duration _republishAfter = Duration(minutes: 10);
static Future<void> _queue = Future.value();
static (bool, String, bool, String?)? _lastFlags;
static String? _lastSignature;
static DateTime? _lastPublishedAt;
/// Forgets what was written last, so the next publish writes again. Called
/// whenever the stored widget data is cleared (sign-out).
static void resetDedupe() {
_lastFlags = null;
_lastSignature = null;
_lastPublishedAt = null;
}
/// Publishes run one at a time: they are fire-and-forget from the bloc
/// stream, and interleaved writes could leave day and week data from
/// different states behind.
/// [epoch] is the session the [state] belongs to; defaults to the current
/// one. Callers that delay the publish must capture it up front.
static Future<void> publishFromBlocState(
TimetableState state, {
required TimetableSubject subject,
Settings? settings,
bool showClassInsteadOfTeacher = false,
int? epoch,
}) {
final sessionEpoch = epoch ?? SessionManager().sessionEpoch;
return _queue = _queue.then(
(_) => _publish(
state,
subject: subject,
settings: settings,
showClassInsteadOfTeacher: showClassInsteadOfTeacher,
epoch: sessionEpoch,
),
);
}
static Future<void> _publish(
TimetableState state, {
required TimetableSubject subject,
required Settings? settings,
required bool showClassInsteadOfTeacher,
required int epoch,
}) async {
if (!SessionManager().isCurrentSession(epoch)) return;
try {
final connectDouble =
settings?.timetableSettings.connectDoubleLessons ?? true;
// Mirror into widget storage so the background isolate sees the same
// values the user just toggled — concurrently, they are independent.
await Future.wait([
WidgetSync.setConnectDoubleLessons(connectDouble),
WidgetSync.setThemeMode(_themeName(settings?.appTheme)),
WidgetSync.setShowClassInsteadOfTeacher(showClassInsteadOfTeacher),
WidgetSync.setSubject(subject),
]);
final flags = (
connectDouble,
_themeName(settings?.appTheme),
showClassInsteadOfTeacher,
WidgetSync.encodeSubject(subject),
);
if (flags != _lastFlags) {
await Future.wait([
WidgetSync.setConnectDoubleLessons(flags.$1),
WidgetSync.setThemeMode(flags.$2),
WidgetSync.setShowClassInsteadOfTeacher(flags.$3),
WidgetSync.setSubject(subject),
]);
_lastFlags = flags;
}
final lessons = state.getAllKnownLessons();
final now = widgetNow();
final dayData = WidgetDataMapper.buildDayData(
@@ -61,6 +115,23 @@ class WidgetPublisher {
connectDoubleLessons: connectDouble,
showClassInsteadOfTeacher: showClassInsteadOfTeacher,
);
// A publish still running at sign-out would put the previous account's
// plan back onto the just cleared widget.
if (!SessionManager().isCurrentSession(epoch)) return;
// Most bloc emits (week swipes, prefetches) don't touch the widget's
// window; skip the SharedPreferences commits and widget re-render then.
final signature = jsonEncode([
_withoutFetchedAt(dayData.toJson()),
_withoutFetchedAt(weekData.toJson()),
]);
final lastAt = _lastPublishedAt;
if (signature == _lastSignature &&
lastAt != null &&
now.difference(lastAt) < _republishAfter) {
return;
}
_lastSignature = signature;
_lastPublishedAt = now;
await WidgetSync.writeDayData(dayData);
await WidgetSync.writeWeekData(weekData);
await WidgetSync.setLoggedIn(true);
@@ -72,6 +143,9 @@ class WidgetPublisher {
}
}
static Map<String, Object?> _withoutFetchedAt(Map<String, Object?> json) =>
Map.of(json)..remove('fetchedAt');
static String _themeName(ThemeMode? mode) {
switch (mode) {
case ThemeMode.light: