reworked home screen widgets to mirror the in-app timetable, hidden below android 12

This commit is contained in:
2026-09-27 14:07:52 +02:00
parent 2aed3d104c
commit 2ca9feb49a
44 changed files with 2401 additions and 1245 deletions
+273 -39
View File
@@ -1,5 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
import 'package:marianum_mobile/api/mhsl/custom_timetable_event/custom_timetable_event.dart';
import 'package:marianum_mobile/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
@@ -33,6 +34,8 @@ McTimetableEntry _lesson({
String? teacherName,
String? teacherOrgname,
String? substText,
String lessonType = 'LESSON',
List<String> classNames = const [],
}) => McTimetableEntry(
id: 1,
date: date,
@@ -50,8 +53,8 @@ McTimetableEntry _lesson({
]
: const [],
rooms: room != null ? [room] : const [],
classNames: const [],
lessonType: 'LESSON',
classNames: classNames,
lessonType: lessonType,
status: status,
substitutionText: substText,
lessonText: null,
@@ -117,8 +120,18 @@ void main() {
test('only includes lessons on the anchor day', () {
final lessons = [
_lesson(date: DateTime(2026, 5, 6), startHhmm: 800, endHhmm: 845, subjectName: 'MA'),
_lesson(date: DateTime(2026, 5, 7), startHhmm: 800, endHhmm: 845, subjectName: 'EN'),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 800,
endHhmm: 845,
subjectName: 'MA',
),
_lesson(
date: DateTime(2026, 5, 7),
startHhmm: 800,
endHhmm: 845,
subjectName: 'EN',
),
];
final data = WidgetDataMapper.buildDayData(
now: now,
@@ -164,14 +177,11 @@ void main() {
rooms: null,
holidays: null,
);
expect(
data.lessons.map((l) => l.status).toList(),
[
WidgetLessonStatus.cancelled,
WidgetLessonStatus.irregular,
WidgetLessonStatus.teacherChanged,
],
);
expect(data.lessons.map((l) => l.status).toList(), [
WidgetLessonStatus.cancelled,
WidgetLessonStatus.irregular,
WidgetLessonStatus.teacherChanged,
]);
});
test('marks day as holiday when in holiday range', () {
@@ -224,10 +234,11 @@ void main() {
rooms: null,
holidays: null,
);
expect(
data.lessons.map((l) => l.subjectShort).toList(),
['MA', 'EN', 'BIO'],
);
expect(data.lessons.map((l) => l.subjectShort).toList(), [
'MA',
'EN',
'BIO',
]);
});
});
@@ -236,9 +247,24 @@ void main() {
test('long event bumps every regular lesson it covers', () {
final lessons = [
_lesson(date: DateTime(2026, 5, 6), startHhmm: 800, endHhmm: 845, subjectName: 'MA'),
_lesson(date: DateTime(2026, 5, 6), startHhmm: 900, endHhmm: 945, subjectName: 'EN'),
_lesson(date: DateTime(2026, 5, 6), startHhmm: 1000, endHhmm: 1045, subjectName: 'BIO'),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 800,
endHhmm: 845,
subjectName: 'MA',
),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 900,
endHhmm: 945,
subjectName: 'EN',
),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 1000,
endHhmm: 1045,
subjectName: 'BIO',
),
];
final events = GetCustomTimetableEventResponse([
_event(
@@ -264,8 +290,18 @@ void main() {
test('event + same-slot duplicate regular: kept lesson shows +2', () {
final lessons = [
_lesson(date: DateTime(2026, 5, 6), startHhmm: 900, endHhmm: 945, subjectName: 'EN'),
_lesson(date: DateTime(2026, 5, 6), startHhmm: 900, endHhmm: 945, subjectName: 'MA'),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 900,
endHhmm: 945,
subjectName: 'EN',
),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 900,
endHhmm: 945,
subjectName: 'MA',
),
];
final events = GetCustomTimetableEventResponse([
_event(
@@ -315,7 +351,12 @@ void main() {
test('two events covering the same regular lesson bump it twice', () {
final lessons = [
_lesson(date: DateTime(2026, 5, 6), startHhmm: 900, endHhmm: 945, subjectName: 'EN'),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 900,
endHhmm: 945,
subjectName: 'EN',
),
];
final events = GetCustomTimetableEventResponse([
_event(
@@ -361,16 +402,198 @@ void main() {
});
});
group('teacher plan', () {
final now = DateTime(2026, 5, 6, 7);
final day = DateTime(2026, 5, 6);
WidgetLesson single(McTimetableEntry lesson) =>
WidgetDataMapper.buildDayData(
now: now,
lessons: [lesson],
subjects: null,
rooms: null,
holidays: null,
showClassInsteadOfTeacher: true,
).lessons.single;
test('shows the classes in place of the teacher', () {
final lesson = single(
_lesson(
date: day,
startHhmm: 755,
endHhmm: 840,
subjectName: 'MA',
teacherName: 'MUE',
teacherOrgname: 'KUN',
classNames: const ['10a', '10b'],
),
);
expect(lesson.teacher, '10a, 10b');
expect(lesson.originalTeacher, isNull);
});
test('leaves the slot empty for entries without a class', () {
final lesson = single(
_lesson(
date: day,
startHhmm: 920,
endHhmm: 935,
room: 'Hof',
teacherName: 'MUE',
lessonType: 'BREAK_SUPERVISION',
),
);
expect(lesson.teacher, isNull);
});
});
group('tile color', () {
final now = DateTime(2026, 5, 6, 7);
final day = DateTime(2026, 5, 6);
WidgetLesson single(
McTimetableEntry lesson, {
TimetableGetSubjectsResponse? subjects,
}) => WidgetDataMapper.buildDayData(
now: now,
lessons: [lesson],
subjects: subjects,
rooms: null,
holidays: null,
).lessons.single;
test('regular lesson without subject color uses the status red', () {
final lesson = single(
_lesson(
date: day,
startHhmm: 755,
endHhmm: 840,
subjectName: 'MA',
teacherName: 'MUE',
),
);
expect(lesson.color, '#993333');
});
test('regular lesson takes the personal subject color', () {
final lesson = single(
_lesson(
date: day,
startHhmm: 755,
endHhmm: 840,
subjectName: 'MA',
teacherName: 'MUE',
),
subjects: TimetableGetSubjectsResponse(
result: [
McSubject(
id: 1,
shortName: 'MA',
longName: 'Mathematik',
color: 'indigo',
),
],
),
);
expect(lesson.color, '#5C6BC0');
});
test('substitution keeps its signal color despite a subject color', () {
final lesson = single(
_lesson(
date: day,
startHhmm: 755,
endHhmm: 840,
status: 'IRREGULAR',
subjectName: 'MA',
teacherName: 'MUE',
),
subjects: TimetableGetSubjectsResponse(
result: [
McSubject(
id: 1,
shortName: 'MA',
longName: 'Mathematik',
color: 'indigo',
),
],
),
);
expect(lesson.status, WidgetLessonStatus.irregular);
expect(lesson.color, '#8F19B3');
});
test('supervision is classified as duty with room in the title', () {
final lesson = single(
_lesson(
date: day,
startHhmm: 920,
endHhmm: 935,
room: 'Hof',
teacherName: 'MUE',
lessonType: 'BREAK_SUPERVISION',
),
);
expect(lesson.status, WidgetLessonStatus.duty);
expect(lesson.subjectShort, 'Hof Aufsicht');
expect(lesson.color, '#00796B');
});
test('custom event resolves legacy color names', () {
final data = WidgetDataMapper.buildDayData(
now: now,
lessons: const [],
subjects: null,
rooms: null,
holidays: null,
customEvents: GetCustomTimetableEventResponse([
_event(
id: 'e',
title: 'Probe',
start: DateTime(2026, 5, 6, 14),
end: DateTime(2026, 5, 6, 15),
),
]),
);
expect(data.lessons.single.color, '#FFA000');
});
});
group('buildWeekData', () {
final now = DateTime(2026, 5, 5, 10); // Tuesday
test('covers the 14-day window including next week', () {
final lessons = [
_lesson(date: DateTime(2026, 5, 4), startHhmm: 800, endHhmm: 845, subjectName: 'MO'),
_lesson(date: DateTime(2026, 5, 6), startHhmm: 800, endHhmm: 845, subjectName: 'WE'),
_lesson(date: DateTime(2026, 5, 8), startHhmm: 800, endHhmm: 845, subjectName: 'FR'),
_lesson(date: DateTime(2026, 5, 11), startHhmm: 800, endHhmm: 845, subjectName: 'NEXT'),
_lesson(date: DateTime(2026, 5, 18), startHhmm: 800, endHhmm: 845, subjectName: 'FAR'),
_lesson(
date: DateTime(2026, 5, 4),
startHhmm: 800,
endHhmm: 845,
subjectName: 'MO',
),
_lesson(
date: DateTime(2026, 5, 6),
startHhmm: 800,
endHhmm: 845,
subjectName: 'WE',
),
_lesson(
date: DateTime(2026, 5, 8),
startHhmm: 800,
endHhmm: 845,
subjectName: 'FR',
),
_lesson(
date: DateTime(2026, 5, 11),
startHhmm: 800,
endHhmm: 845,
subjectName: 'NEXT',
),
_lesson(
date: DateTime(2026, 5, 18),
startHhmm: 800,
endHhmm: 845,
subjectName: 'FAR',
),
];
final data = WidgetDataMapper.buildWeekData(
now: now,
@@ -380,10 +603,12 @@ void main() {
holidays: null,
);
expect(data.anchorDate, DateTime(2026, 5, 4));
expect(
data.lessons.map((l) => l.subjectShort).toList(),
['MO', 'WE', 'FR', 'NEXT'],
);
expect(data.lessons.map((l) => l.subjectShort).toList(), [
'MO',
'WE',
'FR',
'NEXT',
]);
});
test('window stays on the current week when the anchor rolls forward', () {
@@ -391,8 +616,18 @@ void main() {
// still starts at the current week's Monday so today stays available.
final fridayEvening = DateTime(2026, 5, 8, 18);
final lessons = [
_lesson(date: DateTime(2026, 5, 8), startHhmm: 800, endHhmm: 845, subjectName: 'FR'),
_lesson(date: DateTime(2026, 5, 11), startHhmm: 800, endHhmm: 845, subjectName: 'NEXT'),
_lesson(
date: DateTime(2026, 5, 8),
startHhmm: 800,
endHhmm: 845,
subjectName: 'FR',
),
_lesson(
date: DateTime(2026, 5, 11),
startHhmm: 800,
endHhmm: 845,
subjectName: 'NEXT',
),
];
final data = WidgetDataMapper.buildWeekData(
now: fridayEvening,
@@ -402,10 +637,7 @@ void main() {
holidays: null,
);
expect(data.anchorDate, DateTime(2026, 5, 11));
expect(
data.lessons.map((l) => l.subjectShort).toList(),
['FR', 'NEXT'],
);
expect(data.lessons.map((l) => l.subjectShort).toList(), ['FR', 'NEXT']);
});
test('carries per-day holiday info in days', () {
@@ -429,8 +661,10 @@ void main() {
expect(data.days, hasLength(WidgetDataMapper.weekWindowDays));
expect(data.days.first.date, DateTime(2026, 5, 4));
expect(data.days.last.date, DateTime(2026, 5, 17));
final holidayDays =
data.days.where((d) => d.isHoliday).map((d) => d.date).toList();
final holidayDays = data.days
.where((d) => d.isHoliday)
.map((d) => d.date)
.toList();
expect(holidayDays, [DateTime(2026, 5, 14), DateTime(2026, 5, 15)]);
expect(
data.days.firstWhere((d) => d.isHoliday).holidayName,