migrated timetable integration from WebUntis to the MarianumConnect API, implementing a Dio-based client with bearer token authentication, background session validation, and auto-refresh logic.

This commit is contained in:
2026-05-23 17:32:42 +02:00
parent 2858f910c9
commit 93b9929f8f
106 changed files with 2739 additions and 2624 deletions
@@ -2,9 +2,8 @@ import 'dart:developer';
import 'package:intl/intl.dart';
import '../../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
import '../../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
import '../../../../../api/webuntis/queries/get_timetable/get_timetable_response.dart';
import '../../../../../api/webuntis/webuntis_error.dart';
import '../../../../../extensions/date_time.dart';
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
@@ -46,12 +45,8 @@ class TimetableBloc
}
/// Persisted state may carry a stale `startDate`/`endDate` from the user's
/// last view as well as `accessibleStartDate`/`accessibleEndDate` learned
/// from `-7004 no allowed date` errors during scroll. Both must reset on
/// every cold start: otherwise the calendar can mount on a months-old week
/// (e.g. last December's Christmas holidays) or get permanently clamped
/// inside a window Webuntis once refused — even though the server would
/// happily serve the user's current week now.
/// last view. Reset on every cold start so the calendar always mounts on
/// the current week, not on whatever week the user closed the app on.
@override
TimetableState fromStorage(Map<String, dynamic> json) {
final stored = TimetableState.fromJson(json);
@@ -142,55 +137,12 @@ class TimetableBloc
);
if (_lastWeekRequestStart.isAfter(requestStart)) return;
_writeWeekToCache(startDate, week);
} on WebuntisError catch (e) {
if (e.code == _outOfRangeErrorCode) {
_narrowAccessibleRange(startDate, endDate);
// Out-of-range is expected when the user scrolls into territory
// Webuntis doesn't grant access to — surface to UI as a normal
// empty week instead of letting the loadable state escalate it
// into a red error screen.
return;
}
log(
'Webuntis getWeek error: code=${e.code} message="${e.message}" '
'for $startDate$endDate',
);
onError?.call(e);
} catch (e) {
log('getWeek error for $startDate$endDate: $e');
onError?.call(e);
}
}
/// Webuntis returns this for weeks the user has no access to (typically
/// before the active enrolment / after a teacher's planning window).
static const int _outOfRangeErrorCode = -7004;
/// Pulls the calendar's permitted scroll range inward based on a denied
/// week. We don't know the exact cutoff — only that *this* week is out
/// of reach — so we always pick the tighter of the existing bound and
/// the newly discovered one. Pre-now denials shrink the lower bound,
/// post-now denials the upper.
void _narrowAccessibleRange(DateTime startDate, DateTime endDate) {
final now = DateTime.now();
final isPast = endDate.isBefore(now);
add(
Emit((s) {
if (isPast) {
final candidate = endDate.addDays(1);
final current = s.accessibleStartDate;
if (current != null && !candidate.isAfter(current)) return s;
return s.copyWith(accessibleStartDate: candidate);
}
// Treat anything not strictly past as a forward-direction denial,
// including the rare case where startDate == now.
final candidate = startDate.subtractDays(1);
final current = s.accessibleEndDate;
if (current != null && !candidate.isBefore(current)) return s;
return s.copyWith(accessibleEndDate: candidate);
}),
);
}
Future<void> _loadStaticReferenceData({
void Function(Object)? onError,
bool renew = false,
@@ -271,11 +223,11 @@ class TimetableBloc
.catchError((_) {});
}
void _writeWeekToCache(DateTime weekStart, GetTimetableResponse week) {
void _writeWeekToCache(DateTime weekStart, TimetableGetWeekResponse week) {
final key = _weekKeyFormat.format(weekStart);
add(
Emit((s) {
final updated = Map<String, GetTimetableResponse>.of(s.weekCache);
final updated = Map<String, TimetableGetWeekResponse>.of(s.weekCache);
updated[key] = week;
return s.copyWith(weekCache: updated, dataVersion: s.dataVersion + 1);
}),