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:
@@ -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([]);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'data/demo_breaker.dart';
|
||||
import 'data/demo_holidays.dart';
|
||||
import 'data/demo_timetable.dart';
|
||||
|
||||
/// Single source of truth for the MarianumConnect demo responses. The demo
|
||||
/// interceptor asks this for the body of any MC request. Read endpoints reuse
|
||||
/// the typed fixtures and serialize them to the exact shape the matching query
|
||||
/// parses (a bare list vs. an object); write/side-effect endpoints (telemetry,
|
||||
/// client-errors, auth, push, feedback) have no consumed body and resolve to an
|
||||
/// empty success.
|
||||
class DemoMarianumConnect {
|
||||
const DemoMarianumConnect._();
|
||||
|
||||
/// Body for [path] (relative to `/api/mobile/v1/`), using [query] for the
|
||||
/// timetable date window.
|
||||
static Object responseBody(String path, Map<String, dynamic> query) {
|
||||
switch (path) {
|
||||
case 'timetable/me':
|
||||
return DemoTimetable.week(
|
||||
_date(query['from']),
|
||||
_date(query['until']),
|
||||
).toJson();
|
||||
case 'timetable/schoolyear':
|
||||
return DemoTimetable.schoolyear().toJson();
|
||||
case 'timetable/rooms':
|
||||
return DemoTimetable.rooms().result.map((e) => e.toJson()).toList();
|
||||
case 'timetable/subjects':
|
||||
return DemoTimetable.subjects().result.map((e) => e.toJson()).toList();
|
||||
case 'timetable/timegrid':
|
||||
return DemoTimetable.timegrid().result.map((e) => e.toJson()).toList();
|
||||
case 'timetable/holidays':
|
||||
return DemoTimetable.holidays().result.map((e) => e.toJson()).toList();
|
||||
case 'holidays':
|
||||
return DemoHolidays.upcoming().map((e) => e.toJson()).toList();
|
||||
case 'breaker':
|
||||
return DemoBreaker.none().toJson();
|
||||
case 'timetable/elements/teachers':
|
||||
case 'timetable/elements/students':
|
||||
case 'timetable/elements/classes':
|
||||
return const <dynamic>[];
|
||||
}
|
||||
// A foreign element's week (timetable/<type>/<id>) shows the same demo week.
|
||||
if (path.startsWith('timetable/')) {
|
||||
return DemoTimetable.week(
|
||||
_date(query['from']),
|
||||
_date(query['until']),
|
||||
).toJson();
|
||||
}
|
||||
return const <String, dynamic>{};
|
||||
}
|
||||
|
||||
static DateTime _date(Object? raw) =>
|
||||
DateTime.tryParse(raw?.toString() ?? '') ?? DateTime.now();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'demo_marianumconnect.dart';
|
||||
import 'demo_mode.dart';
|
||||
|
||||
/// Answers every MarianumConnect request from fixtures while a demo session is
|
||||
/// active, so the app makes no MC network calls. Installed first on the MC dio
|
||||
/// so it resolves before the auth interceptor tries to attach a (non-existent)
|
||||
/// bearer token.
|
||||
class DemoMarianumConnectInterceptor extends Interceptor {
|
||||
static const _apiMarker = '/api/mobile/v1/';
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (!DemoMode.active) {
|
||||
handler.next(options);
|
||||
return;
|
||||
}
|
||||
handler.resolve(
|
||||
Response<dynamic>(
|
||||
requestOptions: options,
|
||||
statusCode: 200,
|
||||
data: DemoMarianumConnect.responseBody(
|
||||
_relativePath(options.uri),
|
||||
options.queryParameters,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String _relativePath(Uri uri) {
|
||||
final i = uri.path.indexOf(_apiMarker);
|
||||
return i >= 0 ? uri.path.substring(i + _apiMarker.length) : uri.path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import '../../model/account_data.dart';
|
||||
|
||||
/// Central switch for the client-side demo mode.
|
||||
///
|
||||
/// A login whose username starts with [usernamePrefix] (the password is
|
||||
/// ignored) enters demo mode: instead of talking to the real backends, the app
|
||||
/// serves curated fixtures from `DemoData` for every feature. This lets Google
|
||||
/// Play reviewers and automated screenshot runs see a fully populated app
|
||||
/// without a real account and without any network dependency.
|
||||
///
|
||||
/// The flag is persisted through [AccountData.isDemo], so a demo session
|
||||
/// survives cold starts exactly like a normal login. It works in release builds
|
||||
/// too — reviewers run the shipped release build.
|
||||
class DemoMode {
|
||||
const DemoMode._();
|
||||
|
||||
/// Usernames starting with this prefix trigger demo mode. Whatever the
|
||||
/// reviewer types as the password is accepted.
|
||||
static const String usernamePrefix = 'demo@';
|
||||
|
||||
/// Whether [username], as typed on the login screen, should enter demo mode.
|
||||
static bool matches(String username) =>
|
||||
username.trim().toLowerCase().startsWith(usernamePrefix);
|
||||
|
||||
/// True while the active session is a demo session.
|
||||
static bool get active => AccountData().isDemo;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/// Shared identity for the demo mode so fixtures across every feature describe
|
||||
/// the same fictional student, class, teachers and rooms — the timetable, chats
|
||||
/// and grades all line up. See DemoMode.
|
||||
class DemoPersona {
|
||||
const DemoPersona._();
|
||||
|
||||
static const String studentFirstName = 'Lena';
|
||||
static const String studentName = 'Lena Hoffmann';
|
||||
static const String className = '10b';
|
||||
|
||||
static const List<DemoTeacher> teachers = [
|
||||
DemoTeacher('Herr Müller', 'MÜL', 'Mathematik'),
|
||||
DemoTeacher('Frau Schmidt', 'SMT', 'Deutsch'),
|
||||
DemoTeacher('Herr Weber', 'WEB', 'Englisch'),
|
||||
DemoTeacher('Frau Becker', 'BEC', 'Biologie'),
|
||||
DemoTeacher('Herr Hofmann', 'HOF', 'Physik'),
|
||||
DemoTeacher('Frau Wagner', 'WAG', 'Geschichte'),
|
||||
DemoTeacher('Herr Klein', 'KLN', 'Sport'),
|
||||
DemoTeacher('Frau Richter', 'RIC', 'Religion'),
|
||||
];
|
||||
|
||||
static const List<DemoSubject> subjects = [
|
||||
DemoSubject('Mathematik', 'M', 0xFF4E79A7),
|
||||
DemoSubject('Deutsch', 'D', 0xFFE15759),
|
||||
DemoSubject('Englisch', 'E', 0xFFF28E2B),
|
||||
DemoSubject('Biologie', 'BIO', 0xFF59A14F),
|
||||
DemoSubject('Physik', 'PH', 0xFF76B7B2),
|
||||
DemoSubject('Geschichte', 'GE', 0xFFB07AA1),
|
||||
DemoSubject('Sport', 'SP', 0xFFEDC948),
|
||||
DemoSubject('Religion', 'REL', 0xFF9C755F),
|
||||
];
|
||||
|
||||
static const List<String> rooms = [
|
||||
'A101',
|
||||
'A102',
|
||||
'B201',
|
||||
'B203',
|
||||
'C305',
|
||||
'Turnhalle 1',
|
||||
'Aula',
|
||||
];
|
||||
}
|
||||
|
||||
class DemoTeacher {
|
||||
final String name;
|
||||
final String short;
|
||||
final String subject;
|
||||
const DemoTeacher(this.name, this.short, this.subject);
|
||||
}
|
||||
|
||||
class DemoSubject {
|
||||
final String name;
|
||||
final String short;
|
||||
|
||||
/// ARGB color used where the UI colors a lesson/subject.
|
||||
final int color;
|
||||
const DemoSubject(this.name, this.short, this.color);
|
||||
}
|
||||
@@ -14,6 +14,9 @@ class SessionValidator {
|
||||
required Future<void> Function() onInvalidated,
|
||||
}) async {
|
||||
if (!AccountData().isPopulated()) return;
|
||||
// AuthVerify uses its own dio (bypassing the demo interceptor), so a demo
|
||||
// session must be skipped here or its missing token would 401 into a logout.
|
||||
if (AccountData().isDemo) return;
|
||||
final username = AccountData().getUsername();
|
||||
final password = AccountData().getPassword();
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../demo/demo_marianumconnect_interceptor.dart';
|
||||
import 'auth/auth_interceptor.dart';
|
||||
|
||||
/// Singleton dio instance for the Marianum-Connect mobile API. Wired with the
|
||||
@@ -24,6 +25,7 @@ class MarianumConnectApi {
|
||||
contentType: 'application/json',
|
||||
),
|
||||
);
|
||||
dio.interceptors.add(DemoMarianumConnectInterceptor());
|
||||
dio.interceptors.add(MarianumConnectAuthInterceptor());
|
||||
return dio;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user