added guardian login with views for their assigned childs

This commit is contained in:
2026-09-20 11:11:58 +02:00
parent e4e2b1a4fb
commit 67c935c05b
117 changed files with 4784 additions and 1514 deletions
+53 -22
View File
@@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart';
import 'package:workmanager/workmanager.dart';
import '../api/marianumconnect/marianumconnect_endpoint.dart';
import '../api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_get.dart';
import '../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays.dart';
import '../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart';
import '../api/marianumconnect/queries/timetable_get_rooms/timetable_get_rooms.dart';
@@ -14,11 +15,11 @@ import '../api/marianumconnect/queries/timetable_get_subjects/timetable_get_subj
import '../api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
import '../api/marianumconnect/queries/timetable_get_timegrid/timetable_get_timegrid.dart';
import '../api/marianumconnect/queries/timetable_get_timegrid/timetable_get_timegrid_response.dart';
import '../api/marianumconnect/queries/timetable_get_week/timetable_get_week.dart';
import '../api/mhsl/custom_timetable_event/get/get_custom_timetable_event.dart';
import '../api/mhsl/custom_timetable_event/get/get_custom_timetable_event_params.dart';
import '../api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
import '../model/account_data.dart';
import '../session/session.dart';
import '../session/session_manager.dart';
import '../state/app/modules/timetable/data_provider/timetable_data_provider.dart';
import '../state/app/modules/timetable/subject/timetable_subject.dart';
import '../widget_data/widget_data_mapper.dart';
import '../widget_data/widget_publisher.dart';
import '../widget_data/widget_sync.dart';
@@ -81,17 +82,17 @@ class WidgetBackgroundTask {
/// Throws on fetch failure so the worker path can signal a retry.
static Future<void> runRefreshNow({bool force = false}) async {
await WidgetSync.ensureInitialized();
bool populated;
Session? session;
try {
// Bounded: a hanging keystore read must not stall the caller's budget
// (FCM handler ~25s on iOS) forever.
populated = await AccountData().waitForPopulation().timeout(
session = await SessionManager().waitForLoad().timeout(
const Duration(seconds: 10),
);
} on TimeoutException {
populated = false;
session = null;
}
if (!populated) {
if (session == null) {
// Deliberately does NOT flip the widget to logged-out: a failed or slow
// keychain read (locked iOS device during the 06:00 silent push) is
// indistinguishable from "never logged in" here, and blanking the
@@ -101,11 +102,23 @@ class WidgetBackgroundTask {
return;
}
final fetchedAt = await WidgetSync.getFetchedAt();
if (shouldSkipRefresh(fetchedAt: fetchedAt, now: DateTime.now(), force: force)) {
if (shouldSkipRefresh(
fetchedAt: fetchedAt,
now: DateTime.now(),
force: force,
)) {
log('[widget-refresh] snapshot is fresh, skipping refresh');
return;
}
await _refresh();
final subject = widgetRefreshSubject(
session: session,
stored: await WidgetSync.getSubject(),
);
if (subject == null) {
log('[widget-refresh] no plan selected yet, skipping refresh');
return;
}
await _refresh(subject);
}
static Future<void> cancelAll() async {
@@ -126,6 +139,16 @@ bool shouldSkipRefresh({
return !age.isNegative && age < WidgetBackgroundTask.refreshDebounce;
}
/// The plan the background refresh loads. Guardians only get one once the app
/// has published a child; before that there is nothing sensible to fetch.
TimetableSubject? widgetRefreshSubject({
required Session session,
required TimetableSubject? stored,
}) => switch (session) {
CredentialSession() => const OwnTimetable(),
GuardianSession() => stored is ChildTimetable ? stored : null,
};
@pragma('vm:entry-point')
void _callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
@@ -142,7 +165,7 @@ void _callbackDispatcher() {
});
}
Future<void> _refresh() async {
Future<void> _refresh(TimetableSubject subject) async {
await WidgetSync.ensureInitialized();
// The background isolate doesn't go through main.dart's BlocBuilder, so we
// re-apply the endpoint the foreground last persisted. Without this the
@@ -165,9 +188,11 @@ Future<void> _refresh() async {
// latency is the slowest request, not the sum (matters for the push path's
// hard time budget). Reference-data failures fall through to null in the
// mapper rather than aborting the whole refresh.
final timetableFuture = TimetableGetWeek().run(
final until = weekEndExclusive.subtract(const Duration(days: 1));
final timetableFuture = TimetableDataProvider.fetchWeek(
subject,
from: weekStart,
until: weekEndExclusive.subtract(const Duration(days: 1)),
until: until,
);
final subjectsFuture = _runOrNull<TimetableGetSubjectsResponse>(
() => TimetableGetSubjects().run(),
@@ -181,11 +206,11 @@ Future<void> _refresh() async {
final timegridFuture = _runOrNull<TimetableGetTimegridResponse>(
() => TimetableGetTimegrid().run(),
);
final customEventsFuture = _runOrNull<GetCustomTimetableEventResponse>(
() => GetCustomTimetableEvent(
GetCustomTimetableEventParams(AccountData().getUserSecret()),
).run(),
);
final customEventsFuture = subject.supportsCustomEvents
? _runOrNull<GetCustomTimetableEventResponse>(
() => TimetableCustomEventsGet().run(),
)
: Future<GetCustomTimetableEventResponse?>.value();
final timetable = await timetableFuture;
final subjects = await subjectsFuture;
final rooms = await roomsFuture;
@@ -195,9 +220,9 @@ Future<void> _refresh() async {
final lessons = timetable.entries;
final [connectDouble, isTeacher] = await Future.wait([
final [connectDouble, showClassInsteadOfTeacher] = await Future.wait([
WidgetSync.getConnectDoubleLessons(),
WidgetSync.getIsTeacher(),
WidgetSync.getShowClassInsteadOfTeacher(),
]);
final dayData = WidgetDataMapper.buildDayData(
now: now,
@@ -208,7 +233,7 @@ Future<void> _refresh() async {
timegrid: timegrid,
customEvents: customEvents,
connectDoubleLessons: connectDouble,
showClassInsteadOfTeacher: isTeacher,
showClassInsteadOfTeacher: showClassInsteadOfTeacher,
);
final weekData = WidgetDataMapper.buildWeekData(
now: now,
@@ -219,9 +244,15 @@ Future<void> _refresh() async {
timegrid: timegrid,
customEvents: customEvents,
connectDoubleLessons: connectDouble,
showClassInsteadOfTeacher: isTeacher,
showClassInsteadOfTeacher: showClassInsteadOfTeacher,
);
// The user may have switched the child while the requests ran; writing now
// would show the previous child's plan.
if (await WidgetSync.getSubject() != subject) {
log('[widget-bg] subject changed during refresh, discarding');
return;
}
await WidgetSync.writeDayData(dayData);
await WidgetSync.writeWeekData(weekData);
await WidgetSync.setLoggedIn(true);