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
@@ -12,9 +12,9 @@ class BreakerDataProvider {
bool renew = true,
void Function(Object)? onError,
}) => resolveFromCache<GetBreakersResponse>(
(onUpdate, onError) =>
GetBreakersCache(renew: renew, onUpdate: onUpdate, onError: onError),
onError: onError,
operationName: 'getBreakers',
);
(onUpdate, onError) =>
GetBreakersCache(renew: renew, onUpdate: onUpdate, onError: onError),
onError: onError,
operationName: 'getBreakers',
);
}
@@ -2,6 +2,8 @@ import 'dart:developer';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import '../../../../../api/demo/data/demo_capabilities.dart';
import '../../../../../api/demo/demo_mode.dart';
import '../../../../../api/marianumconnect/queries/get_capabilities/get_capabilities.dart';
import 'capabilities_state.dart';
@@ -20,6 +22,10 @@ class CapabilitiesCubit extends HydratedCubit<CapabilitiesState> {
/// state is marked `loaded` — a failed fetch never silently grants a
/// capability, and an offline launch keeps whatever was cached.
Future<void> load() async {
if (DemoMode.active) {
emit(DemoCapabilities.state());
return;
}
try {
final response = await GetCapabilities().run();
emit(
@@ -1,3 +1,5 @@
import '../../../../../api/demo/data/demo_talk.dart';
import '../../../../../api/demo/demo_mode.dart';
import '../../../../../api/marianumcloud/talk/chat/get_chat_cache.dart';
import '../../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
@@ -8,6 +10,12 @@ class ChatDataProvider {
void Function(GetChatResponse data)? onNetworkData,
void Function(Object)? onError,
}) async {
if (DemoMode.active) {
final data = DemoTalk.chat(token);
onCacheData?.call(data);
onNetworkData?.call(data);
return;
}
final cache = GetChatCache(
chatToken: token,
onCacheData: onCacheData,
@@ -1,3 +1,5 @@
import '../../../../../api/demo/data/demo_talk.dart';
import '../../../../../api/demo/demo_mode.dart';
import '../../../../../api/marianumcloud/talk/create_room/create_room.dart';
import '../../../../../api/marianumcloud/talk/create_room/create_room_params.dart';
import '../../../../../api/marianumcloud/talk/room/get_room_cache.dart';
@@ -8,13 +10,18 @@ class ChatListDataProvider {
Future<GetRoomResponse> getRooms({
void Function(Object)? onError,
bool renew = false,
}) => resolveFromCache<GetRoomResponse>(
(onUpdate, onError) =>
GetRoomCache(renew: renew, onUpdate: onUpdate, onError: onError),
onError: onError,
operationName: 'getRooms',
);
}) {
if (DemoMode.active) return Future.value(DemoTalk.rooms());
return resolveFromCache<GetRoomResponse>(
(onUpdate, onError) =>
GetRoomCache(renew: renew, onUpdate: onUpdate, onError: onError),
onError: onError,
operationName: 'getRooms',
);
}
Future<void> createDirectRoom(String invite) =>
CreateRoom(CreateRoomParams(roomType: 1, invite: invite)).run();
Future<void> createDirectRoom(String invite) {
if (DemoMode.active) return Future.value();
return CreateRoom(CreateRoomParams(roomType: 1, invite: invite)).run();
}
}
@@ -1,5 +1,7 @@
import 'package:nextcloud/nextcloud.dart';
import '../../../../../api/demo/data/demo_files.dart';
import '../../../../../api/demo/demo_mode.dart';
import '../../../../../api/marianumcloud/webdav/queries/list_files/list_files_cache.dart';
import '../../../../../api/marianumcloud/webdav/queries/list_files/list_files_response.dart';
import '../../../../../api/marianumcloud/webdav/webdav_api.dart';
@@ -19,20 +21,24 @@ class FilesDataProvider {
void Function(Object)? onError,
bool renew = false,
void Function(int nextAttempt, int maxAttempts)? onRetry,
}) => resolveFromCache<ListFilesResponse>(
(onUpdate, onError) => ListFilesCache(
path: path,
onUpdate: onUpdate,
onCacheData: onCacheData,
}) {
if (DemoMode.active) return Future.value(DemoFiles.listing(path));
return resolveFromCache<ListFilesResponse>(
(onUpdate, onError) => ListFilesCache(
path: path,
onUpdate: onUpdate,
onCacheData: onCacheData,
onError: onError,
renew: renew,
onRetry: onRetry,
),
onError: onError,
renew: renew,
onRetry: onRetry,
),
onError: onError,
operationName: 'listFiles',
);
operationName: 'listFiles',
);
}
Future<void> createFolder(String fullPath) async {
if (DemoMode.active) return;
final webdav = await WebdavApi.webdav;
await webdav.mkcol(PathUri.parse(fullPath));
}
@@ -1,15 +1,19 @@
import 'package:hydrated_bloc/hydrated_bloc.dart';
import '../../../../../api/demo/demo_mode.dart';
import 'grade_averages_event.dart';
import 'grade_averages_state.dart';
class GradeAveragesBloc
extends HydratedBloc<GradeAveragesEvent, GradeAveragesState> {
// Seeded with a realistic spread in demo mode (average 2.0); empty otherwise.
static const List<int> _demoGrades = [1, 2, 2, 2, 3, 2, 1, 3, 2, 2];
GradeAveragesBloc()
: super(
const GradeAveragesState(
GradeAveragesState(
gradingSystem: GradeAveragesGradingSystem.middleSchool,
grades: [],
grades: DemoMode.active ? _demoGrades : const [],
),
) {
on<GradingSystemChanged>((event, emit) {
@@ -1,7 +1,12 @@
import '../../../../../api/demo/data/demo_marianum_dates.dart';
import '../../../../../api/demo/demo_mode.dart';
import '../../../infrastructure/repository/repository.dart';
import '../bloc/marianum_dates_state.dart';
import '../data_provider/marianum_dates_get_events.dart';
class MarianumDatesRepository extends Repository<MarianumDatesState> {
Future<List<MarianumDate>> getEvents() => MarianumDatesGetEvents().run();
Future<List<MarianumDate>> getEvents() {
if (DemoMode.active) return Future.value(DemoMarianumDates.events());
return MarianumDatesGetEvents().run();
}
}
@@ -1,8 +1,12 @@
import '../../../../../api/demo/data/demo_marianum_message.dart';
import '../../../../../api/demo/demo_mode.dart';
import '../../../infrastructure/repository/repository.dart';
import '../bloc/marianum_message_state.dart';
import '../data_provider/marianum_message_get_messages.dart';
class MarianumMessageRepository extends Repository<MarianumMessageState> {
Future<MarianumMessageList> getMessages() =>
MarianumMessageGetMessages().run();
Future<MarianumMessageList> getMessages() {
if (DemoMode.active) return Future.value(DemoMarianumMessage.list());
return MarianumMessageGetMessages().run();
}
}
@@ -2,6 +2,8 @@ import 'dart:developer';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import '../../../../../api/demo/data/demo_capabilities.dart';
import '../../../../../api/demo/demo_mode.dart';
import '../../../../../api/marianumcloud/capabilities/get_nextcloud_capabilities.dart';
import 'nextcloud_capabilities_state.dart';
@@ -57,6 +59,10 @@ class NextcloudCapabilitiesCubit
/// Refreshes capabilities from the server. On any failure the previously
/// hydrated flags are kept but the state is marked `loaded`.
Future<void> load() async {
if (DemoMode.active) {
emit(DemoNextcloudCapabilities.state());
return;
}
try {
final caps = await GetNextcloudCapabilities().run();
emit(
@@ -1,3 +1,5 @@
import '../../../../../api/demo/data/demo_timetable.dart';
import '../../../../../api/demo/demo_mode.dart';
import '../../../../../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays.dart';
import '../../../../../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart';
import '../../../../../api/marianumconnect/queries/timetable_get_rooms/timetable_get_rooms.dart';
@@ -95,27 +97,36 @@ class TimetableDataProvider {
Future<GetCustomTimetableEventResponse> getCustomEvents({
bool renew = false,
void Function(Object)? onError,
}) => resolveFromCache<GetCustomTimetableEventResponse>(
}) {
if (DemoMode.active) return Future.value(DemoTimetable.customEvents());
return resolveFromCache<GetCustomTimetableEventResponse>(
(onUpdate, onError) => GetCustomTimetableEventCache(
GetCustomTimetableEventParams(AccountData().getUserSecret()),
renew: renew,
onUpdate: onUpdate,
onError: onError,
),
onError: onError,
operationName: 'getCustomEvents',
);
onError: onError,
operationName: 'getCustomEvents',
);
}
Future<void> addCustomEvent(CustomTimetableEvent event) =>
AddCustomTimetableEvent(
AddCustomTimetableEventParams(AccountData().getUserSecret(), event),
).run();
Future<void> addCustomEvent(CustomTimetableEvent event) {
if (DemoMode.active) return Future.value();
return AddCustomTimetableEvent(
AddCustomTimetableEventParams(AccountData().getUserSecret(), event),
).run();
}
Future<void> updateCustomEvent(String id, CustomTimetableEvent event) =>
UpdateCustomTimetableEvent(
UpdateCustomTimetableEventParams(id, event),
).run();
Future<void> updateCustomEvent(String id, CustomTimetableEvent event) {
if (DemoMode.active) return Future.value();
return UpdateCustomTimetableEvent(
UpdateCustomTimetableEventParams(id, event),
).run();
}
Future<void> removeCustomEvent(String id) =>
RemoveCustomTimetableEvent(RemoveCustomTimetableEventParams(id)).run();
Future<void> removeCustomEvent(String id) {
if (DemoMode.active) return Future.value();
return RemoveCustomTimetableEvent(RemoveCustomTimetableEventParams(id)).run();
}
}