|
|
|
@@ -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;
|
|
|
|
|
}
|
|
|
|
|