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:
@@ -3,6 +3,7 @@ import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../api/demo/demo_mode.dart';
|
||||
import '../../api/errors/auth_exception.dart';
|
||||
import '../../api/errors/error_mapper.dart';
|
||||
import '../../api/marianumconnect/auth/device_token_name.dart';
|
||||
@@ -33,6 +34,20 @@ class LoginController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
final user = username.trim().toLowerCase();
|
||||
|
||||
// Demo login: the prefix enters local demo mode, password ignored, no
|
||||
// network (see DemoMode).
|
||||
if (DemoMode.matches(user)) {
|
||||
await AccountData().removeData();
|
||||
await const MarianumConnectTokenStorage().clear();
|
||||
await WidgetSync.clear();
|
||||
await WidgetSync.triggerUpdate();
|
||||
await AccountData().setDemo(user);
|
||||
_loading = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
await AccountData().removeData();
|
||||
// Vorherigen Token revoken bevor wir einen neuen anfordern — ein altes
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../../../../state/app/modules/nextcloud_capabilities/bloc/nextcloud_capa
|
||||
import '../../../../utils/clipboard_helper.dart';
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
import '../../../../widget/info_dialog.dart';
|
||||
import 'share_password_sheet.dart';
|
||||
@@ -35,6 +36,7 @@ void showShareOptionsSheet(
|
||||
Share share, {
|
||||
required VoidCallback onChanged,
|
||||
}) {
|
||||
if (guardDemoAction(context)) return;
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
header: ListTile(
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../../../../state/app/modules/nextcloud_capabilities/bloc/nextcloud_capa
|
||||
import '../../../../utils/clipboard_helper.dart';
|
||||
import '../../../../widget/app_progress_indicator.dart';
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
import '../../../../widget/info_dialog.dart';
|
||||
import '../widgets/file_leading.dart';
|
||||
@@ -23,6 +24,7 @@ import 'share_password_sheet.dart';
|
||||
/// gated actions to create new ones (public link, user/group). Capability
|
||||
/// gating mirrors the Nextcloud web UI.
|
||||
void showShareSheet(BuildContext context, CacheableFile file) {
|
||||
if (guardDemoAction(context)) return;
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
header: ListTile(
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../state/app/modules/files/bloc/files_bloc.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
import '../../../../widget/file_pick.dart';
|
||||
|
||||
@@ -13,6 +14,7 @@ void showAddFileSheet(
|
||||
required FilesBloc bloc,
|
||||
required Future<void> Function(List<String>? paths) onPickedFiles,
|
||||
}) {
|
||||
if (guardDemoAction(context)) return;
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
children: (sheetCtx) => [
|
||||
|
||||
@@ -15,6 +15,7 @@ import '../../../../utils/file_clipboard.dart';
|
||||
import '../../../../utils/haptics.dart';
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
import '../../../../widget/downloads/download_trigger.dart';
|
||||
import '../../../../widget/info_dialog.dart';
|
||||
@@ -105,6 +106,7 @@ class _FileElementState extends State<FileElement>
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (guardDemoAction(context)) return;
|
||||
if (EndpointData().getEndpointMode() == EndpointMode.stage) {
|
||||
InfoDialog.show(
|
||||
context,
|
||||
@@ -135,6 +137,7 @@ class _FileElementState extends State<FileElement>
|
||||
isDirectory ? '$folder$name/' : '$folder$name';
|
||||
|
||||
Future<void> _rename() async {
|
||||
if (guardDemoAction(context)) return;
|
||||
final controller = TextEditingController(text: widget.file.name);
|
||||
try {
|
||||
final newName = await showDialog<String>(
|
||||
@@ -182,6 +185,7 @@ class _FileElementState extends State<FileElement>
|
||||
}
|
||||
|
||||
void _putOnClipboard({required bool copy}) {
|
||||
if (guardDemoAction(context)) return;
|
||||
if (copy) {
|
||||
FileClipboard.instance.copy([widget.file]);
|
||||
} else {
|
||||
@@ -198,6 +202,7 @@ class _FileElementState extends State<FileElement>
|
||||
}
|
||||
|
||||
Future<void> _delete() async {
|
||||
if (guardDemoAction(context)) return;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => ConfirmDialog(
|
||||
@@ -269,6 +274,7 @@ class _FileElementState extends State<FileElement>
|
||||
title: const Text('Im Talk-Chat versenden'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
if (guardDemoAction(context)) return;
|
||||
AppRoutes.openInternalShareToChat(
|
||||
context,
|
||||
RemoteFileRef.fromCacheable(widget.file),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../api/marianumconnect/queries/submit_feedback/submit_feedback.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/file_pick.dart';
|
||||
import '../../../../widget/focus_behaviour.dart';
|
||||
import '../../../../widget/info_dialog.dart';
|
||||
@@ -50,6 +51,7 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (guardDemoAction(context)) return;
|
||||
if (_feedbackInput.text.trim().isEmpty) {
|
||||
setState(() => _textFieldEmpty = true);
|
||||
return;
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../../../../widget/app_progress_indicator.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/avatar_actions_sheet.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/large_profile_picture_view.dart';
|
||||
import '../../../../widget/user_avatar.dart';
|
||||
|
||||
@@ -51,6 +52,7 @@ class _AccountSectionState extends State<AccountSection> {
|
||||
}
|
||||
|
||||
Future<void> _editAvatar() async {
|
||||
if (guardDemoAction(context)) return;
|
||||
final result = await showAvatarActionsSheet(context, allowRemove: true);
|
||||
if (result == null || !mounted) return;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../../../../push/push_status.dart';
|
||||
import '../../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../../widget/app_progress_indicator.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
|
||||
/// Opens the push status checklist: one row per link in the push chain with
|
||||
@@ -92,6 +93,7 @@ class _PushStatusBodyState extends State<_PushStatusBody>
|
||||
}
|
||||
|
||||
Future<void> _reRegister() async {
|
||||
if (guardDemoAction(context)) return;
|
||||
if (_busy) return;
|
||||
setState(() => _busy = true);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
@@ -111,6 +113,7 @@ class _PushStatusBodyState extends State<_PushStatusBody>
|
||||
}
|
||||
|
||||
Future<void> _sendTest() async {
|
||||
if (guardDemoAction(context)) return;
|
||||
if (_busy) return;
|
||||
setState(() => _busy = true);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||
import '../../../state/app/modules/chat_list/bloc/chat_list_state.dart';
|
||||
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../widget/confirm_dialog.dart';
|
||||
import '../../../widget/demo_restricted.dart';
|
||||
import '../../../widget/placeholder_view.dart';
|
||||
import 'join_chat.dart';
|
||||
import 'notification_permission_prompt.dart';
|
||||
@@ -100,6 +101,7 @@ class _ChatListViewState extends State<_ChatListView> {
|
||||
heroTag: 'createChat',
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
onPressed: () {
|
||||
if (guardDemoAction(context)) return;
|
||||
showSearch(context: context, delegate: JoinChat()).then((
|
||||
username,
|
||||
) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../../routing/app_routes.dart';
|
||||
import '../../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
|
||||
/// Opens an existing 1:1 chat with [actorId] or, after confirmation, creates
|
||||
/// one. Shared between the group-message context menu and the reactions
|
||||
@@ -14,6 +15,7 @@ void openOrCreateDirectChat(
|
||||
required String actorId,
|
||||
required String actorDisplayName,
|
||||
}) {
|
||||
if (guardDemoAction(context)) return;
|
||||
final chatListBloc = context.read<ChatListBloc>();
|
||||
|
||||
GetRoomResponseObject? findExisting() {
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../../../share_intent/remote_file_ref.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../utils/downloads/download_job.dart';
|
||||
import '../../../../utils/haptics.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/downloads/download_trigger.dart';
|
||||
import '../data/chat_bubble_styles.dart';
|
||||
import '../data/chat_message.dart';
|
||||
@@ -177,6 +178,7 @@ class _ChatBubbleState extends State<ChatBubble>
|
||||
return;
|
||||
}
|
||||
if (message.file == null) return;
|
||||
if (guardDemoAction(context)) return;
|
||||
if (isDownloading) {
|
||||
confirmCancelDownload();
|
||||
} else {
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../../../api/marianumcloud/talk/react_message/react_message.dart';
|
||||
import '../../../../api/marianumcloud/talk/react_message/react_message_params.dart';
|
||||
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/emoji_text.dart';
|
||||
|
||||
/// Reactions wrap shown beneath a chat bubble. Tapping a reaction toggles
|
||||
@@ -60,6 +61,7 @@ class ChatBubbleReactions extends StatelessWidget {
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
onPressed: () {
|
||||
if (guardDemoAction(context)) return;
|
||||
runWithErrorDialog(context, () async {
|
||||
if (hasSelfReacted) {
|
||||
await DeleteReactMessage(
|
||||
|
||||
@@ -14,6 +14,7 @@ import '../../../../widget/app_progress_indicator.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/debug/debug_tile.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
import '../../../../widget/emoji_picker_dialog.dart';
|
||||
import '../../../../widget/emoji_text.dart';
|
||||
@@ -64,6 +65,7 @@ void showChatMessageOptionsDialog(
|
||||
leading: const Icon(Icons.reply_outlined),
|
||||
title: const Text('Antworten'),
|
||||
onTap: () {
|
||||
if (guardDemoAction(sheetCtx)) return;
|
||||
sheetCtx.read<ChatBloc>().setReferenceMessageId(bubbleData.id);
|
||||
Navigator.of(sheetCtx).pop();
|
||||
},
|
||||
@@ -96,6 +98,7 @@ void showChatMessageOptionsDialog(
|
||||
leading: const Icon(Icons.cloud_outlined),
|
||||
title: const Text('In Cloud speichern'),
|
||||
onTap: () {
|
||||
if (guardDemoAction(sheetCtx)) return;
|
||||
Navigator.of(sheetCtx).pop();
|
||||
if (!parentContext.mounted) return;
|
||||
AppRoutes.openInternalSaveToFolder(
|
||||
@@ -109,6 +112,7 @@ void showChatMessageOptionsDialog(
|
||||
leading: const Icon(Icons.shortcut_outlined),
|
||||
title: const Text('Weiterleiten'),
|
||||
onTap: () {
|
||||
if (guardDemoAction(sheetCtx)) return;
|
||||
Navigator.of(sheetCtx).pop();
|
||||
if (!parentContext.mounted) return;
|
||||
AppRoutes.openForwardMessageToChat(
|
||||
@@ -143,6 +147,7 @@ void showChatMessageOptionsDialog(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: const Text('Nachricht löschen'),
|
||||
onTap: () {
|
||||
if (guardDemoAction(sheetCtx)) return;
|
||||
ConfirmDialog(
|
||||
title: 'Nachricht löschen?',
|
||||
content: 'Die Nachricht wird für alle Teilnehmer gelöscht.',
|
||||
@@ -189,6 +194,7 @@ class _ReactionsRowState extends State<_ReactionsRow> {
|
||||
}
|
||||
|
||||
Future<void> _react(String emoji) async {
|
||||
if (guardDemoAction(context)) return;
|
||||
final ok = await _controller.run(() async {
|
||||
await ReactMessage(
|
||||
chatToken: widget.chatToken,
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../../../../api/marianumcloud/webdav/webdav_api.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
import '../../../../widget/downloads/download_tray.dart';
|
||||
import '../../../../widget/emoji_picker_dialog.dart';
|
||||
@@ -125,6 +126,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
}
|
||||
|
||||
void _showAttachmentSheet() {
|
||||
if (guardDemoAction(context)) return;
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
children: (sheetCtx) => [
|
||||
@@ -187,6 +189,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
|
||||
Future<void> _sendMessage(ChatBloc chatBloc) async {
|
||||
if (_textBoxController.text.isEmpty) return;
|
||||
if (guardDemoAction(context)) return;
|
||||
final text = _textBoxController.text;
|
||||
final replyTo = chatBloc.state.data?.referenceMessageId?.toString();
|
||||
final ownToken = widget.sendToToken;
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart'
|
||||
import '../../../../extensions/date_time.dart';
|
||||
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/focus_behaviour.dart';
|
||||
import 'custom_event_colors.dart';
|
||||
|
||||
@@ -130,6 +131,7 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
|
||||
end.second == 0;
|
||||
|
||||
Future<void> _save() async {
|
||||
if (guardDemoAction(context)) return;
|
||||
if (_name.text.trim().isEmpty) {
|
||||
throw Exception('Bitte einen Terminnamen eingeben.');
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
|
||||
Completer<void> showDeleteCustomEventDialog(
|
||||
BuildContext context,
|
||||
CustomTimetableEvent event,
|
||||
) {
|
||||
final completer = Completer<void>();
|
||||
if (guardDemoAction(context)) return completer;
|
||||
final bloc = context.read<TimetableBloc>();
|
||||
ConfirmDialog(
|
||||
title: 'Termin löschen',
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import '../../../state/app/modules/timetable/bloc/timetable_state.dart';
|
||||
import '../../../utils/haptics.dart';
|
||||
import '../../../widget/demo_restricted.dart';
|
||||
import 'custom_events/custom_event_edit_dialog.dart';
|
||||
import 'details/appointment_details_dispatcher.dart';
|
||||
import 'widgets/timetable_calendar_view.dart';
|
||||
@@ -42,6 +43,7 @@ class _TimetableState extends State<Timetable> {
|
||||
state.startDate == _mondayOf(_initialDisplayDate());
|
||||
|
||||
Future<void> _openPicker() async {
|
||||
if (guardDemoAction(context)) return;
|
||||
final ref = await AppRoutes.openElementPicker(context);
|
||||
if (!mounted || ref == null) return;
|
||||
setState(() => _selected = ref);
|
||||
|
||||
Reference in New Issue
Block a user