implemented a comprehensive client-side demo mode triggered by a "demo@" username prefix, serving curated fixture data for all app modules

This commit is contained in:
2026-07-07 20:24:16 +02:00
parent 2668b111f1
commit 9b74c9fd81
45 changed files with 1409 additions and 43 deletions
+9
View File
@@ -0,0 +1,9 @@
import '../../marianumconnect/queries/get_breakers/get_breakers_response.dart';
/// Demo fixtures for the app-maintenance breaker.
class DemoBreaker {
const DemoBreaker._();
/// No active rules — the demo app is never blocked.
static GetBreakersResponse none() => GetBreakersResponse([]);
}
+33
View File
@@ -0,0 +1,33 @@
import '../../../state/app/modules/capabilities/bloc/capabilities_state.dart';
import '../../../state/app/modules/nextcloud_capabilities/bloc/nextcloud_capabilities_state.dart';
/// Demo fixtures for the mobile capability flags — everything granted so the
/// demo persona sees every feature (incl. push) as available.
class DemoCapabilities {
const DemoCapabilities._();
static CapabilitiesState state() => const CapabilitiesState(
viewForeignTimetables: true,
pushNotifications: true,
loaded: true,
);
}
/// Demo fixtures for the Nextcloud `files_sharing` capabilities — a permissive
/// "everything enabled, nothing enforced" server profile.
class DemoNextcloudCapabilities {
const DemoNextcloudCapabilities._();
static NextcloudCapabilitiesState state() => const NextcloudCapabilitiesState(
apiEnabled: true,
publicEnabled: true,
publicMultipleLinks: true,
publicUploadEnabled: true,
publicPasswordEnforced: false,
publicExpireEnabled: false,
publicExpireEnforced: false,
groupEnabled: true,
resharing: true,
loaded: true,
);
}
+155
View File
@@ -0,0 +1,155 @@
import '../../marianumcloud/webdav/queries/list_files/cacheable_file.dart';
import '../../marianumcloud/webdav/queries/list_files/list_files_response.dart';
/// Curated WebDAV listings for the demo mode. Everything is built on
/// [CacheableFile] (the app-level model the file UI renders) so no `nextcloud`
/// DAV objects need to be faked and no network is ever touched.
///
/// `hasPreview`/`fileId` are left null on purpose: without them the file list
/// won't try to fetch server-side thumbnails for the fixtures.
class DemoFiles {
const DemoFiles._();
/// Returns the listing for [path] as passed to `FilesDataProvider.listFiles`
/// (`/` or empty for the root, otherwise the joined folder display names).
/// Unknown folders resolve to an empty listing rather than an error.
static ListFilesResponse listing(String path) {
final key = path.replaceAll('/', '').trim();
switch (key) {
case '':
return _root();
case 'Hausaufgaben':
return _hausaufgaben();
case 'Arbeitsblätter':
return _arbeitsblaetter();
case 'Klausuren':
return _klausuren();
case 'Schuljahr 2025-26':
return _schuljahr();
default:
return ListFilesResponse({});
}
}
static ListFilesResponse _root() => ListFilesResponse({
_folder('Hausaufgaben', daysAgo: 1),
_folder('Arbeitsblätter', daysAgo: 3),
_folder('Klausuren', daysAgo: 12),
_folder('Schuljahr 2025-26', daysAgo: 30),
_file('Deutsch Lektüre.pdf', size: 2_480_000, daysAgo: 2),
_file('Mathe Formelsammlung.pdf', size: 890_000, daysAgo: 9),
_file('Bio Referat.pptx', size: 5_120_000, daysAgo: 5),
_file('Stundenplan.pdf', size: 145_000, daysAgo: 14),
});
static ListFilesResponse _hausaufgaben() => ListFilesResponse({
_file(
'Deutsch Aufsatz.docx',
parent: 'Hausaufgaben',
size: 42_000,
daysAgo: 1,
),
_file(
'Mathe Übungen KW28.pdf',
parent: 'Hausaufgaben',
size: 310_000,
daysAgo: 2,
),
_file(
'Englisch Vokabeln.pdf',
parent: 'Hausaufgaben',
size: 96_000,
daysAgo: 4,
),
});
static ListFilesResponse _arbeitsblaetter() => ListFilesResponse({
_file(
'Physik Optik.pdf',
parent: 'Arbeitsblätter',
size: 720_000,
daysAgo: 6,
),
_file(
'Geschichte Weimar.pdf',
parent: 'Arbeitsblätter',
size: 540_000,
daysAgo: 8,
),
});
static ListFilesResponse _klausuren() => ListFilesResponse({
_file(
'Mathe Klausur 1.pdf',
parent: 'Klausuren',
size: 630_000,
daysAgo: 20,
),
_file(
'Deutsch Klausur 1.pdf',
parent: 'Klausuren',
size: 580_000,
daysAgo: 34,
),
});
static ListFilesResponse _schuljahr() => ListFilesResponse({
_folder('Halbjahr 1', parent: 'Schuljahr 2025-26', daysAgo: 40),
_file(
'Klassenliste 10b.pdf',
parent: 'Schuljahr 2025-26',
size: 78_000,
daysAgo: 60,
),
});
static CacheableFile _folder(
String name, {
String? parent,
required int daysAgo,
}) {
final base = parent == null ? '' : '/$parent';
return CacheableFile(
path: '$base/$name/',
isDirectory: true,
name: name,
modifiedAt: _relative(daysAgo),
createdAt: _relative(daysAgo + 30),
);
}
static CacheableFile _file(
String name, {
String? parent,
required int size,
required int daysAgo,
}) {
final base = parent == null ? '' : '/$parent';
return CacheableFile(
path: '$base/$name',
isDirectory: false,
name: name,
mimeType: _mimeFor(name),
size: size,
modifiedAt: _relative(daysAgo),
createdAt: _relative(daysAgo + 30),
);
}
static DateTime _relative(int daysAgo) =>
DateTime.now().subtract(Duration(days: daysAgo, hours: 3));
static String _mimeFor(String name) {
final ext = name.split('.').last.toLowerCase();
switch (ext) {
case 'pdf':
return 'application/pdf';
case 'pptx':
return 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
case 'docx':
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
default:
return 'application/octet-stream';
}
}
}
+34
View File
@@ -0,0 +1,34 @@
import '../../marianumconnect/models/mc_holiday.dart';
/// Demo fixtures for the holiday calendar — a couple of upcoming breaks,
/// positioned relative to "now" so they always read as still ahead.
class DemoHolidays {
const DemoHolidays._();
static List<McHoliday> upcoming() {
final today = DateTime.now();
DateTime day(int offset) =>
DateTime(today.year, today.month, today.day).add(Duration(days: offset));
return [
McHoliday(
shortName: 'Herbst',
longName: 'Herbstferien',
startDate: day(28),
endDate: day(42),
),
McHoliday(
shortName: 'Weihnachten',
longName: 'Weihnachtsferien',
startDate: day(90),
endDate: day(104),
),
McHoliday(
shortName: 'Ostern',
longName: 'Osterferien',
startDate: day(180),
endDate: day(194),
),
];
}
}
@@ -0,0 +1,54 @@
import '../../../state/app/modules/marianum_dates/bloc/marianum_dates_state.dart';
import '../demo_persona.dart';
/// Demo fixtures for the school calendar — a handful of upcoming events built
/// relative to "now" and consistent with the demo persona (class 10b).
class DemoMarianumDates {
const DemoMarianumDates._();
static List<MarianumDate> events() {
final today = DateTime.now();
DateTime at(int offset, [int hour = 0, int minute = 0]) => DateTime(
today.year,
today.month,
today.day,
hour,
minute,
).add(Duration(days: offset));
return [
MarianumDate(
uid: 'demo-klausur-mathe',
title: 'Klausur Mathematik ${DemoPersona.className}',
description: 'Erste Klausur bei Herr Müller. Themen: Analysis.',
start: at(4, 9, 45),
end: at(4, 11, 15),
isAllDay: false,
),
MarianumDate(
uid: 'demo-wandertag',
title: 'Wandertag',
description: 'Gemeinsamer Ausflug der Jahrgangsstufe 10.',
start: at(11),
end: at(12),
isAllDay: true,
),
MarianumDate(
uid: 'demo-elternsprechtag',
title: 'Elternsprechtag',
description: 'Anmeldung zu den Gesprächen über das Sekretariat.',
start: at(18, 14, 0),
end: at(18, 19, 0),
isAllDay: false,
),
MarianumDate(
uid: 'demo-zeugniskonferenz',
title: 'Zeugniskonferenz',
description: 'Unterrichtsfrei ab der 4. Stunde.',
start: at(32),
end: at(33),
isAllDay: true,
),
];
}
}
@@ -0,0 +1,24 @@
import '../../../state/app/modules/marianum_message/bloc/marianum_message_state.dart';
/// Demo fixtures for the info messages — a single friendly welcome entry so the
/// section is populated rather than empty.
class DemoMarianumMessage {
const DemoMarianumMessage._();
static MarianumMessageList list() {
final today = DateTime.now();
final date =
'${today.day.toString().padLeft(2, '0')}.${today.month.toString().padLeft(2, '0')}.${today.year}';
return MarianumMessageList(
base: 'https://marianum-fulda.de',
messages: [
MarianumMessage(
name: 'Willkommen in der Marianum-App',
date: date,
url: 'https://marianum-fulda.de',
),
],
);
}
}
+429
View File
@@ -0,0 +1,429 @@
import '../../../model/account_data.dart';
import '../../marianumcloud/talk/chat/get_chat_response.dart';
import '../../marianumcloud/talk/room/get_room_response.dart';
import '../demo_persona.dart';
/// Fixtures for the Talk module while [DemoMode] is active: a curated set of
/// rooms plus a believable German conversation per room. Room tokens are the
/// join between [rooms] and [chat] — they must stay in sync.
class DemoTalk {
const DemoTalk._();
static const String tokenClass = 'demo-klasse-10b';
static const String tokenMathLk = 'demo-mathe-lk';
static const String tokenWeber = 'demo-herr-weber';
static const String tokenWeimar = 'demo-weimar';
static const String tokenSv = 'demo-sv';
static GetRoomResponse rooms() {
return GetRoomResponse({
_room(
id: 100,
token: tokenClass,
type: GetRoomResponseObjectConversationType.group,
name: 'Klasse 10b',
displayName: 'Klasse ${DemoPersona.className}',
lastMessage: _lastOf(tokenClass),
unreadMessages: 2,
isFavorite: true,
),
_room(
id: 200,
token: tokenMathLk,
type: GetRoomResponseObjectConversationType.group,
name: 'Mathe-LK',
displayName: 'Mathe-LK · Herr Müller',
lastMessage: _lastOf(tokenMathLk),
),
_room(
id: 300,
token: tokenWeber,
type: GetRoomResponseObjectConversationType.oneToOne,
name: 'weber',
displayName: 'Herr Weber',
lastMessage: _lastOf(tokenWeber),
unreadMessages: 1,
),
_room(
id: 400,
token: tokenWeimar,
type: GetRoomResponseObjectConversationType.group,
name: 'Fahrt nach Weimar',
displayName: 'Fahrt nach Weimar 🚌',
lastMessage: _lastOf(tokenWeimar),
),
_room(
id: 500,
token: tokenSv,
type: GetRoomResponseObjectConversationType.group,
name: 'SV',
displayName: 'SV Schülervertretung',
lastMessage: _lastOf(tokenSv),
),
});
}
static GetChatResponse chat(String token) =>
GetChatResponse(_messages(token).toSet());
static GetChatResponseObject _lastOf(String token) => _messages(token).last;
static List<GetChatResponseObject> _messages(String token) {
switch (token) {
case tokenClass:
return _classChat();
case tokenMathLk:
return _mathLkChat();
case tokenWeber:
return _weberChat();
case tokenWeimar:
return _weimarChat();
case tokenSv:
return _svChat();
default:
return _genericChat(token);
}
}
static List<GetChatResponseObject> _classChat() {
const token = tokenClass;
var base = 10000;
return [
_msg(
id: base + 1,
token: token,
actor: 'sophie',
display: 'Sophie',
ago: const Duration(days: 1, hours: 4),
message: 'Weiß jemand, was wir in Bio auf haben?',
),
_msg(
id: base + 2,
token: token,
actor: 'max',
display: 'Max',
ago: const Duration(days: 1, hours: 3, minutes: 55),
message: 'S. 42 Nr. 3 und 4, glaube ich',
),
_selfMsg(
id: base + 3,
token: token,
ago: const Duration(days: 1, hours: 3, minutes: 50),
message: 'Ja genau, und das Arbeitsblatt zur Zellteilung 🙌',
reactions: {'👍': 3},
),
_msg(
id: base + 4,
token: token,
actor: 'jonas',
display: 'Jonas',
ago: const Duration(days: 1, hours: 3, minutes: 40),
message: 'Danke euch! 🙏',
),
_msg(
id: base + 5,
token: token,
actor: 'emma',
display: 'Emma',
ago: const Duration(hours: 20),
message: 'Ist die Mathe-Arbeit jetzt am Donnerstag oder Freitag?',
),
_msg(
id: base + 6,
token: token,
actor: 'mueller',
display: 'Herr Müller',
ago: const Duration(hours: 19, minutes: 30),
message: 'Die Arbeit findet am Freitag in der 3. Stunde statt.',
),
_msg(
id: base + 7,
token: token,
actor: 'emma',
display: 'Emma',
ago: const Duration(hours: 19, minutes: 20),
message: 'Danke, Herr Müller!',
),
_msg(
id: base + 8,
token: token,
actor: 'paul',
display: 'Paul',
ago: const Duration(hours: 6),
message: 'Wer kommt morgen mit in die Mensa?',
),
_msg(
id: base + 9,
token: token,
actor: 'mia',
display: 'Mia',
ago: const Duration(hours: 5, minutes: 50),
message: 'Ich bin dabei 🍕',
),
_selfMsg(
id: base + 10,
token: token,
ago: const Duration(hours: 5, minutes: 45),
message: 'Ich auch!',
),
_msg(
id: base + 11,
token: token,
actor: 'sophie',
display: 'Sophie',
ago: const Duration(hours: 3),
message:
'Vergesst nicht, die Einverständniserklärung für Weimar abzugeben 🚌',
reactions: {'👍': 5, '🚌': 2},
),
_msg(
id: base + 12,
token: token,
actor: 'ben',
display: 'Ben',
ago: const Duration(hours: 2, minutes: 10),
message: 'Oh stimmt, hab ich fast vergessen 😅',
),
];
}
static List<GetChatResponseObject> _mathLkChat() {
const token = tokenMathLk;
var base = 20000;
return [
_msg(
id: base + 1,
token: token,
actor: 'mueller',
display: 'Herr Müller',
ago: const Duration(days: 2, hours: 2),
message:
'Zur Vorbereitung auf die Arbeit findet ihr die Übungsblätter in den Dateien.',
),
_selfMsg(
id: base + 2,
token: token,
ago: const Duration(days: 1, hours: 6),
message: 'Danke! Können wir Aufgabe 5 nächste Stunde nochmal besprechen?',
),
_msg(
id: base + 3,
token: token,
actor: 'mueller',
display: 'Herr Müller',
ago: const Duration(days: 1, hours: 5, minutes: 30),
message: 'Klar, wir schauen uns die Kurvendiskussion gemeinsam an.',
reactions: {'👍': 4},
),
_msg(
id: base + 4,
token: token,
actor: 'jonas',
display: 'Jonas',
ago: const Duration(hours: 8),
message: 'Super, danke!',
),
];
}
static List<GetChatResponseObject> _weberChat() {
const token = tokenWeber;
var base = 30000;
return [
_selfMsg(
id: base + 1,
token: token,
ago: const Duration(days: 1, hours: 2),
message:
'Guten Tag Herr Weber, ich war letzte Woche krank. Was muss ich in Englisch nachholen?',
),
_msg(
id: base + 2,
token: token,
actor: 'weber',
display: 'Herr Weber',
ago: const Duration(hours: 4),
message:
'Hallo Lena, gute Besserung! Lies bitte Unit 4 und bereite die Vokabeln vor.',
),
_msg(
id: base + 3,
token: token,
actor: 'weber',
display: 'Herr Weber',
ago: const Duration(hours: 3, minutes: 58),
message: 'Die Präsentation kannst du nächste Woche nachholen.',
),
];
}
static List<GetChatResponseObject> _weimarChat() {
const token = tokenWeimar;
var base = 40000;
return [
_msg(
id: base + 1,
token: token,
actor: 'wagner',
display: 'Frau Wagner',
ago: const Duration(days: 3),
message:
'Wir treffen uns am Montag um 7:45 Uhr am Haupteingang. Bitte pünktlich sein!',
reactions: {'👍': 8},
),
_msg(
id: base + 2,
token: token,
actor: 'paul',
display: 'Paul',
ago: const Duration(days: 2, hours: 20),
message: 'Wie lange sind wir ungefähr unterwegs?',
),
_msg(
id: base + 3,
token: token,
actor: 'wagner',
display: 'Frau Wagner',
ago: const Duration(days: 2, hours: 19),
message: 'Etwa zwei Stunden mit dem Bus. Denkt an Verpflegung!',
),
_selfMsg(
id: base + 4,
token: token,
ago: const Duration(hours: 12),
message: 'Freue mich schon! 🎉',
reactions: {'🎉': 6},
),
];
}
static List<GetChatResponseObject> _svChat() {
const token = tokenSv;
var base = 50000;
return [
_msg(
id: base + 1,
token: token,
actor: 'ben',
display: 'Ben',
ago: const Duration(days: 2, hours: 5),
message: 'Nächste SV-Sitzung ist am Mittwoch in der großen Pause.',
),
_msg(
id: base + 2,
token: token,
actor: 'mia',
display: 'Mia',
ago: const Duration(days: 1, hours: 8),
message: 'Können wir über den Ablauf vom Sommerfest sprechen?',
),
_selfMsg(
id: base + 3,
token: token,
ago: const Duration(hours: 9),
message: 'Gute Idee, ich sammle schon mal Vorschläge.',
reactions: {'👍': 3},
),
];
}
static List<GetChatResponseObject> _genericChat(String token) {
return [
_msg(
id: 90001,
token: token,
actor: 'info',
display: 'MarianumConnect',
ago: const Duration(hours: 1),
message: 'Willkommen im Demo-Chat 👋',
),
];
}
static GetChatResponseObject _selfMsg({
required int id,
required String token,
required Duration ago,
required String message,
Map<String, int>? reactions,
}) => _msg(
id: id,
token: token,
actor: AccountData().getUsername(),
display: DemoPersona.studentName,
ago: ago,
message: message,
reactions: reactions,
);
static GetChatResponseObject _msg({
required int id,
required String token,
required String actor,
required String display,
required Duration ago,
required String message,
Map<String, int>? reactions,
}) => GetChatResponseObject(
id,
token,
GetRoomResponseObjectMessageActorType.user,
actor,
display,
_epoch(ago),
'',
GetRoomResponseObjectMessageType.comment,
true,
'demo-ref-$id',
message,
null,
reactions,
null,
null,
);
static GetRoomResponseObject _room({
required int id,
required String token,
required GetRoomResponseObjectConversationType type,
required String name,
required String displayName,
required GetChatResponseObject lastMessage,
int unreadMessages = 0,
bool isFavorite = false,
}) => GetRoomResponseObject(
id,
token,
type,
name,
displayName,
'',
3,
0,
0,
0,
0,
'demo-session-$id',
false,
false,
0,
false,
false,
true,
lastMessage.timestamp,
isFavorite,
GetRoomResponseObjectParticipantNotificationLevel.defaultLevel,
unreadMessages,
false,
false,
lastMessage.id - unreadMessages,
lastMessage.id,
lastMessage,
null,
null,
null,
);
static int _epoch(Duration ago) =>
DateTime.now().subtract(ago).millisecondsSinceEpoch ~/ 1000;
}
+202
View File
@@ -0,0 +1,202 @@
import '../../marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart';
import '../../marianumconnect/queries/timetable_get_rooms/timetable_get_rooms_response.dart';
import '../../marianumconnect/queries/timetable_get_schoolyear/timetable_get_schoolyear_response.dart';
import '../../marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
import '../../marianumconnect/queries/timetable_get_timegrid/timetable_get_timegrid_response.dart';
import '../../marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
import '../../mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
import '../demo_persona.dart';
/// Curated timetable fixtures for the demo mode. Everything is derived from
/// [DemoPersona] so subjects, teachers and rooms line up with the rest of the
/// demo app, and every week is generated relative to the requested range so
/// navigating forward/backward always lands on a populated 10b schedule.
class DemoTimetable {
const DemoTimetable._();
/// Period → (startHour, startMinute, endHour, endMinute).
static const Map<int, (int, int, int, int)> _periods = {
1: (7, 55, 8, 40),
2: (8, 40, 9, 25),
3: (9, 45, 10, 30),
4: (10, 30, 11, 15),
5: (11, 35, 12, 20),
6: (12, 20, 13, 5),
7: (13, 50, 14, 35),
8: (14, 35, 15, 20),
};
/// Weekday (DateTime.monday..friday) → period → subject short code.
static const Map<int, Map<int, String>> _plan = {
DateTime.monday: {1: 'M', 2: 'M', 3: 'D', 4: 'E', 5: 'BIO', 6: 'GE', 7: 'SP', 8: 'SP'},
DateTime.tuesday: {1: 'E', 2: 'E', 3: 'PH', 4: 'M', 5: 'D', 6: 'REL', 7: 'GE'},
DateTime.wednesday: {1: 'BIO', 2: 'BIO', 3: 'M', 4: 'D', 5: 'E', 6: 'PH'},
DateTime.thursday: {1: 'D', 2: 'GE', 3: 'M', 4: 'M', 5: 'REL', 6: 'BIO', 7: 'SP', 8: 'SP'},
DateTime.friday: {1: 'PH', 2: 'PH', 3: 'E', 4: 'M', 5: 'D', 6: 'GE'},
};
static const Map<String, String> _roomBySubject = {
'M': 'A101',
'D': 'A101',
'E': 'A102',
'BIO': 'B201',
'PH': 'B203',
'GE': 'A101',
'SP': 'Turnhalle 1',
'REL': 'C305',
};
static TimetableGetWeekResponse week(DateTime from, DateTime until) {
final monday = _mondayOf(from);
final entries = <McTimetableEntry>[];
var id = 1;
for (final weekday in _plan.keys) {
final date = monday.add(Duration(days: weekday - DateTime.monday));
final day = _plan[weekday]!;
for (final entry in day.entries) {
entries.add(_entry(id++, date, weekday, entry.key, entry.value));
}
}
return TimetableGetWeekResponse(from: from, until: until, entries: entries);
}
static McTimetableEntry _entry(
int id,
DateTime date,
int weekday,
int period,
String subjectShort,
) {
final times = _periods[period]!;
final teacher = _teacherFor(subjectShort);
// A couple of recurring irregularities so the demo shows off the cancelled
// and substitution rendering.
final cancelled = weekday == DateTime.monday && period == 4;
final substituted = weekday == DateTime.tuesday && period == 3;
return McTimetableEntry(
id: id,
date: date,
startTime: DateTime(1970, 1, 1, times.$1, times.$2),
endTime: DateTime(1970, 1, 1, times.$3, times.$4),
subjects: [subjectShort],
teachers: [
if (substituted)
McTimetableTeacher(
shortName: 'MÜL',
displayName: 'Herr Müller',
originalShortName: teacher.short,
originalDisplayName: teacher.name,
)
else
McTimetableTeacher(shortName: teacher.short, displayName: teacher.name),
],
rooms: [_roomBySubject[subjectShort] ?? DemoPersona.rooms.first],
classNames: [DemoPersona.className],
lessonType: 'LESSON',
status: cancelled
? 'CANCELLED'
: substituted
? 'IRREGULAR'
: 'REGULAR',
substitutionText: substituted ? 'Vertretung für ${teacher.name}' : null,
lessonText: null,
infoText: cancelled ? 'Fällt aus' : null,
);
}
static TimetableGetRoomsResponse rooms() => TimetableGetRoomsResponse(
result: [
for (var i = 0; i < DemoPersona.rooms.length; i++)
McRoom(id: i + 1, shortName: DemoPersona.rooms[i], longName: DemoPersona.rooms[i]),
],
);
static TimetableGetSubjectsResponse subjects() => TimetableGetSubjectsResponse(
result: [
for (var i = 0; i < DemoPersona.subjects.length; i++)
McSubject(
id: i + 1,
shortName: DemoPersona.subjects[i].short,
longName: DemoPersona.subjects[i].name,
),
],
);
static TimetableGetTimegridResponse timegrid() {
final units = <McTimegridUnit>[];
for (final day in _weekdays) {
for (final period in _periods.entries) {
final t = period.value;
units.add(
McTimegridUnit(
dayOfWeek: day,
label: '${period.key}',
startTime: DateTime(1970, 1, 1, t.$1, t.$2),
endTime: DateTime(1970, 1, 1, t.$3, t.$4),
),
);
}
}
return TimetableGetTimegridResponse(result: units);
}
static TimetableGetSchoolyearResponse schoolyear() {
final startYear = _schoolyearStart(DateTime.now());
return TimetableGetSchoolyearResponse(
id: 1,
name: '$startYear/${startYear + 1}',
startDate: DateTime(startYear, 8, 1),
endDate: DateTime(startYear + 1, 7, 31),
);
}
static TimetableGetHolidaysResponse holidays() {
final startYear = _schoolyearStart(DateTime.now());
return TimetableGetHolidaysResponse(
result: [
McHoliday(
shortName: 'Herbst',
longName: 'Herbstferien',
startDate: DateTime(startYear, 10, 20),
endDate: DateTime(startYear, 10, 31),
),
McHoliday(
shortName: 'Weihnachten',
longName: 'Weihnachtsferien',
startDate: DateTime(startYear, 12, 22),
endDate: DateTime(startYear + 1, 1, 6),
),
],
);
}
static GetCustomTimetableEventResponse customEvents() =>
GetCustomTimetableEventResponse([]);
static const List<McDayOfWeek> _weekdays = [
McDayOfWeek.monday,
McDayOfWeek.tuesday,
McDayOfWeek.wednesday,
McDayOfWeek.thursday,
McDayOfWeek.friday,
];
static DemoTeacher _teacherFor(String subjectShort) {
final subjectName = DemoPersona.subjects
.firstWhere((s) => s.short == subjectShort)
.name;
return DemoPersona.teachers.firstWhere((t) => t.subject == subjectName);
}
static DateTime _mondayOf(DateTime date) {
final day = DateTime(date.year, date.month, date.day);
return day.subtract(Duration(days: day.weekday - DateTime.monday));
}
static int _schoolyearStart(DateTime now) =>
now.month >= 8 ? now.year : now.year - 1;
}