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:
@@ -2,16 +2,16 @@ import 'dart:developer';
|
||||
|
||||
import 'package:rrule/rrule.dart';
|
||||
|
||||
import '../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart';
|
||||
import '../api/marianumconnect/queries/timetable_get_rooms/timetable_get_rooms_response.dart';
|
||||
import '../api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
|
||||
import '../api/marianumconnect/queries/timetable_get_timegrid/timetable_get_timegrid_response.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/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
|
||||
import '../api/webuntis/queries/get_holidays/get_holidays_response.dart';
|
||||
import '../api/webuntis/queries/get_rooms/get_rooms_response.dart';
|
||||
import '../api/webuntis/queries/get_subjects/get_subjects_response.dart';
|
||||
import '../api/webuntis/queries/get_timegrid_units/get_timegrid_units_response.dart';
|
||||
import '../api/webuntis/queries/get_timetable/get_timetable_response.dart';
|
||||
import '../view/pages/timetable/data/lesson_merger.dart';
|
||||
import '../view/pages/timetable/data/lesson_period_schedule.dart';
|
||||
import '../view/pages/timetable/data/lesson_status.dart';
|
||||
import '../view/pages/timetable/data/webuntis_time.dart';
|
||||
import 'widget_data.dart';
|
||||
|
||||
class WidgetDataMapper {
|
||||
@@ -42,11 +42,11 @@ class WidgetDataMapper {
|
||||
|
||||
static WidgetTimetableData buildDayData({
|
||||
required DateTime now,
|
||||
required Iterable<GetTimetableResponseObject> lessons,
|
||||
required GetSubjectsResponse? subjects,
|
||||
required GetRoomsResponse? rooms,
|
||||
required GetHolidaysResponse? holidays,
|
||||
GetTimegridUnitsResponse? timegrid,
|
||||
required Iterable<McTimetableEntry> lessons,
|
||||
required TimetableGetSubjectsResponse? subjects,
|
||||
required TimetableGetRoomsResponse? rooms,
|
||||
required TimetableGetHolidaysResponse? holidays,
|
||||
TimetableGetTimegridResponse? timegrid,
|
||||
GetCustomTimetableEventResponse? customEvents,
|
||||
bool connectDoubleLessons = true,
|
||||
}) {
|
||||
@@ -56,7 +56,7 @@ class WidgetDataMapper {
|
||||
final dayEnd = anchor.add(const Duration(days: 1));
|
||||
final dayLessons = lessons.where((l) => _onSameDay(l, anchor)).toList();
|
||||
final source = connectDoubleLessons
|
||||
? _mergeAdjacentLessons(dayLessons)
|
||||
? LessonMerger.merge(dayLessons)
|
||||
: dayLessons;
|
||||
final mapped = <WidgetLesson>[
|
||||
...source.map((l) => _mapLesson(l, now, subjects, rooms)),
|
||||
@@ -74,18 +74,18 @@ class WidgetDataMapper {
|
||||
|
||||
static WidgetTimetableData buildWeekData({
|
||||
required DateTime now,
|
||||
required Iterable<GetTimetableResponseObject> lessons,
|
||||
required GetSubjectsResponse? subjects,
|
||||
required GetRoomsResponse? rooms,
|
||||
required GetHolidaysResponse? holidays,
|
||||
GetTimegridUnitsResponse? timegrid,
|
||||
required Iterable<McTimetableEntry> lessons,
|
||||
required TimetableGetSubjectsResponse? subjects,
|
||||
required TimetableGetRoomsResponse? rooms,
|
||||
required TimetableGetHolidaysResponse? holidays,
|
||||
TimetableGetTimegridResponse? timegrid,
|
||||
GetCustomTimetableEventResponse? customEvents,
|
||||
bool connectDoubleLessons = true,
|
||||
}) {
|
||||
final anchor = resolveWeekAnchor(now);
|
||||
final endExclusive = anchor.add(const Duration(days: 5));
|
||||
final weekLessons = lessons.where((l) {
|
||||
final dt = WebuntisTime.parse(l.date, l.startTime);
|
||||
final dt = l.startDateTime;
|
||||
return !dt.isBefore(anchor) && dt.isBefore(endExclusive);
|
||||
}).toList();
|
||||
// Per-day merge: otherwise a 4th-period lesson on Mon would collapse with
|
||||
@@ -192,7 +192,7 @@ class WidgetDataMapper {
|
||||
static const int _smallBreakThresholdMinutes = 5;
|
||||
|
||||
static List<WidgetPeriod> _resolvePeriods(
|
||||
GetTimegridUnitsResponse? timegrid,
|
||||
TimetableGetTimegridResponse? timegrid,
|
||||
) {
|
||||
final schedule =
|
||||
(timegrid != null ? LessonPeriodSchedule.fromApi(timegrid) : null) ??
|
||||
@@ -232,92 +232,52 @@ class WidgetDataMapper {
|
||||
return result;
|
||||
}
|
||||
|
||||
static List<GetTimetableResponseObject> _mergePerDay(
|
||||
List<GetTimetableResponseObject> lessons,
|
||||
) {
|
||||
final byDay = <int, List<GetTimetableResponseObject>>{};
|
||||
// Per-Tag-Merge: ohne diese Gruppierung würde eine letzte Stunde am Montag
|
||||
// mit der ersten Stunde am Dienstag verschmelzen, wenn Fach + Lehrer
|
||||
// identisch sind.
|
||||
static List<McTimetableEntry> _mergePerDay(List<McTimetableEntry> lessons) {
|
||||
final byDay = <String, List<McTimetableEntry>>{};
|
||||
for (final l in lessons) {
|
||||
byDay.putIfAbsent(l.date, () => []).add(l);
|
||||
final key = '${l.date.year}-${l.date.month}-${l.date.day}';
|
||||
byDay.putIfAbsent(key, () => []).add(l);
|
||||
}
|
||||
return [for (final group in byDay.values) ..._mergeAdjacentLessons(group)];
|
||||
}
|
||||
|
||||
/// Mirrors `TimetableAppointmentFactory._mergeAdjacentLessons` so the
|
||||
/// widget shows the same merged blocks the in-app calendar does.
|
||||
static List<GetTimetableResponseObject> _mergeAdjacentLessons(
|
||||
List<GetTimetableResponseObject> input, {
|
||||
Duration maxGap = const Duration(minutes: 5),
|
||||
}) {
|
||||
if (input.isEmpty) return const [];
|
||||
final sorted = [...input]..sort(
|
||||
(a, b) => WebuntisTime.parse(
|
||||
a.date,
|
||||
a.startTime,
|
||||
).compareTo(WebuntisTime.parse(b.date, b.startTime)),
|
||||
);
|
||||
final merged = <GetTimetableResponseObject>[];
|
||||
for (final current in sorted) {
|
||||
if (merged.isNotEmpty && _canMerge(merged.last, current, maxGap)) {
|
||||
merged.last.endTime = current.endTime;
|
||||
} else {
|
||||
merged.add(GetTimetableResponseObject.fromJson(current.toJson()));
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
static bool _canMerge(
|
||||
GetTimetableResponseObject a,
|
||||
GetTimetableResponseObject b,
|
||||
Duration maxGap,
|
||||
) {
|
||||
final aSubject = a.su.firstOrNull?.id;
|
||||
final bSubject = b.su.firstOrNull?.id;
|
||||
if (aSubject == null || bSubject == null || aSubject != bSubject) {
|
||||
return false;
|
||||
}
|
||||
if (a.ro.firstOrNull?.id != b.ro.firstOrNull?.id) return false;
|
||||
if (a.te.firstOrNull?.id != b.te.firstOrNull?.id) return false;
|
||||
if (a.code != b.code) return false;
|
||||
final gap = WebuntisTime.parse(
|
||||
b.date,
|
||||
b.startTime,
|
||||
).difference(WebuntisTime.parse(a.date, a.endTime));
|
||||
return !gap.isNegative && gap <= maxGap;
|
||||
return [for (final group in byDay.values) ...LessonMerger.merge(group)];
|
||||
}
|
||||
|
||||
static WidgetLesson _mapLesson(
|
||||
GetTimetableResponseObject lesson,
|
||||
McTimetableEntry lesson,
|
||||
DateTime now,
|
||||
GetSubjectsResponse? subjects,
|
||||
GetRoomsResponse? rooms,
|
||||
TimetableGetSubjectsResponse? subjects,
|
||||
TimetableGetRoomsResponse? rooms,
|
||||
) {
|
||||
final start = WebuntisTime.parse(lesson.date, lesson.startTime);
|
||||
final end = WebuntisTime.parse(lesson.date, lesson.endTime);
|
||||
final start = lesson.startDateTime;
|
||||
final end = lesson.endDateTime;
|
||||
final status = _mapStatus(
|
||||
LessonStatusClassifier.classify(lesson, start, end, now),
|
||||
);
|
||||
final subject = lesson.su.firstOrNull;
|
||||
final subjectShortRaw = lesson.subjects.firstOrNull?.trim() ?? '';
|
||||
// Webuntis sometimes ships subject-less entries (Wandertag etc.). Fall
|
||||
// back to "Event" so the tile isn't just a dash.
|
||||
final rawSubjectName = subject?.name.trim() ?? '';
|
||||
final subjectShort = rawSubjectName.isEmpty ? 'Event' : rawSubjectName;
|
||||
final subjectShort = subjectShortRaw.isEmpty ? 'Event' : subjectShortRaw;
|
||||
String? subjectLong;
|
||||
if (subjects != null && subject != null) {
|
||||
final found = subjects.result.where((s) => s.id == subject.id).firstOrNull;
|
||||
subjectLong = found?.longName;
|
||||
if (subjects != null && subjectShortRaw.isNotEmpty) {
|
||||
subjectLong = subjects.result
|
||||
.where((s) => s.shortName == subjectShortRaw)
|
||||
.firstOrNull
|
||||
?.longName;
|
||||
}
|
||||
subjectLong ??= subject?.longname;
|
||||
final room = lesson.ro.firstOrNull;
|
||||
var roomName = room?.name;
|
||||
if (rooms != null && room != null) {
|
||||
final resolved =
|
||||
rooms.result.where((r) => r.id == room.id).firstOrNull?.name;
|
||||
roomName = resolved ?? roomName;
|
||||
final roomShort = lesson.rooms.firstOrNull;
|
||||
var roomName = roomShort;
|
||||
if (rooms != null && roomShort != null) {
|
||||
roomName = rooms.result
|
||||
.where((r) => r.shortName == roomShort)
|
||||
.firstOrNull
|
||||
?.shortName ??
|
||||
roomName;
|
||||
}
|
||||
final teacher = lesson.te.firstOrNull;
|
||||
final teacherName = teacher?.id == 0 ? null : teacher?.name;
|
||||
final originalTeacher = teacher?.orgname;
|
||||
final teacher = lesson.teachers.firstOrNull;
|
||||
final teacherName = teacher?.shortName;
|
||||
final originalTeacher = teacher?.originalShortName;
|
||||
return WidgetLesson(
|
||||
start: start,
|
||||
end: end,
|
||||
@@ -340,6 +300,8 @@ class WidgetDataMapper {
|
||||
return WidgetLessonStatus.irregular;
|
||||
case LessonStatus.teacherChanged:
|
||||
return WidgetLessonStatus.teacherChanged;
|
||||
case LessonStatus.duty:
|
||||
return WidgetLessonStatus.duty;
|
||||
case LessonStatus.past:
|
||||
return WidgetLessonStatus.past;
|
||||
case LessonStatus.ongoing:
|
||||
@@ -349,19 +311,22 @@ class WidgetDataMapper {
|
||||
}
|
||||
}
|
||||
|
||||
static bool _onSameDay(GetTimetableResponseObject lesson, DateTime day) {
|
||||
final dt = WebuntisTime.parse(lesson.date, lesson.startTime);
|
||||
return dt.year == day.year && dt.month == day.month && dt.day == day.day;
|
||||
static bool _onSameDay(McTimetableEntry lesson, DateTime day) {
|
||||
return lesson.date.year == day.year &&
|
||||
lesson.date.month == day.month &&
|
||||
lesson.date.day == day.day;
|
||||
}
|
||||
|
||||
static GetHolidaysResponseObject? _findHoliday(
|
||||
static McHoliday? _findHoliday(
|
||||
DateTime day,
|
||||
GetHolidaysResponse? holidays,
|
||||
TimetableGetHolidaysResponse? holidays,
|
||||
) {
|
||||
if (holidays == null) return null;
|
||||
final asInt = WebuntisTime.formatDate(day);
|
||||
final asDay = DateTime(day.year, day.month, day.day);
|
||||
for (final h in holidays.result) {
|
||||
if (asInt >= h.startDate && asInt <= h.endDate) return h;
|
||||
final start = DateTime(h.startDate.year, h.startDate.month, h.startDate.day);
|
||||
final end = DateTime(h.endDate.year, h.endDate.month, h.endDate.day);
|
||||
if (!asDay.isBefore(start) && !asDay.isAfter(end)) return h;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user