From 4f796dac2eea3fa7073150500ca2cf768e5c708a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Tue, 5 May 2026 21:44:23 +0200 Subject: [PATCH] folder restructuring --- .../autocomplete/autocompleteApi.dart | 4 +- .../files-sharing/fileSharingApi.dart | 4 +- lib/api/marianumcloud/talk/talkApi.dart | 4 +- lib/api/marianumcloud/webdav/webdavApi.dart | 4 +- .../userIndex/update/updateUserindex.dart | 2 +- .../queries/authenticate/authenticate.dart | 2 +- lib/api/webuntis/webuntisApi.dart | 2 +- lib/app.dart | 10 +- .../{dateTime.dart => date_time.dart} | 0 ...enderNotNull.dart => render_not_null.dart} | 0 .../{timeOfDay.dart => time_of_day.dart} | 0 lib/main.dart | 8 +- .../{accountData.dart => account_data.dart} | 0 .../{dataCleaner.dart => data_cleaner.dart} | 0 .../{endpointData.dart => endpoint_data.dart} | 2 +- ...ller.dart => notification_controller.dart} | 16 +- ...Service.dart => notification_service.dart} | 0 ...tionTasks.dart => notification_tasks.dart} | 16 +- ...notifyUpdater.dart => notify_updater.dart} | 4 +- lib/routing/app_routes.dart | 202 +++++++++++ .../view/loadable_state_error_bar.dart | 2 +- lib/state/app/modules/app_modules.dart | 23 +- .../modules/settings/bloc/settings_cubit.dart | 2 +- .../dataProvider/timetable_data_provider.dart | 2 +- .../timetable/timetable_name_mode.dart | 2 +- lib/theming/{appTheme.dart => app_theme.dart} | 2 +- ...{darkAppTheme.dart => dark_app_theme.dart} | 0 ...ightAppTheme.dart => light_app_theme.dart} | 0 lib/utils/{FileSaver.dart => file_saver.dart} | 0 lib/utils/{UrlOpener.dart => url_opener.dart} | 0 lib/view/login/login.dart | 2 +- lib/view/pages/files/fileUploadDialog.dart | 0 lib/view/pages/files/files.dart | 8 +- ...adDialog.dart => files_upload_dialog.dart} | 4 +- .../file_element.dart} | 29 +- .../grade_averages_list_view.dart | 4 +- .../grade_averages}/grade_averages_view.dart | 8 +- .../pages/holidays}/holidays_view.dart | 22 +- .../marianum_dates}/marianum_dates_view.dart | 22 +- .../marianum_message_list_view.dart | 14 +- .../marianum_message_view.dart | 4 +- ...edbackDialog.dart => feedback_dialog.dart} | 8 +- ...View.dart => app_share_platform_view.dart} | 0 .../{qrShareView.dart => qr_share_view.dart} | 2 +- ...log.dart => select_share_type_dialog.dart} | 10 +- lib/view/pages/overhang.dart | 27 +- .../settings/data/default_settings.dart} | 24 +- .../settings/sections/about_section.dart | 135 ++++++++ .../settings/sections/account_section.dart | 41 +++ .../settings/sections/appearance_section.dart | 36 ++ .../settings/sections/dev_tools_section.dart} | 23 +- .../settings/sections/files_section.dart | 33 ++ .../pages/settings/sections/talk_section.dart | 72 ++++ .../settings/sections/timetable_section.dart | 57 ++++ lib/view/pages/settings/settings.dart | 37 ++ .../settings/widgets/privacy_info.dart} | 4 +- lib/view/pages/talk/chatList.dart | 142 -------- lib/view/pages/talk/chat_list.dart | 180 ++++++++++ .../talk/{chatView.dart => chat_view.dart} | 16 +- .../chat_bubble_styles.dart} | 2 +- .../chat_message.dart} | 6 +- .../chatInfo.dart => details/chat_info.dart} | 10 +- .../message_reactions.dart} | 16 +- .../participants_list_view.dart} | 4 +- .../talk/{joinChat.dart => join_chat.dart} | 4 +- .../{searchChat.dart => search_chat.dart} | 2 +- ...talkNavigator.dart => talk_navigator.dart} | 0 .../answer_reference.dart} | 2 +- .../chat_bubble.dart} | 202 +---------- .../widgets/chat_message_options_dialog.dart | 202 +++++++++++ .../chat_textfield.dart} | 8 +- .../chatTile.dart => widgets/chat_tile.dart} | 12 +- .../poll_options_list.dart} | 2 +- .../split_view_placeholder.dart} | 2 +- .../custom_events/custom_event_colors.dart | 2 +- .../custom_event_edit_dialog.dart | 6 +- .../custom_events/custom_events_view.dart | 4 +- .../timetable/details/custom_event_sheet.dart | 4 +- .../details/delete_custom_event.dart | 2 +- .../details/webuntis_lesson_sheet.dart | 9 +- lib/view/pages/timetable/timetable.dart | 6 +- .../widgets/special_regions_builder.dart | 2 +- lib/view/settings/settings.dart | 318 ------------------ .../{animatedTime.dart => animated_time.dart} | 0 lib/widget/breaker/breaker.dart | 2 +- ...eredLeading.dart => centered_leading.dart} | 0 ...ableAppBar.dart => clickable_app_bar.dart} | 0 ...confirmDialog.dart => confirm_dialog.dart} | 0 .../debug/{cacheView.dart => cache_view.dart} | 4 +- .../debug/{debugTile.dart => debug_tile.dart} | 4 +- .../{jsonViewer.dart => json_viewer.dart} | 0 ...downDisplay.dart => dropdown_display.dart} | 0 lib/widget/{filePick.dart => file_pick.dart} | 0 .../{fileViewer.dart => file_viewer.dart} | 13 +- ...cusBehaviour.dart => focus_behaviour.dart} | 0 .../{infoDialog.dart => info_dialog.dart} | 0 ...w.dart => large_profile_picture_view.dart} | 2 +- ...adingSpinner.dart => loading_spinner.dart} | 0 ...eholderView.dart => placeholder_view.dart} | 0 ...Origin.dart => share_position_origin.dart} | 0 ...dDialog.dart => unimplemented_dialog.dart} | 0 .../{userAvatar.dart => user_avatar.dart} | 4 +- 102 files changed, 1254 insertions(+), 879 deletions(-) rename lib/extensions/{dateTime.dart => date_time.dart} (100%) rename lib/extensions/{renderNotNull.dart => render_not_null.dart} (100%) rename lib/extensions/{timeOfDay.dart => time_of_day.dart} (100%) rename lib/model/{accountData.dart => account_data.dart} (100%) rename lib/model/{dataCleaner.dart => data_cleaner.dart} (100%) rename lib/model/{endpointData.dart => endpoint_data.dart} (97%) rename lib/notification/{notificationController.dart => notification_controller.dart} (83%) rename lib/notification/{notificationService.dart => notification_service.dart} (100%) rename lib/notification/{notificationTasks.dart => notification_tasks.dart} (57%) rename lib/notification/{notifyUpdater.dart => notify_updater.dart} (95%) create mode 100644 lib/routing/app_routes.dart rename lib/theming/{appTheme.dart => app_theme.dart} (93%) rename lib/theming/{darkAppTheme.dart => dark_app_theme.dart} (100%) rename lib/theming/{lightAppTheme.dart => light_app_theme.dart} (100%) rename lib/utils/{FileSaver.dart => file_saver.dart} (100%) rename lib/utils/{UrlOpener.dart => url_opener.dart} (100%) delete mode 100644 lib/view/pages/files/fileUploadDialog.dart rename lib/view/pages/files/{filesUploadDialog.dart => files_upload_dialog.dart} (99%) rename lib/view/pages/files/{fileElement.dart => widgets/file_element.dart} (86%) rename lib/{state/app/modules/gradeAverages/view => view/pages/grade_averages}/grade_averages_list_view.dart (93%) rename lib/{state/app/modules/gradeAverages/view => view/pages/grade_averages}/grade_averages_view.dart (93%) rename lib/{state/app/modules/holidays/view => view/pages/holidays}/holidays_view.dart (87%) rename lib/{state/app/modules/marianumDates/view => view/pages/marianum_dates}/marianum_dates_view.dart (86%) rename lib/{state/app/modules/marianumMessage/view => view/pages/marianum_message}/marianum_message_list_view.dart (69%) rename lib/{state/app/modules/marianumMessage/view => view/pages/marianum_message}/marianum_message_view.dart (92%) rename lib/view/pages/more/feedback/{feedbackDialog.dart => feedback_dialog.dart} (97%) rename lib/view/pages/more/share/{appSharePlatformView.dart => app_share_platform_view.dart} (100%) rename lib/view/pages/more/share/{qrShareView.dart => qr_share_view.dart} (97%) rename lib/view/pages/more/share/{selectShareTypeDialog.dart => select_share_type_dialog.dart} (84%) rename lib/view/{settings/defaultSettings.dart => pages/settings/data/default_settings.dart} (68%) create mode 100644 lib/view/pages/settings/sections/about_section.dart create mode 100644 lib/view/pages/settings/sections/account_section.dart create mode 100644 lib/view/pages/settings/sections/appearance_section.dart rename lib/view/{settings/devToolsSettings.dart => pages/settings/sections/dev_tools_section.dart} (89%) create mode 100644 lib/view/pages/settings/sections/files_section.dart create mode 100644 lib/view/pages/settings/sections/talk_section.dart create mode 100644 lib/view/pages/settings/sections/timetable_section.dart create mode 100644 lib/view/pages/settings/settings.dart rename lib/view/{settings/privacyInfo.dart => pages/settings/widgets/privacy_info.dart} (90%) delete mode 100644 lib/view/pages/talk/chatList.dart create mode 100644 lib/view/pages/talk/chat_list.dart rename lib/view/pages/talk/{chatView.dart => chat_view.dart} (93%) rename lib/view/pages/talk/{components/chatBubbleStyles.dart => data/chat_bubble_styles.dart} (97%) rename lib/view/pages/talk/{components/chatMessage.dart => data/chat_message.dart} (95%) rename lib/view/pages/talk/{chatDetails/chatInfo.dart => details/chat_info.dart} (91%) rename lib/view/pages/talk/{messageReactions.dart => details/message_reactions.dart} (85%) rename lib/view/pages/talk/{chatDetails/participants/participantsListView.dart => details/participants_list_view.dart} (91%) rename lib/view/pages/talk/{joinChat.dart => join_chat.dart} (96%) rename lib/view/pages/talk/{searchChat.dart => search_chat.dart} (96%) rename lib/view/pages/talk/{talkNavigator.dart => talk_navigator.dart} (100%) rename lib/view/pages/talk/{components/answerReference.dart => widgets/answer_reference.dart} (98%) rename lib/view/pages/talk/{components/chatBubble.dart => widgets/chat_bubble.dart} (60%) create mode 100644 lib/view/pages/talk/widgets/chat_message_options_dialog.dart rename lib/view/pages/talk/{components/chatTextfield.dart => widgets/chat_textfield.dart} (98%) rename lib/view/pages/talk/{components/chatTile.dart => widgets/chat_tile.dart} (96%) rename lib/view/pages/talk/{components/pollOptionsList.dart => widgets/poll_options_list.dart} (98%) rename lib/view/pages/talk/{components/splitViewPlaceholder.dart => widgets/split_view_placeholder.dart} (94%) delete mode 100644 lib/view/settings/settings.dart rename lib/widget/{animatedTime.dart => animated_time.dart} (100%) rename lib/widget/{centeredLeading.dart => centered_leading.dart} (100%) rename lib/widget/{clickableAppBar.dart => clickable_app_bar.dart} (100%) rename lib/widget/{confirmDialog.dart => confirm_dialog.dart} (100%) rename lib/widget/debug/{cacheView.dart => cache_view.dart} (97%) rename lib/widget/debug/{debugTile.dart => debug_tile.dart} (94%) rename lib/widget/debug/{jsonViewer.dart => json_viewer.dart} (100%) rename lib/widget/{dropdownDisplay.dart => dropdown_display.dart} (100%) rename lib/widget/{filePick.dart => file_pick.dart} (100%) rename lib/widget/{fileViewer.dart => file_viewer.dart} (94%) rename lib/widget/{focusBehaviour.dart => focus_behaviour.dart} (100%) rename lib/widget/{infoDialog.dart => info_dialog.dart} (100%) rename lib/widget/{largeProfilePictureView.dart => large_profile_picture_view.dart} (94%) rename lib/widget/{loadingSpinner.dart => loading_spinner.dart} (100%) rename lib/widget/{placeholderView.dart => placeholder_view.dart} (100%) rename lib/widget/{sharePositionOrigin.dart => share_position_origin.dart} (100%) rename lib/widget/{unimplementedDialog.dart => unimplemented_dialog.dart} (100%) rename lib/widget/{userAvatar.dart => user_avatar.dart} (97%) diff --git a/lib/api/marianumcloud/autocomplete/autocompleteApi.dart b/lib/api/marianumcloud/autocomplete/autocompleteApi.dart index 883168b..8c67694 100644 --- a/lib/api/marianumcloud/autocomplete/autocompleteApi.dart +++ b/lib/api/marianumcloud/autocomplete/autocompleteApi.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:http/http.dart' as http; -import '../../../model/accountData.dart'; -import '../../../model/endpointData.dart'; +import '../../../model/account_data.dart'; +import '../../../model/endpoint_data.dart'; import 'autocompleteResponse.dart'; class AutocompleteApi { diff --git a/lib/api/marianumcloud/files-sharing/fileSharingApi.dart b/lib/api/marianumcloud/files-sharing/fileSharingApi.dart index a2b8317..ab1b180 100644 --- a/lib/api/marianumcloud/files-sharing/fileSharingApi.dart +++ b/lib/api/marianumcloud/files-sharing/fileSharingApi.dart @@ -2,8 +2,8 @@ import 'dart:io'; import 'package:http/http.dart' as http; -import '../../../model/accountData.dart'; -import '../../../model/endpointData.dart'; +import '../../../model/account_data.dart'; +import '../../../model/endpoint_data.dart'; import 'fileSharingApiParams.dart'; class FileSharingApi { diff --git a/lib/api/marianumcloud/talk/talkApi.dart b/lib/api/marianumcloud/talk/talkApi.dart index a72b5f5..5f6c31e 100644 --- a/lib/api/marianumcloud/talk/talkApi.dart +++ b/lib/api/marianumcloud/talk/talkApi.dart @@ -2,8 +2,8 @@ import 'dart:developer'; import 'package:http/http.dart' as http; -import '../../../model/accountData.dart'; -import '../../../model/endpointData.dart'; +import '../../../model/account_data.dart'; +import '../../../model/endpoint_data.dart'; import '../../apiError.dart'; import '../../apiParams.dart'; import '../../apiRequest.dart'; diff --git a/lib/api/marianumcloud/webdav/webdavApi.dart b/lib/api/marianumcloud/webdav/webdavApi.dart index 1baf291..5049153 100644 --- a/lib/api/marianumcloud/webdav/webdavApi.dart +++ b/lib/api/marianumcloud/webdav/webdavApi.dart @@ -1,7 +1,7 @@ import 'package:nextcloud/nextcloud.dart'; -import '../../../model/accountData.dart'; -import '../../../model/endpointData.dart'; +import '../../../model/account_data.dart'; +import '../../../model/endpoint_data.dart'; import '../../apiRequest.dart'; import '../../apiResponse.dart'; diff --git a/lib/api/mhsl/server/userIndex/update/updateUserindex.dart b/lib/api/mhsl/server/userIndex/update/updateUserindex.dart index 9ac1b8c..c020fe3 100644 --- a/lib/api/mhsl/server/userIndex/update/updateUserindex.dart +++ b/lib/api/mhsl/server/userIndex/update/updateUserindex.dart @@ -6,7 +6,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; -import '../../../../../model/accountData.dart'; +import '../../../../../model/account_data.dart'; import '../../../mhslApi.dart'; import 'updateUserIndexParams.dart'; diff --git a/lib/api/webuntis/queries/authenticate/authenticate.dart b/lib/api/webuntis/queries/authenticate/authenticate.dart index 483278e..5f49dc2 100644 --- a/lib/api/webuntis/queries/authenticate/authenticate.dart +++ b/lib/api/webuntis/queries/authenticate/authenticate.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; -import '../../../../model/accountData.dart'; +import '../../../../model/account_data.dart'; import '../../webuntisApi.dart'; import 'authenticateParams.dart'; import 'authenticateResponse.dart'; diff --git a/lib/api/webuntis/webuntisApi.dart b/lib/api/webuntis/webuntisApi.dart index 6b48e01..9008949 100644 --- a/lib/api/webuntis/webuntisApi.dart +++ b/lib/api/webuntis/webuntisApi.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; -import '../../model/endpointData.dart'; +import '../../model/endpoint_data.dart'; import '../apiParams.dart'; import '../apiRequest.dart'; import '../apiResponse.dart'; diff --git a/lib/app.dart b/lib/app.dart index 86b2650..e483fb6 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -11,10 +11,10 @@ import 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart'; import 'api/mhsl/server/userIndex/update/updateUserindex.dart'; import 'main.dart'; import 'widget/breaker/breaker.dart'; -import 'model/dataCleaner.dart'; -import 'notification/notificationController.dart'; -import 'notification/notificationTasks.dart'; -import 'notification/notifyUpdater.dart'; +import 'model/data_cleaner.dart'; +import 'notification/notification_controller.dart'; +import 'notification/notification_tasks.dart'; +import 'notification/notify_updater.dart'; import 'state/app/modules/app_modules.dart'; import 'state/app/modules/breaker/bloc/breaker_bloc.dart'; import 'state/app/modules/chatList/bloc/chat_list_bloc.dart'; @@ -106,7 +106,7 @@ class _AppState extends State with WidgetsBindingObserver { controller: Main.bottomNavigator, navBarOverlap: const NavBarOverlap.none(), backgroundColor: Theme.of(context).colorScheme.primary, - handleAndroidBackButtonPress: false, + handleAndroidBackButtonPress: true, screenTransitionAnimation: const ScreenTransitionAnimation( curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200), diff --git a/lib/extensions/dateTime.dart b/lib/extensions/date_time.dart similarity index 100% rename from lib/extensions/dateTime.dart rename to lib/extensions/date_time.dart diff --git a/lib/extensions/renderNotNull.dart b/lib/extensions/render_not_null.dart similarity index 100% rename from lib/extensions/renderNotNull.dart rename to lib/extensions/render_not_null.dart diff --git a/lib/extensions/timeOfDay.dart b/lib/extensions/time_of_day.dart similarity index 100% rename from lib/extensions/timeOfDay.dart rename to lib/extensions/time_of_day.dart diff --git a/lib/main.dart b/lib/main.dart index 5c3d278..307af7b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,7 +19,7 @@ import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart'; import 'app.dart'; import 'firebase_options.dart'; -import 'model/accountData.dart'; +import 'model/account_data.dart'; import 'widget/breaker/breaker.dart'; import 'state/app/modules/account/bloc/account_bloc.dart'; import 'state/app/modules/account/bloc/account_state.dart'; @@ -29,10 +29,10 @@ import 'state/app/modules/chatList/bloc/chat_list_bloc.dart'; import 'state/app/modules/settings/bloc/settings_cubit.dart'; import 'state/app/modules/timetable/bloc/timetable_bloc.dart'; import 'storage/base/settings.dart'; -import 'theming/darkAppTheme.dart'; -import 'theming/lightAppTheme.dart'; +import 'theming/dark_app_theme.dart'; +import 'theming/light_app_theme.dart'; import 'view/login/login.dart'; -import 'widget/placeholderView.dart'; +import 'widget/placeholder_view.dart'; Future main() async { log('MarianumMobile started'); diff --git a/lib/model/accountData.dart b/lib/model/account_data.dart similarity index 100% rename from lib/model/accountData.dart rename to lib/model/account_data.dart diff --git a/lib/model/dataCleaner.dart b/lib/model/data_cleaner.dart similarity index 100% rename from lib/model/dataCleaner.dart rename to lib/model/data_cleaner.dart diff --git a/lib/model/endpointData.dart b/lib/model/endpoint_data.dart similarity index 97% rename from lib/model/endpointData.dart rename to lib/model/endpoint_data.dart index d5d7893..6bdd4f4 100644 --- a/lib/model/endpointData.dart +++ b/lib/model/endpoint_data.dart @@ -1,5 +1,5 @@ -import 'accountData.dart'; +import 'account_data.dart'; enum EndpointMode { live, diff --git a/lib/notification/notificationController.dart b/lib/notification/notification_controller.dart similarity index 83% rename from lib/notification/notificationController.dart rename to lib/notification/notification_controller.dart index babe229..0a1631f 100644 --- a/lib/notification/notificationController.dart +++ b/lib/notification/notification_controller.dart @@ -1,9 +1,9 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; -import '../widget/debug/debugTile.dart'; -import '../widget/debug/jsonViewer.dart'; -import 'notificationTasks.dart'; +import '../widget/debug/debug_tile.dart'; +import '../widget/debug/json_viewer.dart'; +import 'notification_tasks.dart'; class NotificationController { @pragma('vm:entry-point') @@ -44,7 +44,7 @@ class NotificationController { } static Future onAppOpenedByNotification(RemoteMessage message, BuildContext context) async { - NotificationTasks.navigateToTalk(context); + NotificationTasks.navigateToTalk(context, chatToken: _extractChatToken(message)); NotificationTasks.updateProviders(context); DebugTile(context).run(() { @@ -60,4 +60,12 @@ class NotificationController { )); }); } + + static String? _extractChatToken(RemoteMessage message) { + for (final key in const ['chatToken', 'token', 'roomToken']) { + final value = message.data[key]; + if (value is String && value.isNotEmpty) return value; + } + return null; + } } diff --git a/lib/notification/notificationService.dart b/lib/notification/notification_service.dart similarity index 100% rename from lib/notification/notificationService.dart rename to lib/notification/notification_service.dart diff --git a/lib/notification/notificationTasks.dart b/lib/notification/notification_tasks.dart similarity index 57% rename from lib/notification/notificationTasks.dart rename to lib/notification/notification_tasks.dart index e858f08..e5e43ff 100644 --- a/lib/notification/notificationTasks.dart +++ b/lib/notification/notification_tasks.dart @@ -3,8 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_app_badge/flutter_app_badge.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../main.dart'; -import '../state/app/modules/app_modules.dart'; +import '../routing/app_routes.dart'; import '../state/app/modules/chat/bloc/chat_bloc.dart'; import '../state/app/modules/chatList/bloc/chat_list_bloc.dart'; @@ -18,9 +17,14 @@ class NotificationTasks { context.read().refresh(); } - static void navigateToTalk(BuildContext context) { - final talkTab = AppModule.getBottomBarModules(context).map((e) => e.module).toList().indexOf(Modules.talk); - if (talkTab == -1) return; - Main.bottomNavigator.jumpToTab(talkTab); + /// Switches to the Talk tab. If [chatToken] is provided, also schedules + /// the matching chat to be opened automatically once the chat list view + /// resolves the token (handled inside [ChatList]). + static void navigateToTalk(BuildContext context, {String? chatToken}) { + if (chatToken != null && chatToken.isNotEmpty) { + AppRoutes.openChatByToken(context, chatToken); + } else { + AppRoutes.goToTalkTab(context); + } } } diff --git a/lib/notification/notifyUpdater.dart b/lib/notification/notify_updater.dart similarity index 95% rename from lib/notification/notifyUpdater.dart rename to lib/notification/notify_updater.dart index dd9527a..584b9f7 100644 --- a/lib/notification/notifyUpdater.dart +++ b/lib/notification/notify_updater.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import '../api/mhsl/notify/register/notifyRegister.dart'; import '../api/mhsl/notify/register/notifyRegisterParams.dart'; -import '../model/accountData.dart'; +import '../model/account_data.dart'; import '../state/app/modules/settings/bloc/settings_cubit.dart'; -import '../widget/confirmDialog.dart'; +import '../widget/confirm_dialog.dart'; class NotifyUpdater { static ConfirmDialog enableAfterDisclaimer(SettingsCubit settings) => ConfirmDialog( diff --git a/lib/routing/app_routes.dart b/lib/routing/app_routes.dart new file mode 100644 index 0000000..94b85d9 --- /dev/null +++ b/lib/routing/app_routes.dart @@ -0,0 +1,202 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; + +import '../api/marianumcloud/talk/room/getRoomResponse.dart'; +import '../main.dart'; +import '../model/account_data.dart'; +import '../state/app/modules/app_modules.dart'; +import '../state/app/modules/chatList/bloc/chat_list_bloc.dart'; +import '../state/app/modules/chat/bloc/chat_bloc.dart'; +import '../state/app/modules/marianumMessage/bloc/marianum_message_state.dart'; +import '../view/pages/files/files.dart'; +import '../view/pages/marianum_message/marianum_message_view.dart'; +import '../view/pages/more/feedback/feedback_dialog.dart'; +import '../view/pages/more/roomplan/roomplan.dart'; +import '../view/pages/more/share/qr_share_view.dart'; +import '../view/pages/talk/chat_view.dart'; +import '../view/pages/talk/details/message_reactions.dart'; +import '../view/pages/talk/talk_navigator.dart'; +import '../view/pages/timetable/custom_events/custom_events_view.dart'; +import '../view/pages/settings/settings.dart'; +import '../widget/debug/cache_view.dart'; +import '../widget/file_viewer.dart'; +import '../widget/user_avatar.dart'; + +/// Single entry point for full-page navigations across the app. +/// +/// Every full-page push in modules should go through one of these methods. +/// Dialogs (`showDialog`), bottom sheets (`showStickyFlexibleBottomSheet`, +/// `showAppointmentBottomSheet`), and `Navigator.pop` for closing those +/// remain unchanged and live at the call sites. +class AppRoutes { + AppRoutes._(); + + /// Token of a chat that should be auto-opened in the Talk tab once + /// the chat list view picks it up. Set by [openChatByToken] (e.g. from + /// a tapped notification) and consumed by the `ChatList` widget. + static final ValueNotifier pendingChatToken = ValueNotifier(null); + + // -- Files -------------------------------------------------------------- + + static void openFolder(BuildContext context, List path) { + pushScreen(context, withNavBar: false, screen: Files(path: path)); + } + + static void openFileViewer(BuildContext context, String localPath, {bool openExternal = false}) { + pushScreen( + context, + withNavBar: false, + screen: FileViewer(path: localPath, openExternal: openExternal), + ); + } + + // -- Timetable ---------------------------------------------------------- + + static void openCustomEvents(BuildContext context) { + pushScreen(context, withNavBar: false, screen: const CustomEventsView()); + } + + // -- Marianum Message --------------------------------------------------- + + static void openMarianumMessage(BuildContext context, String basePath, MarianumMessage message) { + pushScreen( + context, + withNavBar: false, + screen: MessageView(basePath: basePath, message: message), + ); + } + + // -- Sharing / Settings / Feedback / DevTools --------------------------- + + static void openQrShare(BuildContext context) { + pushScreen(context, withNavBar: false, screen: const QrShareView()); + } + + static void openSettings(BuildContext context) { + pushScreen(context, withNavBar: false, screen: const Settings()); + } + + static void openFeedback(BuildContext context) { + pushScreen(context, withNavBar: false, screen: const FeedbackDialog()); + } + + static void openCacheView(BuildContext context) { + pushScreen(context, withNavBar: false, screen: const CacheView()); + } + + static void openRoomplan(BuildContext context) { + pushScreen(context, withNavBar: false, screen: const Roomplan()); + } + + // -- Talk --------------------------------------------------------------- + + static void openMessageReactions(BuildContext context, String token, int messageId) { + pushScreen( + context, + withNavBar: false, + screen: MessageReactions(token: token, messageId: messageId), + ); + } + + /// Opens a chat from a known [GetRoomResponseObject]. Delegates to + /// [TalkNavigator.pushSplitView] so tablet split-view behaviour stays intact. + static void openChatView( + BuildContext context, { + required GetRoomResponseObject room, + required String selfId, + required UserAvatar avatar, + bool overrideToSingleSubScreen = true, + }) { + TalkNavigator.pushSplitView( + context, + ChatView(room: room, selfId: selfId, avatar: avatar), + overrideToSingleSubScreen: overrideToSingleSubScreen, + ); + context.read().setToken(room.token); + } + + /// Schedules a chat to be opened in the Talk tab. Use this when only the + /// token is known (e.g. from a tapped notification) — the actual push + /// happens inside the `ChatList` widget once the room is available. + static void openChatByToken(BuildContext context, String token) { + pendingChatToken.value = token; + goToTalkTab(context); + try { + context.read().refresh(); + } catch (e) { + if (kDebugMode) debugPrint('openChatByToken: ChatListBloc refresh failed: $e'); + } + } + + /// Resolves a pending chat token (set via [openChatByToken]) using the + /// [ChatListBloc]'s current rooms and the active [AccountData] credentials. + /// Returns `null` if the token cannot yet be matched (e.g. the room is + /// still being loaded). Callers should keep listening to [pendingChatToken] + /// and the bloc state and retry when either changes. + static ResolvedPendingChat? resolvePendingChat(BuildContext context) { + final token = pendingChatToken.value; + if (token == null) return null; + if (!AccountData().isPopulated()) return null; + + final rooms = context.read().state.data?.rooms; + final room = _findRoomByToken(rooms, token); + if (room == null) return null; + + final isGroup = room.type != GetRoomResponseObjectConversationType.oneToOne; + final avatar = UserAvatar(id: isGroup ? room.token : room.name, isGroup: isGroup); + return ResolvedPendingChat( + room: room, + selfId: AccountData().getUsername(), + avatar: avatar, + ); + } + + static GetRoomResponseObject? _findRoomByToken(GetRoomResponse? rooms, String token) { + if (rooms == null) return null; + for (final room in rooms.data) { + if (room.token == token) return room; + } + return null; + } + + // -- Module / Tab navigation ------------------------------------------- + + /// Opens an [AppModule]'s root view as a full screen push (used by the + /// "Mehr" tab list). Modules that live in the bottom bar are reached via + /// [goToTab] instead. + static void openModule(BuildContext context, AppModule module) { + pushScreen(context, withNavBar: false, screen: module.create()); + } + + /// Switches the bottom navigation to the given [module] if it is currently + /// in the bottom bar. Returns `true` if the jump happened. + static bool goToTab(BuildContext context, Modules module) { + final index = AppModule.getBottomBarModules(context) + .map((e) => e.module) + .toList() + .indexOf(module); + if (index == -1) return false; + Main.bottomNavigator.jumpToTab(index); + return true; + } + + /// Convenience wrapper for the Talk tab — preserved for the notification + /// handler API which only knows about Talk. + static void goToTalkTab(BuildContext context) { + goToTab(context, Modules.talk); + } +} + +class ResolvedPendingChat { + final GetRoomResponseObject room; + final String selfId; + final UserAvatar avatar; + + const ResolvedPendingChat({ + required this.room, + required this.selfId, + required this.avatar, + }); +} diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart index ba158b7..169d489 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../../../widget/infoDialog.dart'; +import '../../../../../widget/info_dialog.dart'; import '../bloc/loadable_state_bloc.dart'; class LoadableStateErrorBar extends StatelessWidget { diff --git a/lib/state/app/modules/app_modules.dart b/lib/state/app/modules/app_modules.dart index 81bb355..b93c698 100644 --- a/lib/state/app/modules/app_modules.dart +++ b/lib/state/app/modules/app_modules.dart @@ -2,23 +2,24 @@ import 'package:flutter/material.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:badges/badges.dart' as badges; + import '../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart'; -import '../../../widget/breaker/breaker.dart'; +import '../../../routing/app_routes.dart'; import '../../../view/pages/files/files.dart'; +import '../../../view/pages/grade_averages/grade_averages_view.dart'; +import '../../../view/pages/holidays/holidays_view.dart'; +import '../../../view/pages/marianum_dates/marianum_dates_view.dart'; +import '../../../view/pages/marianum_message/marianum_message_list_view.dart'; import '../../../view/pages/more/roomplan/roomplan.dart'; -import '../../../view/pages/talk/chatList.dart'; +import '../../../view/pages/talk/chat_list.dart'; import '../../../view/pages/timetable/timetable.dart'; -import '../../../widget/centeredLeading.dart'; +import '../../../widget/breaker/breaker.dart'; +import '../../../widget/centered_leading.dart'; +import '../infrastructure/loadableState/loadable_state.dart'; import 'chatList/bloc/chat_list_bloc.dart'; import 'chatList/bloc/chat_list_state.dart'; import 'settings/bloc/settings_cubit.dart'; -import '../infrastructure/loadableState/loadable_state.dart'; -import 'gradeAverages/view/grade_averages_view.dart'; -import 'holidays/view/holidays_view.dart'; -import 'marianumDates/view/marianum_dates_view.dart'; -import 'marianumMessage/view/marianum_message_list_view.dart'; - -import 'package:badges/badges.dart' as badges; class AppModule { Modules module; @@ -120,7 +121,7 @@ class AppModule { key: key, leading: CenteredLeading(icon()), title: Text(name), - onTap: isReorder ? null : () => pushScreen(context, withNavBar: false, screen: create()), + onTap: isReorder ? null : () => AppRoutes.openModule(context, this), trailing: isReorder ? Row(mainAxisSize: MainAxisSize.min, children: [ IconButton(onPressed: onVisibleChange, icon: Icon(isVisible ? Icons.visibility_outlined : Icons.visibility_off_outlined)), diff --git a/lib/state/app/modules/settings/bloc/settings_cubit.dart b/lib/state/app/modules/settings/bloc/settings_cubit.dart index 68ca12a..0fde846 100644 --- a/lib/state/app/modules/settings/bloc/settings_cubit.dart +++ b/lib/state/app/modules/settings/bloc/settings_cubit.dart @@ -4,7 +4,7 @@ import 'package:easy_debounce/easy_debounce.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import '../../../../../storage/base/settings.dart'; -import '../../../../../view/settings/defaultSettings.dart'; +import '../../../../../view/pages/settings/data/default_settings.dart'; import '../../app_modules.dart'; class SettingsCubit extends HydratedCubit { diff --git a/lib/state/app/modules/timetable/dataProvider/timetable_data_provider.dart b/lib/state/app/modules/timetable/dataProvider/timetable_data_provider.dart index 5d394c0..8e4766b 100644 --- a/lib/state/app/modules/timetable/dataProvider/timetable_data_provider.dart +++ b/lib/state/app/modules/timetable/dataProvider/timetable_data_provider.dart @@ -20,7 +20,7 @@ import '../../../../../api/webuntis/queries/getTimegridUnits/getTimegridUnitsCac import '../../../../../api/webuntis/queries/getTimegridUnits/getTimegridUnitsResponse.dart'; import '../../../../../api/webuntis/queries/getTimetable/getTimetableCache.dart'; import '../../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart'; -import '../../../../../model/accountData.dart'; +import '../../../../../model/account_data.dart'; class TimetableDataProvider { static final DateFormat _dateFormat = DateFormat('yyyyMMdd'); diff --git a/lib/storage/timetable/timetable_name_mode.dart b/lib/storage/timetable/timetable_name_mode.dart index d6aeeb2..36e0f7e 100644 --- a/lib/storage/timetable/timetable_name_mode.dart +++ b/lib/storage/timetable/timetable_name_mode.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../widget/dropdownDisplay.dart'; +import '../../widget/dropdown_display.dart'; enum TimetableNameMode { name, longName, alternateName } diff --git a/lib/theming/appTheme.dart b/lib/theming/app_theme.dart similarity index 93% rename from lib/theming/appTheme.dart rename to lib/theming/app_theme.dart index c122cc2..cf831c0 100644 --- a/lib/theming/appTheme.dart +++ b/lib/theming/app_theme.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../widget/dropdownDisplay.dart'; +import '../widget/dropdown_display.dart'; class AppTheme { static DropdownDisplay getDisplayOptions(ThemeMode theme) { diff --git a/lib/theming/darkAppTheme.dart b/lib/theming/dark_app_theme.dart similarity index 100% rename from lib/theming/darkAppTheme.dart rename to lib/theming/dark_app_theme.dart diff --git a/lib/theming/lightAppTheme.dart b/lib/theming/light_app_theme.dart similarity index 100% rename from lib/theming/lightAppTheme.dart rename to lib/theming/light_app_theme.dart diff --git a/lib/utils/FileSaver.dart b/lib/utils/file_saver.dart similarity index 100% rename from lib/utils/FileSaver.dart rename to lib/utils/file_saver.dart diff --git a/lib/utils/UrlOpener.dart b/lib/utils/url_opener.dart similarity index 100% rename from lib/utils/UrlOpener.dart rename to lib/utils/url_opener.dart diff --git a/lib/view/login/login.dart b/lib/view/login/login.dart index 167b06f..73df505 100644 --- a/lib/view/login/login.dart +++ b/lib/view/login/login.dart @@ -7,7 +7,7 @@ import 'package:flutter_login/flutter_login.dart'; import '../../api/marianumcloud/talk/room/getRoom.dart'; import '../../api/marianumcloud/talk/room/getRoomParams.dart'; -import '../../model/accountData.dart'; +import '../../model/account_data.dart'; import '../../state/app/modules/account/bloc/account_bloc.dart'; import '../../state/app/modules/account/bloc/account_state.dart'; diff --git a/lib/view/pages/files/fileUploadDialog.dart b/lib/view/pages/files/fileUploadDialog.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/view/pages/files/files.dart b/lib/view/pages/files/files.dart index 257006c..9f98cea 100644 --- a/lib/view/pages/files/files.dart +++ b/lib/view/pages/files/files.dart @@ -12,10 +12,10 @@ import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart'; import '../../../state/app/modules/files/bloc/files_bloc.dart'; import '../../../state/app/modules/files/bloc/files_state.dart'; import '../../../state/app/modules/settings/bloc/settings_cubit.dart'; -import '../../../widget/filePick.dart'; -import '../../../widget/placeholderView.dart'; -import 'fileElement.dart'; -import 'filesUploadDialog.dart'; +import '../../../widget/file_pick.dart'; +import '../../../widget/placeholder_view.dart'; +import 'widgets/file_element.dart'; +import 'files_upload_dialog.dart'; class BetterSortOption { String displayName; diff --git a/lib/view/pages/files/filesUploadDialog.dart b/lib/view/pages/files/files_upload_dialog.dart similarity index 99% rename from lib/view/pages/files/filesUploadDialog.dart rename to lib/view/pages/files/files_upload_dialog.dart index bc0112f..d4d53fd 100644 --- a/lib/view/pages/files/filesUploadDialog.dart +++ b/lib/view/pages/files/files_upload_dialog.dart @@ -6,8 +6,8 @@ import 'package:nextcloud/nextcloud.dart'; import 'package:uuid/uuid.dart'; import '../../../api/marianumcloud/webdav/webdavApi.dart'; -import '../../../widget/confirmDialog.dart'; -import '../../../widget/focusBehaviour.dart'; +import '../../../widget/confirm_dialog.dart'; +import '../../../widget/focus_behaviour.dart'; class FilesUploadDialog extends StatefulWidget { final List filePaths; diff --git a/lib/view/pages/files/fileElement.dart b/lib/view/pages/files/widgets/file_element.dart similarity index 86% rename from lib/view/pages/files/fileElement.dart rename to lib/view/pages/files/widgets/file_element.dart index 6743547..dc2c626 100644 --- a/lib/view/pages/files/fileElement.dart +++ b/lib/view/pages/files/widgets/file_element.dart @@ -7,19 +7,18 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:open_filex/open_filex.dart'; -import '../../../widget/infoDialog.dart'; +import '../../../../widget/info_dialog.dart'; import 'package:nextcloud/nextcloud.dart'; import 'package:path_provider/path_provider.dart'; -import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart'; -import '../../../api/marianumcloud/webdav/webdavApi.dart'; -import '../../../model/accountData.dart'; -import '../../../model/endpointData.dart'; -import '../../../widget/centeredLeading.dart'; -import '../../../widget/confirmDialog.dart'; -import '../../../widget/fileViewer.dart'; -import '../../../widget/unimplementedDialog.dart'; -import 'files.dart'; +import '../../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart'; +import '../../../../api/marianumcloud/webdav/webdavApi.dart'; +import '../../../../model/account_data.dart'; +import '../../../../model/endpoint_data.dart'; +import '../../../../routing/app_routes.dart'; +import '../../../../widget/centered_leading.dart'; +import '../../../../widget/confirm_dialog.dart'; +import '../../../../widget/unimplemented_dialog.dart'; class FileElement extends StatefulWidget { final CacheableFile file; @@ -45,12 +44,8 @@ class FileElement extends StatefulWidget { deleteOnCancel: true, client: Dio(BaseOptions(headers: AccountData().authHeaders())), onDone: () { - //Future result = OpenFile.open(local); // TODO legacy - refactor: remove onDone parameter - Navigator.of(context).push(MaterialPageRoute(builder: (context) => FileViewer(path: local))); + AppRoutes.openFileViewer(context, local); onDone(OpenResult(message: 'File viewer opened', type: ResultType.done)); - // result.then((value) => { - // onDone(value) - // }); }, ); @@ -101,9 +96,7 @@ class _FileElementState extends State { trailing: Icon(widget.file.isDirectory ? Icons.arrow_right : null), onTap: () { if(widget.file.isDirectory) { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => Files(path: widget.path.toList()..add(widget.file.name)), - )); + AppRoutes.openFolder(context, widget.path.toList()..add(widget.file.name)); } else { if(EndpointData().getEndpointMode() == EndpointMode.stage) { InfoDialog.show(context, 'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!'); diff --git a/lib/state/app/modules/gradeAverages/view/grade_averages_list_view.dart b/lib/view/pages/grade_averages/grade_averages_list_view.dart similarity index 93% rename from lib/state/app/modules/gradeAverages/view/grade_averages_list_view.dart rename to lib/view/pages/grade_averages/grade_averages_list_view.dart index 3366a9c..d4152b2 100644 --- a/lib/state/app/modules/gradeAverages/view/grade_averages_list_view.dart +++ b/lib/view/pages/grade_averages/grade_averages_list_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../bloc/grade_averages_bloc.dart'; -import '../bloc/grade_averages_event.dart'; +import '../../../state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart'; +import '../../../state/app/modules/gradeAverages/bloc/grade_averages_event.dart'; class GradeAveragesListView extends StatelessWidget { const GradeAveragesListView({super.key}); diff --git a/lib/state/app/modules/gradeAverages/view/grade_averages_view.dart b/lib/view/pages/grade_averages/grade_averages_view.dart similarity index 93% rename from lib/state/app/modules/gradeAverages/view/grade_averages_view.dart rename to lib/view/pages/grade_averages/grade_averages_view.dart index 03d396a..1a49e0f 100644 --- a/lib/state/app/modules/gradeAverages/view/grade_averages_view.dart +++ b/lib/view/pages/grade_averages/grade_averages_view.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../../../widget/confirmDialog.dart'; -import '../bloc/grade_averages_bloc.dart'; -import '../bloc/grade_averages_event.dart'; -import '../bloc/grade_averages_state.dart'; +import '../../../state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart'; +import '../../../state/app/modules/gradeAverages/bloc/grade_averages_event.dart'; +import '../../../state/app/modules/gradeAverages/bloc/grade_averages_state.dart'; +import '../../../widget/confirm_dialog.dart'; import 'grade_averages_list_view.dart'; class GradeAveragesView extends StatelessWidget { diff --git a/lib/state/app/modules/holidays/view/holidays_view.dart b/lib/view/pages/holidays/holidays_view.dart similarity index 87% rename from lib/state/app/modules/holidays/view/holidays_view.dart rename to lib/view/pages/holidays/holidays_view.dart index 41be1f3..6e9515c 100644 --- a/lib/state/app/modules/holidays/view/holidays_view.dart +++ b/lib/view/pages/holidays/holidays_view.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; -import '../../../../../widget/animatedTime.dart'; -import '../../../../../widget/list_view_util.dart'; -import '../../../../../widget/centeredLeading.dart'; -import '../../../../../widget/debug/debugTile.dart'; -import '../../../../../widget/string_extensions.dart'; -import '../../../infrastructure/loadableState/loadable_state.dart'; -import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart'; -import '../../../infrastructure/utilityWidgets/bloc_module.dart'; -import '../bloc/holidays_bloc.dart'; -import '../bloc/holidays_event.dart'; -import '../bloc/holidays_state.dart'; +import '../../../state/app/infrastructure/loadableState/loadable_state.dart'; +import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart'; +import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart'; +import '../../../state/app/modules/holidays/bloc/holidays_bloc.dart'; +import '../../../state/app/modules/holidays/bloc/holidays_event.dart'; +import '../../../state/app/modules/holidays/bloc/holidays_state.dart'; +import '../../../widget/animated_time.dart'; +import '../../../widget/centered_leading.dart'; +import '../../../widget/debug/debug_tile.dart'; +import '../../../widget/list_view_util.dart'; +import '../../../widget/string_extensions.dart'; class HolidaysView extends StatelessWidget { const HolidaysView({super.key}); diff --git a/lib/state/app/modules/marianumDates/view/marianum_dates_view.dart b/lib/view/pages/marianum_dates/marianum_dates_view.dart similarity index 86% rename from lib/state/app/modules/marianumDates/view/marianum_dates_view.dart rename to lib/view/pages/marianum_dates/marianum_dates_view.dart index 4f283d3..081d9e8 100644 --- a/lib/state/app/modules/marianumDates/view/marianum_dates_view.dart +++ b/lib/view/pages/marianum_dates/marianum_dates_view.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; -import '../../../../../view/pages/timetable/custom_events/custom_event_edit_dialog.dart'; -import '../../../../../widget/animatedTime.dart'; -import '../../../../../widget/centeredLeading.dart'; -import '../../../../../widget/debug/debugTile.dart'; -import '../../../../../widget/list_view_util.dart'; -import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart'; -import '../../../infrastructure/utilityWidgets/bloc_module.dart'; -import '../../../infrastructure/loadableState/loadable_state.dart'; -import '../bloc/marianum_dates_bloc.dart'; -import '../bloc/marianum_dates_event.dart'; -import '../bloc/marianum_dates_state.dart'; +import '../../../state/app/infrastructure/loadableState/loadable_state.dart'; +import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart'; +import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart'; +import '../../../state/app/modules/marianumDates/bloc/marianum_dates_bloc.dart'; +import '../../../state/app/modules/marianumDates/bloc/marianum_dates_event.dart'; +import '../../../state/app/modules/marianumDates/bloc/marianum_dates_state.dart'; +import '../../../widget/animated_time.dart'; +import '../../../widget/centered_leading.dart'; +import '../../../widget/debug/debug_tile.dart'; +import '../../../widget/list_view_util.dart'; +import '../timetable/custom_events/custom_event_edit_dialog.dart'; class MarianumDatesView extends StatelessWidget { const MarianumDatesView({super.key}); diff --git a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart b/lib/view/pages/marianum_message/marianum_message_list_view.dart similarity index 69% rename from lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart rename to lib/view/pages/marianum_message/marianum_message_list_view.dart index 1957886..7164219 100644 --- a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart +++ b/lib/view/pages/marianum_message/marianum_message_list_view.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'marianum_message_view.dart'; -import '../../../infrastructure/loadableState/loadable_state.dart'; -import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart'; -import '../../../infrastructure/utilityWidgets/bloc_module.dart'; -import '../bloc/marianum_message_bloc.dart'; -import '../bloc/marianum_message_state.dart'; +import '../../../routing/app_routes.dart'; +import '../../../state/app/infrastructure/loadableState/loadable_state.dart'; +import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart'; +import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart'; +import '../../../state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart'; +import '../../../state/app/modules/marianumMessage/bloc/marianum_message_state.dart'; class MarianumMessageListView extends StatelessWidget { const MarianumMessageListView({super.key}); @@ -31,7 +31,7 @@ class MarianumMessageListView extends StatelessWidget { subtitle: Text('vom ${message.date}'), trailing: const Icon(Icons.arrow_right), onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: state.messageList.base, message: message))); + AppRoutes.openMarianumMessage(context, state.messageList.base, message); }, ); } diff --git a/lib/state/app/modules/marianumMessage/view/marianum_message_view.dart b/lib/view/pages/marianum_message/marianum_message_view.dart similarity index 92% rename from lib/state/app/modules/marianumMessage/view/marianum_message_view.dart rename to lib/view/pages/marianum_message/marianum_message_view.dart index ae8fd36..f95712a 100644 --- a/lib/state/app/modules/marianumMessage/view/marianum_message_view.dart +++ b/lib/view/pages/marianum_message/marianum_message_view.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../bloc/marianum_message_state.dart'; -import '../../../../../widget/confirmDialog.dart'; +import '../../../state/app/modules/marianumMessage/bloc/marianum_message_state.dart'; +import '../../../widget/confirm_dialog.dart'; class MessageView extends StatefulWidget { final String basePath; diff --git a/lib/view/pages/more/feedback/feedbackDialog.dart b/lib/view/pages/more/feedback/feedback_dialog.dart similarity index 97% rename from lib/view/pages/more/feedback/feedbackDialog.dart rename to lib/view/pages/more/feedback/feedback_dialog.dart index 1b532f2..373883c 100644 --- a/lib/view/pages/more/feedback/feedbackDialog.dart +++ b/lib/view/pages/more/feedback/feedback_dialog.dart @@ -11,11 +11,11 @@ import 'package:badges/badges.dart' as badges; import '../../../../api/mhsl/server/feedback/addFeedback.dart'; import '../../../../api/mhsl/server/feedback/addFeedbackParams.dart'; -import '../../../../model/accountData.dart'; +import '../../../../model/account_data.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; -import '../../../../widget/filePick.dart'; -import '../../../../widget/focusBehaviour.dart'; -import '../../../../widget/infoDialog.dart'; +import '../../../../widget/file_pick.dart'; +import '../../../../widget/focus_behaviour.dart'; +import '../../../../widget/info_dialog.dart'; class FeedbackDialog extends StatefulWidget { const FeedbackDialog({super.key}); diff --git a/lib/view/pages/more/share/appSharePlatformView.dart b/lib/view/pages/more/share/app_share_platform_view.dart similarity index 100% rename from lib/view/pages/more/share/appSharePlatformView.dart rename to lib/view/pages/more/share/app_share_platform_view.dart diff --git a/lib/view/pages/more/share/qrShareView.dart b/lib/view/pages/more/share/qr_share_view.dart similarity index 97% rename from lib/view/pages/more/share/qrShareView.dart rename to lib/view/pages/more/share/qr_share_view.dart index 8aba4c8..ec66db1 100644 --- a/lib/view/pages/more/share/qrShareView.dart +++ b/lib/view/pages/more/share/qr_share_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:screen_brightness/screen_brightness.dart'; -import 'appSharePlatformView.dart'; +import 'app_share_platform_view.dart'; class QrShareView extends StatefulWidget { const QrShareView({super.key}); diff --git a/lib/view/pages/more/share/selectShareTypeDialog.dart b/lib/view/pages/more/share/select_share_type_dialog.dart similarity index 84% rename from lib/view/pages/more/share/selectShareTypeDialog.dart rename to lib/view/pages/more/share/select_share_type_dialog.dart index 001c86b..50c9491 100644 --- a/lib/view/pages/more/share/selectShareTypeDialog.dart +++ b/lib/view/pages/more/share/select_share_type_dialog.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:share_plus/share_plus.dart'; -import '../../../../widget/sharePositionOrigin.dart'; -import 'qrShareView.dart'; +import '../../../../widget/share_position_origin.dart'; + +enum ShareTargetType { qr } class SelectShareTypeDialog extends StatelessWidget { const SelectShareTypeDialog({super.key}); @@ -14,15 +15,14 @@ class SelectShareTypeDialog extends StatelessWidget { leading: const Icon(Icons.qr_code_2_outlined), title: const Text('Per QR-Code'), trailing: const Icon(Icons.arrow_right), - onTap: () { - Navigator.of(context).push(MaterialPageRoute(builder: (context) => const QrShareView())); - }, + onTap: () => Navigator.of(context).pop(ShareTargetType.qr), ), ListTile( leading: const Icon(Icons.link_outlined), title: const Text('Per Link teilen'), trailing: const Icon(Icons.arrow_right), onTap: () { + Navigator.of(context).pop(); SharePlus.instance.share(ShareParams( sharePositionOrigin: SharePositionOrigin.get(context), subject: 'App Teilen', diff --git a/lib/view/pages/overhang.dart b/lib/view/pages/overhang.dart index 074e3e9..c43412f 100644 --- a/lib/view/pages/overhang.dart +++ b/lib/view/pages/overhang.dart @@ -4,18 +4,16 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:in_app_review/in_app_review.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../extensions/renderNotNull.dart'; -import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; +import '../../extensions/render_not_null.dart'; +import '../../routing/app_routes.dart'; import '../../state/app/modules/app_modules.dart'; import '../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../storage/base/settings.dart' as model; -import '../../widget/centeredLeading.dart'; -import '../../widget/infoDialog.dart'; -import '../settings/defaultSettings.dart'; -import '../settings/settings.dart'; -import 'more/feedback/feedbackDialog.dart'; -import 'more/share/selectShareTypeDialog.dart'; +import '../../widget/centered_leading.dart'; +import '../../widget/info_dialog.dart'; +import 'settings/data/default_settings.dart'; +import 'more/share/select_share_type_dialog.dart'; class Overhang extends StatefulWidget { const Overhang({super.key}); @@ -41,7 +39,7 @@ class _OverhangState extends State { icon: Icon(Icons.undo_outlined) ), IconButton(onPressed: () => setState(() => editMode = !editMode), icon: Icon(Icons.edit_note_outlined), color: editMode ? Theme.of(context).primaryColor : null), - IconButton(onPressed: editMode ? null : () => pushScreen(context, screen: const Settings(), withNavBar: false), icon: const Icon(Icons.settings)), + IconButton(onPressed: editMode ? null : () => AppRoutes.openSettings(context), icon: const Icon(Icons.settings)), ], ), body: editMode ? _sorting() : _overhang(), @@ -92,7 +90,14 @@ class _OverhangState extends State { title: const Text('Teile die App'), subtitle: const Text('Mit Freunden und deiner Klasse teilen'), trailing: const Icon(Icons.arrow_right), - onTap: () => showDialog(context: context, builder: (context) => const SelectShareTypeDialog()) + onTap: () async { + final result = await showDialog( + context: context, + builder: (_) => const SelectShareTypeDialog(), + ); + if (!mounted || result != ShareTargetType.qr) return; + if (context.mounted) AppRoutes.openQrShare(context); + }, ), FutureBuilder( future: InAppReview.instance.isAvailable(), @@ -130,7 +135,7 @@ class _OverhangState extends State { title: const Text('Du hast eine Idee?'), subtitle: const Text('Fehler und Verbessungsvorschläge'), trailing: const Icon(Icons.arrow_right), - onTap: () => pushScreen(context, withNavBar: false, screen: const FeedbackDialog()), + onTap: () => AppRoutes.openFeedback(context), ), ], ); diff --git a/lib/view/settings/defaultSettings.dart b/lib/view/pages/settings/data/default_settings.dart similarity index 68% rename from lib/view/settings/defaultSettings.dart rename to lib/view/pages/settings/data/default_settings.dart index 3e2fac8..18088d0 100644 --- a/lib/view/settings/defaultSettings.dart +++ b/lib/view/pages/settings/data/default_settings.dart @@ -2,18 +2,18 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import '../../state/app/modules/app_modules.dart'; -import '../../storage/base/settings.dart'; -import '../../storage/devTools/devToolsSettings.dart'; -import '../../storage/file/fileSettings.dart'; -import '../../storage/fileView/fileViewSettings.dart'; -import '../../storage/general/modulesSettings.dart'; -import '../../storage/holidays/holidaysSettings.dart'; -import '../../storage/notification/notificationSettings.dart'; -import '../../storage/talk/talkSettings.dart'; -import '../../storage/timetable/timetableSettings.dart'; -import '../pages/files/files.dart'; -import '../../storage/timetable/timetable_name_mode.dart'; +import '../../../../state/app/modules/app_modules.dart'; +import '../../../../storage/base/settings.dart'; +import '../../../../storage/devTools/devToolsSettings.dart'; +import '../../../../storage/file/fileSettings.dart'; +import '../../../../storage/fileView/fileViewSettings.dart'; +import '../../../../storage/general/modulesSettings.dart'; +import '../../../../storage/holidays/holidaysSettings.dart'; +import '../../../../storage/notification/notificationSettings.dart'; +import '../../../../storage/talk/talkSettings.dart'; +import '../../../../storage/timetable/timetable_name_mode.dart'; +import '../../../../storage/timetable/timetableSettings.dart'; +import '../../files/files.dart'; class DefaultSettings { static Settings get() => Settings( diff --git a/lib/view/pages/settings/sections/about_section.dart b/lib/view/pages/settings/sections/about_section.dart new file mode 100644 index 0000000..0784a6d --- /dev/null +++ b/lib/view/pages/settings/sections/about_section.dart @@ -0,0 +1,135 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../../../../widget/centered_leading.dart'; +import '../../../../widget/confirm_dialog.dart'; +import '../data/default_settings.dart'; +import '../widgets/privacy_info.dart'; +import 'dev_tools_section.dart'; + +class AboutSection extends StatelessWidget { + const AboutSection({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.read(); + return Column( + children: [ + ListTile( + leading: const Icon(Icons.live_help_outlined), + title: const Text('Informationen und Lizenzen'), + onTap: () => _showAppInfo(context), + trailing: const Icon(Icons.arrow_right), + ), + ListTile( + leading: const Icon(Icons.policy_outlined), + title: const Text('Impressum & Datenschutz'), + onTap: () => _showPrivacyDialog(context), + trailing: const Icon(Icons.arrow_right), + ), + const Divider(), + ListTile( + leading: const CenteredLeading(Icon(Icons.code)), + title: const Text('Quellcode MarianumMobile/Client'), + subtitle: const Text('GNU GPL v3'), + onTap: () => ConfirmDialog.openBrowser(context, 'https://mhsl.eu/gitea/MarianumMobile/Client'), + ), + ListTile( + leading: const Icon(Icons.developer_mode_outlined), + title: const Text('Entwicklermodus'), + trailing: Checkbox( + value: settings.val().devToolsEnabled, + onChanged: (state) => _toggleDeveloperMode(context, settings, state), + ), + ), + Visibility( + visible: settings.val().devToolsEnabled, + child: DevToolsSection(settings: settings), + ), + ], + ); + } + + Future _showAppInfo(BuildContext context) async { + final appInfo = await PackageInfo.fromPlatform(); + if (!context.mounted) return; + showAboutDialog( + context: context, + applicationIcon: const Icon(Icons.apps), + applicationName: 'MarianumMobile', + applicationVersion: '${appInfo.appName}\n\nPackage: ${appInfo.packageName}\nVersion: ${appInfo.version}\nBuild: ${appInfo.buildNumber}', + applicationLegalese: 'Dies ist ein Inoffizieller Nextcloud & Webuntis Client und wird nicht vom Marianum selbst betrieben.\n' + 'Keinerlei Gewähr für Vollständigkeit, Richtigkeit und Aktualität!\n\n' + "${kReleaseMode ? "Production" : "Development"} build\n" + 'Marianum Fulda 2023-${Jiffy.now().year}\nElias Müller', + ); + } + + void _showPrivacyDialog(BuildContext context) => showDialog( + context: context, + builder: (context) => SimpleDialog( + children: [ + ListTile( + leading: const CenteredLeading(Icon(Icons.school_outlined)), + title: const Text('Infos zum Marianum Fulda'), + subtitle: const Text('Für Talk-Chats und Dateien'), + trailing: const Icon(Icons.arrow_right), + onTap: () => PrivacyInfo( + providerText: 'Marianum', + imprintUrl: 'https://www.marianum-fulda.de/impressum', + privacyUrl: 'https://www.marianum-fulda.de/datenschutz', + ).showPopup(context), + ), + ListTile( + leading: const CenteredLeading(Icon(Icons.date_range_outlined)), + title: const Text('Infos zu Web-/ Untis'), + subtitle: const Text('Für den Stundenplan'), + trailing: const Icon(Icons.arrow_right), + onTap: () => PrivacyInfo( + providerText: 'Untis', + imprintUrl: 'https://www.untis.at/impressum', + privacyUrl: 'https://www.untis.at/datenschutz-wu-apps', + ).showPopup(context), + ), + ListTile( + leading: const CenteredLeading(Icon(Icons.send_time_extension_outlined)), + title: const Text('Infos zu mhsl'), + subtitle: const Text('Für Countdowns, Marianum Message und mehr'), + trailing: const Icon(Icons.arrow_right), + onTap: () => PrivacyInfo( + providerText: 'mhsl', + imprintUrl: 'https://mhsl.eu/id.html', + privacyUrl: 'https://mhsl.eu/datenschutz.html', + ).showPopup(context), + ), + ], + ), + ); + + void _toggleDeveloperMode(BuildContext context, SettingsCubit settings, bool? state) { + void apply() { + final enabled = state ?? false; + settings.val(write: true).devToolsEnabled = enabled; + if (!enabled) settings.val(write: true).devToolsSettings = DefaultSettings.get().devToolsSettings; + } + + if (!state!) { + apply(); + return; + } + + ConfirmDialog( + title: 'Entwicklermodus', + content: 'Die Entwickleransicht bietet erweiterte Funktionen, die für den üblichen Gebrauch nicht benötigt werden.\n\n' + 'Die Verwendung der Tools kann darüber hinaus bei falscher Verwendung zu Fehlern führen.\n\n' + 'Aktivieren auf eigene Verantwortung.', + confirmButton: 'Ja, ich verstehe das Risiko', + cancelButton: 'Nein, zurück zur App', + onConfirm: apply, + ).asDialog(context); + } +} diff --git a/lib/view/pages/settings/sections/account_section.dart b/lib/view/pages/settings/sections/account_section.dart new file mode 100644 index 0000000..b7bf522 --- /dev/null +++ b/lib/view/pages/settings/sections/account_section.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../../../model/account_data.dart'; +import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../../../../widget/centered_leading.dart'; +import '../../../../widget/confirm_dialog.dart'; +import '../../../../widget/debug/cache_view.dart'; + +class AccountSection extends StatelessWidget { + const AccountSection({super.key}); + + @override + Widget build(BuildContext context) => ListTile( + leading: const CenteredLeading(Icon(Icons.logout_outlined)), + title: const Text('Konto abmelden'), + subtitle: Text('Angemeldet als ${AccountData().getUsername()}'), + onTap: () => _showLogoutDialog(context), + ); + + void _showLogoutDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + title: 'Abmelden?', + content: 'Möchtest du dich wirklich abmelden?', + confirmButton: 'Abmelden', + onConfirm: () async { + final prefs = await SharedPreferences.getInstance(); + await prefs.clear(); + PaintingBinding.instance.imageCache.clear(); + if (!context.mounted) return; + context.read().reset(); + const CacheView().clear(); + AccountData().removeData(context: context); + }, + ), + ); + } +} diff --git a/lib/view/pages/settings/sections/appearance_section.dart b/lib/view/pages/settings/sections/appearance_section.dart new file mode 100644 index 0000000..f5df162 --- /dev/null +++ b/lib/view/pages/settings/sections/appearance_section.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../../../../theming/app_theme.dart'; + +class AppearanceSection extends StatelessWidget { + const AppearanceSection({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.read(); + return ListTile( + leading: const Icon(Icons.dark_mode_outlined), + title: const Text('Farbgebung'), + trailing: DropdownButton( + value: settings.val().appTheme, + icon: const Icon(Icons.arrow_drop_down), + items: ThemeMode.values + .map((e) => DropdownMenuItem( + value: e, + enabled: e != settings.val().appTheme, + child: Row( + children: [ + Icon(AppTheme.getDisplayOptions(e).icon), + const SizedBox(width: 10), + Text(AppTheme.getDisplayOptions(e).displayName), + ], + ), + )) + .toList(), + onChanged: (e) => settings.val(write: true).appTheme = e!, + ), + ); + } +} diff --git a/lib/view/settings/devToolsSettings.dart b/lib/view/pages/settings/sections/dev_tools_section.dart similarity index 89% rename from lib/view/settings/devToolsSettings.dart rename to lib/view/pages/settings/sections/dev_tools_section.dart index 147861f..619392c 100644 --- a/lib/view/settings/devToolsSettings.dart +++ b/lib/view/pages/settings/sections/dev_tools_section.dart @@ -4,21 +4,22 @@ import 'package:flutter/material.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../state/app/modules/settings/bloc/settings_cubit.dart'; -import '../../widget/centeredLeading.dart'; -import '../../widget/confirmDialog.dart'; -import '../../widget/debug/cacheView.dart'; -import '../../widget/debug/jsonViewer.dart'; +import '../../../../routing/app_routes.dart'; +import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../../../../widget/centered_leading.dart'; +import '../../../../widget/confirm_dialog.dart'; +import '../../../../widget/debug/cache_view.dart'; +import '../../../../widget/debug/json_viewer.dart'; -class DevToolsSettings extends StatefulWidget { +class DevToolsSection extends StatefulWidget { final SettingsCubit settings; - const DevToolsSettings({required this.settings, super.key}); + const DevToolsSection({required this.settings, super.key}); @override - State createState() => _DevToolsSettingsState(); + State createState() => _DevToolsSectionState(); } -class _DevToolsSettingsState extends State { +class _DevToolsSectionState extends State { @override Widget build(BuildContext context) => Column( children: [ @@ -96,9 +97,7 @@ class _DevToolsSettingsState extends State { future: const CacheView().totalSize(), builder: (context, snapshot) => Text("etwa ${snapshot.hasError ? "?" : snapshot.hasData ? filesize(snapshot.data) : "..."}\nLange tippen um zu löschen"), ), - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => const CacheView())); - }, + onTap: () => AppRoutes.openCacheView(context), onLongPress: () { ConfirmDialog( title: 'App-Cache löschen', diff --git a/lib/view/pages/settings/sections/files_section.dart b/lib/view/pages/settings/sections/files_section.dart new file mode 100644 index 0000000..2035648 --- /dev/null +++ b/lib/view/pages/settings/sections/files_section.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; + +class FilesSection extends StatelessWidget { + const FilesSection({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.read(); + return Column( + children: [ + ListTile( + leading: const Icon(Icons.drive_folder_upload_outlined), + title: const Text('Ordner in Dateien nach oben sortieren'), + trailing: Checkbox( + value: settings.val().fileSettings.sortFoldersToTop, + onChanged: (e) => settings.val(write: true).fileSettings.sortFoldersToTop = e!, + ), + ), + ListTile( + leading: const Icon(Icons.open_in_new_outlined), + title: const Text('Dateien immer mit Systemdialog öffnen'), + trailing: Checkbox( + value: settings.val().fileViewSettings.alwaysOpenExternally, + onChanged: (e) => settings.val(write: true).fileViewSettings.alwaysOpenExternally = e!, + ), + ), + ], + ); + } +} diff --git a/lib/view/pages/settings/sections/talk_section.dart b/lib/view/pages/settings/sections/talk_section.dart new file mode 100644 index 0000000..1133c11 --- /dev/null +++ b/lib/view/pages/settings/sections/talk_section.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../notification/notify_updater.dart'; +import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../../../../widget/centered_leading.dart'; + +class TalkSection extends StatelessWidget { + const TalkSection({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.read(); + final talkSettings = settings.val().talkSettings; + final notificationSettings = settings.val().notificationSettings; + return Column( + children: [ + ListTile( + leading: const Icon(Icons.star_border), + title: const Text('Favoriten im Talk nach oben sortieren'), + trailing: Checkbox( + value: talkSettings.sortFavoritesToTop, + onChanged: (e) => settings.val(write: true).talkSettings.sortFavoritesToTop = e!, + ), + ), + ListTile( + leading: const Icon(Icons.mark_email_unread_outlined), + title: const Text('Ungelesene Chats nach oben sortieren'), + trailing: Checkbox( + value: talkSettings.sortUnreadToTop, + onChanged: (e) => settings.val(write: true).talkSettings.sortUnreadToTop = e!, + ), + ), + ListTile( + leading: const CenteredLeading(Icon(Icons.notifications_active_outlined)), + title: const Text('Push-Benachrichtigungen aktivieren'), + subtitle: const Text('Lange tippen für mehr Informationen'), + trailing: Checkbox( + value: notificationSettings.enabled, + onChanged: (e) { + if (e!) { + NotifyUpdater.enableAfterDisclaimer(settings).asDialog(context); + } else { + settings.val(write: true).notificationSettings.enabled = e; + } + }, + ), + onLongPress: () => _showInfoDialog(context), + ), + ], + ); + } + + void _showInfoDialog(BuildContext context) => showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Info über Push'), + content: const SingleChildScrollView( + child: Text( + "Aufgrund technischer Limitationen müssen Push-Nachrichten über einen externen Server - hier 'mhsl.eu' (Author dieser App) - erfolgen.\n\n" + 'Wenn Push aktiviert wird, werden deine Zugangsdaten und ein Token verschlüsselt an den Betreiber gesendet und von ihm unverschlüsselt gespeichert.\n\n' + 'Der extene Server verwendet die Zugangsdaten um sich maschinell in Nextcloud Talk anzumelden und via Websockets auf neue Nachrichten zu warten.\n\n' + 'Wenn eine neue Nachricht eintrifft wird dein Telefon via FBC-Messaging (Google Firebase Push) vom externen Server benachrichtigt.\n\n' + 'Behalte im Hinterkopf, dass deine Zugangsdaten auf einem externen Server gespeichert werden und dies trotz bester Absichten ein Sicherheitsrisiko sein kann!', + ), + ), + actions: [ + TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Zurück')), + ], + ), + ); +} diff --git a/lib/view/pages/settings/sections/timetable_section.dart b/lib/view/pages/settings/sections/timetable_section.dart new file mode 100644 index 0000000..a755eb5 --- /dev/null +++ b/lib/view/pages/settings/sections/timetable_section.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart'; +import '../../../../storage/timetable/timetable_name_mode.dart'; + +class TimetableSection extends StatelessWidget { + const TimetableSection({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.read(); + final timetableBloc = context.read(); + final timetableSettings = settings.val().timetableSettings; + return Column( + children: [ + ListTile( + leading: const Icon(Icons.abc_outlined), + title: const Text('Fachbezeichnung'), + trailing: DropdownButton( + value: timetableSettings.timetableNameMode, + icon: const Icon(Icons.arrow_drop_down), + items: TimetableNameMode.values + .map((e) => DropdownMenuItem( + value: e, + enabled: e != timetableSettings.timetableNameMode, + child: Row( + children: [ + Icon(TimetableNameModes.getDisplayOptions(e).icon), + const SizedBox(width: 10), + Text(TimetableNameModes.getDisplayOptions(e).displayName), + ], + ), + )) + .toList(), + onChanged: (value) { + settings.val(write: true).timetableSettings.timetableNameMode = value!; + timetableBloc.refresh(); + }, + ), + ), + ListTile( + leading: const Icon(Icons.calendar_view_day_outlined), + title: const Text('Doppelstunden zusammenhängend anzeigen'), + trailing: Checkbox( + value: timetableSettings.connectDoubleLessons, + onChanged: (e) { + settings.val(write: true).timetableSettings.connectDoubleLessons = e!; + timetableBloc.refresh(); + }, + ), + ), + ], + ); + } +} diff --git a/lib/view/pages/settings/settings.dart b/lib/view/pages/settings/settings.dart new file mode 100644 index 0000000..b584bd4 --- /dev/null +++ b/lib/view/pages/settings/settings.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../../../storage/base/settings.dart' as model; +import 'sections/about_section.dart'; +import 'sections/account_section.dart'; +import 'sections/appearance_section.dart'; +import 'sections/files_section.dart'; +import 'sections/talk_section.dart'; +import 'sections/timetable_section.dart'; + +class Settings extends StatelessWidget { + const Settings({super.key}); + + @override + Widget build(BuildContext context) => BlocBuilder( + builder: (context, _) => Scaffold( + appBar: AppBar(title: const Text('Einstellungen')), + body: ListView( + children: const [ + AccountSection(), + Divider(), + AppearanceSection(), + Divider(), + TimetableSection(), + Divider(), + TalkSection(), + Divider(), + FilesSection(), + Divider(), + AboutSection(), + ], + ), + ), + ); +} diff --git a/lib/view/settings/privacyInfo.dart b/lib/view/pages/settings/widgets/privacy_info.dart similarity index 90% rename from lib/view/settings/privacyInfo.dart rename to lib/view/pages/settings/widgets/privacy_info.dart index ceb9ea5..13ba668 100644 --- a/lib/view/settings/privacyInfo.dart +++ b/lib/view/pages/settings/widgets/privacy_info.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import '../../widget/centeredLeading.dart'; -import '../../widget/confirmDialog.dart'; +import '../../../../widget/centered_leading.dart'; +import '../../../../widget/confirm_dialog.dart'; class PrivacyInfo { String providerText; diff --git a/lib/view/pages/talk/chatList.dart b/lib/view/pages/talk/chatList.dart deleted file mode 100644 index df0f4bc..0000000 --- a/lib/view/pages/talk/chatList.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_split_view/flutter_split_view.dart'; - -import '../../../state/app/infrastructure/loadableState/loadable_state.dart'; -import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart'; -import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart'; -import '../../../state/app/modules/chatList/bloc/chat_list_bloc.dart'; -import '../../../state/app/modules/chatList/bloc/chat_list_state.dart'; -import '../../../notification/notifyUpdater.dart'; -import '../../../state/app/modules/settings/bloc/settings_cubit.dart'; -import '../../../widget/confirmDialog.dart'; -import 'components/chatTile.dart'; -import 'components/splitViewPlaceholder.dart'; -import 'joinChat.dart'; -import 'searchChat.dart'; - -class ChatList extends StatelessWidget { - const ChatList({super.key}); - - @override - Widget build(BuildContext context) => BlocModule>( - create: (_) => ChatListBloc(), - child: (context, bloc, _) => const _ChatListView(), - ); -} - -class _ChatListView extends StatefulWidget { - const _ChatListView(); - - @override - State<_ChatListView> createState() => _ChatListViewState(); -} - -class _ChatListViewState extends State<_ChatListView> { - late final SettingsCubit _settings; - - @override - void initState() { - super.initState(); - _settings = context.read(); - - WidgetsBinding.instance.addPostFrameCallback((_) => _maybeAskForNotificationPermission()); - } - - void _maybeAskForNotificationPermission() { - final notificationSettings = _settings.val().notificationSettings; - if (notificationSettings.enabled || notificationSettings.askUsageDismissed) return; - - _settings.val(write: true).notificationSettings.askUsageDismissed = true; - ConfirmDialog( - icon: Icons.notifications_active_outlined, - title: 'Benachrichtigungen aktivieren', - content: 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.', - confirmButton: 'Weiter', - onConfirm: () { - FirebaseMessaging.instance.requestPermission(provisional: false).then((value) { - if (!mounted) return; - switch (value.authorizationStatus) { - case AuthorizationStatus.authorized: - NotifyUpdater.enableAfterDisclaimer(_settings).asDialog(context); - break; - case AuthorizationStatus.denied: - showDialog( - context: context, - builder: (_) => const AlertDialog( - content: Text('Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren.'), - ), - ); - break; - default: - break; - } - }); - }, - ).asDialog(context); - } - - @override - Widget build(BuildContext context) { - final bloc = context.read(); - return SplitView.material( - placeholder: const SplitViewPlaceholder(), - breakpoint: 1000, - child: Scaffold( - appBar: AppBar( - title: const Text('Talk'), - actions: [ - IconButton( - icon: const Icon(Icons.search), - onPressed: () { - final rooms = bloc.state.data?.rooms; - if (rooms == null) return; - showSearch(context: context, delegate: SearchChat(rooms.data.toList())); - }, - ), - ], - ), - floatingActionButton: FloatingActionButton( - heroTag: 'createChat', - backgroundColor: Theme.of(context).primaryColor, - onPressed: () { - showSearch(context: context, delegate: JoinChat()).then((username) { - if (username == null || !context.mounted) return; - ConfirmDialog( - title: 'Chat starten', - content: "Möchtest du einen Chat mit Nutzer '$username' starten?", - confirmButton: 'Chat starten', - onConfirm: () { - bloc.createDirectChat(username); - }, - ).asDialog(context); - }); - }, - child: const Icon(Icons.add_comment_outlined), - ), - body: LoadableStateConsumer( - child: (state, _) { - final rooms = state.rooms; - if (rooms == null) return const SizedBox.shrink(); - - final talkSettings = context.watch().val().talkSettings; - final sorted = rooms.sortBy( - lastActivity: true, - favoritesToTop: talkSettings.sortFavoritesToTop, - unreadToTop: talkSettings.sortUnreadToTop, - ); - - return ListView( - padding: EdgeInsets.zero, - children: sorted.map((room) { - final hasDraft = _settings.val().talkSettings.drafts.containsKey(room.token); - return ChatTile(data: room, hasDraft: hasDraft); - }).toList(), - ); - }, - ), - ), - ); - } -} diff --git a/lib/view/pages/talk/chat_list.dart b/lib/view/pages/talk/chat_list.dart new file mode 100644 index 0000000..04c53d2 --- /dev/null +++ b/lib/view/pages/talk/chat_list.dart @@ -0,0 +1,180 @@ +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_split_view/flutter_split_view.dart'; + +import '../../../routing/app_routes.dart'; +import '../../../state/app/infrastructure/loadableState/loadable_state.dart'; +import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart'; +import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart'; +import '../../../state/app/modules/chatList/bloc/chat_list_bloc.dart'; +import '../../../state/app/modules/chatList/bloc/chat_list_state.dart'; +import '../../../notification/notify_updater.dart'; +import '../../../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../../../widget/confirm_dialog.dart'; +import 'widgets/chat_tile.dart'; +import 'widgets/split_view_placeholder.dart'; +import 'join_chat.dart'; +import 'search_chat.dart'; + +class ChatList extends StatelessWidget { + const ChatList({super.key}); + + @override + Widget build(BuildContext context) => BlocModule>( + create: (_) => ChatListBloc(), + child: (context, bloc, _) => const _ChatListView(), + ); +} + +class _ChatListView extends StatefulWidget { + const _ChatListView(); + + @override + State<_ChatListView> createState() => _ChatListViewState(); +} + +class _ChatListViewState extends State<_ChatListView> { + late final SettingsCubit _settings; + + @override + void initState() { + super.initState(); + _settings = context.read(); + + AppRoutes.pendingChatToken.addListener(_maybeOpenPendingChat); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _maybeAskForNotificationPermission(); + _maybeOpenPendingChat(); + }); + } + + @override + void dispose() { + AppRoutes.pendingChatToken.removeListener(_maybeOpenPendingChat); + super.dispose(); + } + + void _maybeOpenPendingChat() { + if (!mounted) return; + final resolved = AppRoutes.resolvePendingChat(context); + if (resolved == null) return; + AppRoutes.pendingChatToken.value = null; + + // Replace any chat already pushed on top of the chat list so a freshly + // tapped notification doesn't stack indefinitely on previous chats. + final navigator = Navigator.of(context); + if (navigator.canPop()) { + navigator.popUntil((route) => route.isFirst); + } + + AppRoutes.openChatView( + context, + room: resolved.room, + selfId: resolved.selfId, + avatar: resolved.avatar, + overrideToSingleSubScreen: true, + ); + } + + void _maybeAskForNotificationPermission() { + final notificationSettings = _settings.val().notificationSettings; + if (notificationSettings.enabled || notificationSettings.askUsageDismissed) return; + + _settings.val(write: true).notificationSettings.askUsageDismissed = true; + ConfirmDialog( + icon: Icons.notifications_active_outlined, + title: 'Benachrichtigungen aktivieren', + content: 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.', + confirmButton: 'Weiter', + onConfirm: () { + FirebaseMessaging.instance.requestPermission(provisional: false).then((value) { + if (!mounted) return; + switch (value.authorizationStatus) { + case AuthorizationStatus.authorized: + NotifyUpdater.enableAfterDisclaimer(_settings).asDialog(context); + break; + case AuthorizationStatus.denied: + showDialog( + context: context, + builder: (_) => const AlertDialog( + content: Text('Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren.'), + ), + ); + break; + default: + break; + } + }); + }, + ).asDialog(context); + } + + @override + Widget build(BuildContext context) { + final bloc = context.read(); + return SplitView.material( + placeholder: const SplitViewPlaceholder(), + breakpoint: 1000, + child: BlocListener>( + listener: (_, _) => _maybeOpenPendingChat(), + child: Scaffold( + appBar: AppBar( + title: const Text('Talk'), + actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + final rooms = bloc.state.data?.rooms; + if (rooms == null) return; + showSearch(context: context, delegate: SearchChat(rooms.data.toList())); + }, + ), + ], + ), + floatingActionButton: FloatingActionButton( + heroTag: 'createChat', + backgroundColor: Theme.of(context).primaryColor, + onPressed: () { + showSearch(context: context, delegate: JoinChat()).then((username) { + if (username == null || !context.mounted) return; + ConfirmDialog( + title: 'Chat starten', + content: "Möchtest du einen Chat mit Nutzer '$username' starten?", + confirmButton: 'Chat starten', + onConfirm: () { + bloc.createDirectChat(username); + }, + ).asDialog(context); + }); + }, + child: const Icon(Icons.add_comment_outlined), + ), + body: LoadableStateConsumer( + child: (state, _) { + final rooms = state.rooms; + if (rooms == null) return const SizedBox.shrink(); + + final talkSettings = context.watch().val().talkSettings; + final sorted = rooms.sortBy( + lastActivity: true, + favoritesToTop: talkSettings.sortFavoritesToTop, + unreadToTop: talkSettings.sortUnreadToTop, + ); + + return ListView( + padding: EdgeInsets.zero, + children: sorted.map((room) { + final hasDraft = _settings.val().talkSettings.drafts.containsKey(room.token); + return ChatTile(data: room, hasDraft: hasDraft); + }).toList(), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/view/pages/talk/chatView.dart b/lib/view/pages/talk/chat_view.dart similarity index 93% rename from lib/view/pages/talk/chatView.dart rename to lib/view/pages/talk/chat_view.dart index e570405..e332150 100644 --- a/lib/view/pages/talk/chatView.dart +++ b/lib/view/pages/talk/chat_view.dart @@ -3,17 +3,17 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../../api/marianumcloud/talk/room/getRoomResponse.dart'; -import '../../../extensions/dateTime.dart'; +import '../../../extensions/date_time.dart'; import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart'; import '../../../state/app/modules/chat/bloc/chat_bloc.dart'; import '../../../state/app/modules/chat/bloc/chat_state.dart'; -import '../../../theming/appTheme.dart'; -import '../../../widget/clickableAppBar.dart'; -import '../../../widget/userAvatar.dart'; -import 'chatDetails/chatInfo.dart'; -import 'components/chatBubble.dart'; -import 'components/chatTextfield.dart'; -import 'talkNavigator.dart'; +import '../../../theming/app_theme.dart'; +import '../../../widget/clickable_app_bar.dart'; +import '../../../widget/user_avatar.dart'; +import 'details/chat_info.dart'; +import 'widgets/chat_bubble.dart'; +import 'widgets/chat_textfield.dart'; +import 'talk_navigator.dart'; class ChatView extends StatefulWidget { final GetRoomResponseObject room; diff --git a/lib/view/pages/talk/components/chatBubbleStyles.dart b/lib/view/pages/talk/data/chat_bubble_styles.dart similarity index 97% rename from lib/view/pages/talk/components/chatBubbleStyles.dart rename to lib/view/pages/talk/data/chat_bubble_styles.dart index 7451dd1..bad0822 100644 --- a/lib/view/pages/talk/components/chatBubbleStyles.dart +++ b/lib/view/pages/talk/data/chat_bubble_styles.dart @@ -1,7 +1,7 @@ import 'package:bubble/bubble.dart'; import 'package:flutter/material.dart'; -import '../../../../theming/appTheme.dart'; +import '../../../../theming/app_theme.dart'; extension ColorExtensions on Color { Color invert() { diff --git a/lib/view/pages/talk/components/chatMessage.dart b/lib/view/pages/talk/data/chat_message.dart similarity index 95% rename from lib/view/pages/talk/components/chatMessage.dart rename to lib/view/pages/talk/data/chat_message.dart index f545fa4..358e289 100644 --- a/lib/view/pages/talk/components/chatMessage.dart +++ b/lib/view/pages/talk/data/chat_message.dart @@ -5,9 +5,9 @@ import 'package:flutter_linkify/flutter_linkify.dart'; import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart'; -import '../../../../model/accountData.dart'; -import '../../../../model/endpointData.dart'; -import '../../../../utils/UrlOpener.dart'; +import '../../../../model/account_data.dart'; +import '../../../../model/endpoint_data.dart'; +import '../../../../utils/url_opener.dart'; class ChatMessage { String originalMessage; diff --git a/lib/view/pages/talk/chatDetails/chatInfo.dart b/lib/view/pages/talk/details/chat_info.dart similarity index 91% rename from lib/view/pages/talk/chatDetails/chatInfo.dart rename to lib/view/pages/talk/details/chat_info.dart index 81ca7a3..74afa79 100644 --- a/lib/view/pages/talk/chatDetails/chatInfo.dart +++ b/lib/view/pages/talk/details/chat_info.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsCache.dart'; import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart'; import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart'; -import '../../../../widget/largeProfilePictureView.dart'; -import '../../../../widget/loadingSpinner.dart'; -import '../../../../widget/userAvatar.dart'; -import '../talkNavigator.dart'; -import 'participants/participantsListView.dart'; +import '../../../../widget/large_profile_picture_view.dart'; +import '../../../../widget/loading_spinner.dart'; +import '../../../../widget/user_avatar.dart'; +import '../talk_navigator.dart'; +import 'participants_list_view.dart'; class ChatInfo extends StatefulWidget { final GetRoomResponseObject room; diff --git a/lib/view/pages/talk/messageReactions.dart b/lib/view/pages/talk/details/message_reactions.dart similarity index 85% rename from lib/view/pages/talk/messageReactions.dart rename to lib/view/pages/talk/details/message_reactions.dart index b3aeaf2..29158f3 100644 --- a/lib/view/pages/talk/messageReactions.dart +++ b/lib/view/pages/talk/details/message_reactions.dart @@ -1,14 +1,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../../../api/marianumcloud/talk/getReactions/getReactions.dart'; -import '../../../api/marianumcloud/talk/getReactions/getReactionsResponse.dart'; -import '../../../model/accountData.dart'; -import '../../../widget/centeredLeading.dart'; -import '../../../widget/loadingSpinner.dart'; -import '../../../widget/placeholderView.dart'; -import '../../../widget/unimplementedDialog.dart'; -import '../../../widget/userAvatar.dart'; +import '../../../../api/marianumcloud/talk/getReactions/getReactions.dart'; +import '../../../../api/marianumcloud/talk/getReactions/getReactionsResponse.dart'; +import '../../../../model/account_data.dart'; +import '../../../../widget/centered_leading.dart'; +import '../../../../widget/loading_spinner.dart'; +import '../../../../widget/placeholder_view.dart'; +import '../../../../widget/unimplemented_dialog.dart'; +import '../../../../widget/user_avatar.dart'; class MessageReactions extends StatefulWidget { final String token; diff --git a/lib/view/pages/talk/chatDetails/participants/participantsListView.dart b/lib/view/pages/talk/details/participants_list_view.dart similarity index 91% rename from lib/view/pages/talk/chatDetails/participants/participantsListView.dart rename to lib/view/pages/talk/details/participants_list_view.dart index dfb6b82..e58d1e5 100644 --- a/lib/view/pages/talk/chatDetails/participants/participantsListView.dart +++ b/lib/view/pages/talk/details/participants_list_view.dart @@ -1,8 +1,8 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import '../../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart'; -import '../../../../../widget/userAvatar.dart'; +import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart'; +import '../../../../widget/user_avatar.dart'; class ParticipantsListView extends StatelessWidget { final GetParticipantsResponse participantsResponse; diff --git a/lib/view/pages/talk/joinChat.dart b/lib/view/pages/talk/join_chat.dart similarity index 96% rename from lib/view/pages/talk/joinChat.dart rename to lib/view/pages/talk/join_chat.dart index e51815a..3acc834 100644 --- a/lib/view/pages/talk/joinChat.dart +++ b/lib/view/pages/talk/join_chat.dart @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; import '../../../api/marianumcloud/autocomplete/autocompleteApi.dart'; import '../../../api/marianumcloud/autocomplete/autocompleteResponse.dart'; -import '../../../model/endpointData.dart'; -import '../../../widget/placeholderView.dart'; +import '../../../model/endpoint_data.dart'; +import '../../../widget/placeholder_view.dart'; class JoinChat extends SearchDelegate { CancelableOperation? future; diff --git a/lib/view/pages/talk/searchChat.dart b/lib/view/pages/talk/search_chat.dart similarity index 96% rename from lib/view/pages/talk/searchChat.dart rename to lib/view/pages/talk/search_chat.dart index ebca04d..68976fc 100644 --- a/lib/view/pages/talk/searchChat.dart +++ b/lib/view/pages/talk/search_chat.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../../../api/marianumcloud/talk/room/getRoomResponse.dart'; -import 'components/chatTile.dart'; +import 'widgets/chat_tile.dart'; class SearchChat extends SearchDelegate { List chats; diff --git a/lib/view/pages/talk/talkNavigator.dart b/lib/view/pages/talk/talk_navigator.dart similarity index 100% rename from lib/view/pages/talk/talkNavigator.dart rename to lib/view/pages/talk/talk_navigator.dart diff --git a/lib/view/pages/talk/components/answerReference.dart b/lib/view/pages/talk/widgets/answer_reference.dart similarity index 98% rename from lib/view/pages/talk/components/answerReference.dart rename to lib/view/pages/talk/widgets/answer_reference.dart index 6a39b09..825ad18 100644 --- a/lib/view/pages/talk/components/answerReference.dart +++ b/lib/view/pages/talk/widgets/answer_reference.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart'; -import 'chatBubbleStyles.dart'; +import '../data/chat_bubble_styles.dart'; class AnswerReference extends StatelessWidget { final BuildContext context; diff --git a/lib/view/pages/talk/components/chatBubble.dart b/lib/view/pages/talk/widgets/chat_bubble.dart similarity index 60% rename from lib/view/pages/talk/components/chatBubble.dart rename to lib/view/pages/talk/widgets/chat_bubble.dart index c5f216e..e34a878 100644 --- a/lib/view/pages/talk/components/chatBubble.dart +++ b/lib/view/pages/talk/widgets/chat_bubble.dart @@ -1,31 +1,26 @@ import 'package:bubble/bubble.dart'; -import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis; import 'package:flowder/flowder.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:jiffy/jiffy.dart'; import 'package:open_filex/open_filex.dart'; -import '../../../../api/marianumcloud/talk/getPoll/getPollState.dart'; -import '../../../../extensions/text.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart'; -import '../../../../api/marianumcloud/talk/deleteMessage/deleteMessage.dart'; import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessage.dart'; import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessageParams.dart'; +import '../../../../api/marianumcloud/talk/getPoll/getPollState.dart'; import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart'; import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart'; import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart'; +import '../../../../extensions/text.dart'; import '../../../../state/app/modules/chat/bloc/chat_bloc.dart'; -import '../../../../widget/debug/debugTile.dart'; -import '../../../../widget/loadingSpinner.dart'; -import '../../files/fileElement.dart'; -import 'answerReference.dart'; -import 'chatBubbleStyles.dart'; -import 'chatMessage.dart'; -import '../messageReactions.dart'; -import 'pollOptionsList.dart'; +import '../../../../widget/loading_spinner.dart'; +import '../../files/widgets/file_element.dart'; +import '../data/chat_bubble_styles.dart'; +import '../data/chat_message.dart'; +import 'answer_reference.dart'; +import 'chat_message_options_dialog.dart'; +import 'poll_options_list.dart'; class ChatBubble extends StatefulWidget { final BuildContext context; @@ -77,176 +72,13 @@ class _ChatBubbleState extends State with SingleTickerProviderStateM } void showOptionsDialog() { - showDialog(context: context, builder: (context) { - var commonReactions = ['👍', '👎', '😆', '❤️', '👀']; - var canReact = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment; - return SimpleDialog( - children: [ - Visibility( - visible: canReact, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - alignment: WrapAlignment.center, - children: [ - ...commonReactions.map((e) => TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - minimumSize: const Size(40, 40) - ), - onPressed: () { - Navigator.of(context).pop(); - ReactMessage( - chatToken: widget.chatData.token, - messageId: widget.bubbleData.id, - params: ReactMessageParams(e), - ).run().then((value) => widget.refetch(renew: true)); - }, - child: Text(e), - ), - ), - IconButton( - onPressed: () { - showDialog(context: context, builder: (context) => AlertDialog( - contentPadding: const EdgeInsets.all(15), - titlePadding: const EdgeInsets.only(left: 6, top: 15), - title: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - IconButton( - onPressed: () { - Navigator.of(context).pop(); - }, - icon: const Icon(Icons.arrow_back), - ), - const SizedBox(width: 10), - const Text('Reagieren'), - ], - ), - content: SizedBox( - width: 256, - height: 270, - child: Column( - children: [ - emojis.EmojiPicker( - config: emojis.Config( - height: 256, - // swapCategoryAndBottomBar: true, // TODO this property is no longer supported, need to find an replacement - emojiViewConfig: emojis.EmojiViewConfig( - backgroundColor: Theme.of(context).canvasColor, - recentsLimit: 67, - emojiSizeMax: 25, - noRecents: const Text('Keine zuletzt verwendeten Emojis'), - columns: 7, - ), - bottomActionBarConfig: const emojis.BottomActionBarConfig( - enabled: false, - ), - categoryViewConfig: emojis.CategoryViewConfig( - backgroundColor: Theme.of(context).hoverColor, - iconColorSelected: Theme.of(context).primaryColor, - indicatorColor: Theme.of(context).primaryColor, - ), - searchViewConfig: emojis.SearchViewConfig( - backgroundColor: Theme.of(context).dividerColor, - // buttonColor: Theme.of(context).dividerColor, // TODO property no longer supported - hintText: 'Suchen', - buttonIconColor: Colors.white, - ), - ), - onEmojiSelected: (emojis.Category? category, emojis.Emoji emoji) { - Navigator.of(context).pop(); - Navigator.of(context).pop(); - ReactMessage( - chatToken: widget.chatData.token, - messageId: widget.bubbleData.id, - params: ReactMessageParams(emoji.emoji), - ).run().then((value) => widget.refetch(renew: true)); - }, - ), - ], - ), - ), - )); - }, - style: IconButton.styleFrom( - padding: EdgeInsets.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - minimumSize: const Size(40, 40), - ), - icon: const Icon(Icons.add_circle_outline_outlined), - ), - ], - ), - const Divider(), - ], - ), - ), - Visibility( - visible: widget.bubbleData.isReplyable, - child: ListTile( - leading: const Icon(Icons.reply_outlined), - title: const Text('Antworten'), - onTap: () { - context.read().setReferenceMessageId(widget.bubbleData.id); - Navigator.of(context).pop(); - }, - ), - ), - Visibility( - visible: canReact, - child: ListTile( - leading: const Icon(Icons.emoji_emotions_outlined), - title: const Text('Reaktionen'), - onTap: () { - Navigator.of(context).push(MaterialPageRoute(builder: (context) => MessageReactions( - token: widget.chatData.token, - messageId: widget.bubbleData.id, - ))); - }, - ), - ), - Visibility( - visible: widget.bubbleData.message != '{file}', - child: ListTile( - leading: const Icon(Icons.copy), - title: const Text('Nachricht kopieren'), - onTap: () => { - Clipboard.setData(ClipboardData(text: widget.bubbleData.message)), - Navigator.of(context).pop(), - }, - ), - ), - Visibility( - visible: !kReleaseMode && !widget.isSender && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne, - child: ListTile( - leading: const Icon(Icons.sms_outlined), - title: Text("Private Nachricht an '${widget.bubbleData.actorDisplayName}'"), - onTap: () => { - Navigator.of(context).pop() - }, - ), - ), - Visibility( - visible: widget.isSender && DateTime.fromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).add(const Duration(hours: 6)).isAfter(DateTime.now()), - child: ListTile( - leading: const Icon(Icons.delete_outline), - title: const Text('Nachricht löschen'), - onTap: () { - DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) { - if (!context.mounted) return; - context.read().refresh(); - Navigator.of(context).pop(); - }); - }, - ), - ), - DebugTile(context).jsonData(widget.bubbleData.toJson()), - ], - ); - }); + showChatMessageOptionsDialog( + context, + chatData: widget.chatData, + bubbleData: widget.bubbleData, + isSender: widget.isSender, + onRefetch: widget.refetch, + ); } diff --git a/lib/view/pages/talk/widgets/chat_message_options_dialog.dart b/lib/view/pages/talk/widgets/chat_message_options_dialog.dart new file mode 100644 index 0000000..02600f4 --- /dev/null +++ b/lib/view/pages/talk/widgets/chat_message_options_dialog.dart @@ -0,0 +1,202 @@ +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart'; +import '../../../../api/marianumcloud/talk/deleteMessage/deleteMessage.dart'; +import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart'; +import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart'; +import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart'; +import '../../../../routing/app_routes.dart'; +import '../../../../state/app/modules/chat/bloc/chat_bloc.dart'; +import '../../../../widget/debug/debug_tile.dart'; + +const _commonReactions = ['👍', '👎', '😆', '❤️', '👀']; + +/// Long-press / double-tap options dialog for a single chat message bubble. +/// The hosting [ChatBubble] keeps responsibility for rendering the bubble; +/// this file owns the modal interactions (react, reply, copy, delete, ...). +Future showChatMessageOptionsDialog( + BuildContext context, { + required GetRoomResponseObject chatData, + required GetChatResponseObject bubbleData, + required bool isSender, + required void Function({bool renew}) onRefetch, +}) { + final parentContext = context; + final canReact = bubbleData.messageType == GetRoomResponseObjectMessageType.comment; + final canDelete = isSender && + DateTime.fromMillisecondsSinceEpoch(bubbleData.timestamp * 1000) + .add(const Duration(hours: 6)) + .isAfter(DateTime.now()); + + return showDialog( + context: context, + builder: (dialogCtx) => SimpleDialog( + children: [ + if (canReact) + _ReactionsRow( + chatToken: chatData.token, + messageId: bubbleData.id, + onRefetch: onRefetch, + dialogContext: dialogCtx, + ), + if (bubbleData.isReplyable) + ListTile( + leading: const Icon(Icons.reply_outlined), + title: const Text('Antworten'), + onTap: () { + dialogCtx.read().setReferenceMessageId(bubbleData.id); + Navigator.of(dialogCtx).pop(); + }, + ), + if (canReact) + ListTile( + leading: const Icon(Icons.emoji_emotions_outlined), + title: const Text('Reaktionen'), + onTap: () { + Navigator.of(dialogCtx).pop(); + if (!parentContext.mounted) return; + AppRoutes.openMessageReactions(parentContext, chatData.token, bubbleData.id); + }, + ), + if (bubbleData.message != '{file}') + ListTile( + leading: const Icon(Icons.copy), + title: const Text('Nachricht kopieren'), + onTap: () { + Clipboard.setData(ClipboardData(text: bubbleData.message)); + Navigator.of(dialogCtx).pop(); + }, + ), + if (!kReleaseMode && !isSender && chatData.type != GetRoomResponseObjectConversationType.oneToOne) + ListTile( + leading: const Icon(Icons.sms_outlined), + title: Text("Private Nachricht an '${bubbleData.actorDisplayName}'"), + onTap: () => Navigator.of(dialogCtx).pop(), + ), + if (canDelete) + ListTile( + leading: const Icon(Icons.delete_outline), + title: const Text('Nachricht löschen'), + onTap: () async { + await DeleteMessage(chatData.token, bubbleData.id).run(); + if (!dialogCtx.mounted) return; + dialogCtx.read().refresh(); + Navigator.of(dialogCtx).pop(); + }, + ), + DebugTile(dialogCtx).jsonData(bubbleData.toJson()), + ], + ), + ); +} + +class _ReactionsRow extends StatelessWidget { + final String chatToken; + final int messageId; + final void Function({bool renew}) onRefetch; + final BuildContext dialogContext; + + const _ReactionsRow({ + required this.chatToken, + required this.messageId, + required this.onRefetch, + required this.dialogContext, + }); + + void _react(String emoji) { + Navigator.of(dialogContext).pop(); + ReactMessage( + chatToken: chatToken, + messageId: messageId, + params: ReactMessageParams(emoji), + ).run().then((_) => onRefetch(renew: true)); + } + + @override + Widget build(BuildContext context) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + alignment: WrapAlignment.center, + children: [ + ..._commonReactions.map( + (emoji) => TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size(40, 40), + ), + onPressed: () => _react(emoji), + child: Text(emoji), + ), + ), + IconButton( + onPressed: () => _showEmojiPicker(context), + style: IconButton.styleFrom( + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size(40, 40), + ), + icon: const Icon(Icons.add_circle_outline_outlined), + ), + ], + ), + const Divider(), + ], + ); + + void _showEmojiPicker(BuildContext rowContext) { + showDialog( + context: rowContext, + builder: (pickerCtx) => AlertDialog( + contentPadding: const EdgeInsets.all(15), + titlePadding: const EdgeInsets.only(left: 6, top: 15), + title: Row( + children: [ + IconButton( + onPressed: () => Navigator.of(pickerCtx).pop(), + icon: const Icon(Icons.arrow_back), + ), + const SizedBox(width: 10), + const Text('Reagieren'), + ], + ), + content: SizedBox( + width: 256, + height: 270, + child: emojis.EmojiPicker( + config: emojis.Config( + height: 256, + emojiViewConfig: emojis.EmojiViewConfig( + backgroundColor: Theme.of(pickerCtx).canvasColor, + recentsLimit: 67, + emojiSizeMax: 25, + noRecents: const Text('Keine zuletzt verwendeten Emojis'), + columns: 7, + ), + bottomActionBarConfig: const emojis.BottomActionBarConfig(enabled: false), + categoryViewConfig: emojis.CategoryViewConfig( + backgroundColor: Theme.of(pickerCtx).hoverColor, + iconColorSelected: Theme.of(pickerCtx).primaryColor, + indicatorColor: Theme.of(pickerCtx).primaryColor, + ), + searchViewConfig: emojis.SearchViewConfig( + backgroundColor: Theme.of(pickerCtx).dividerColor, + hintText: 'Suchen', + buttonIconColor: Colors.white, + ), + ), + onEmojiSelected: (_, emoji) { + Navigator.of(pickerCtx).pop(); + _react(emoji.emoji); + }, + ), + ), + ), + ); + } +} diff --git a/lib/view/pages/talk/components/chatTextfield.dart b/lib/view/pages/talk/widgets/chat_textfield.dart similarity index 98% rename from lib/view/pages/talk/components/chatTextfield.dart rename to lib/view/pages/talk/widgets/chat_textfield.dart index a056984..2ef4527 100644 --- a/lib/view/pages/talk/components/chatTextfield.dart +++ b/lib/view/pages/talk/widgets/chat_textfield.dart @@ -12,10 +12,10 @@ import '../../../../api/marianumcloud/talk/sendMessage/sendMessageParams.dart'; import '../../../../api/marianumcloud/webdav/webdavApi.dart'; import '../../../../state/app/modules/chat/bloc/chat_bloc.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; -import '../../../../widget/filePick.dart'; -import '../../../../widget/focusBehaviour.dart'; -import '../../files/filesUploadDialog.dart'; -import 'answerReference.dart'; +import '../../../../widget/file_pick.dart'; +import '../../../../widget/focus_behaviour.dart'; +import '../../files/files_upload_dialog.dart'; +import 'answer_reference.dart'; class ChatTextfield extends StatefulWidget { final String sendToToken; diff --git a/lib/view/pages/talk/components/chatTile.dart b/lib/view/pages/talk/widgets/chat_tile.dart similarity index 96% rename from lib/view/pages/talk/components/chatTile.dart rename to lib/view/pages/talk/widgets/chat_tile.dart index 53d20b4..1696479 100644 --- a/lib/view/pages/talk/components/chatTile.dart +++ b/lib/view/pages/talk/widgets/chat_tile.dart @@ -9,14 +9,14 @@ import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart'; import '../../../../api/marianumcloud/talk/setFavorite/setFavorite.dart'; import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarker.dart'; import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarkerParams.dart'; -import '../../../../model/accountData.dart'; +import '../../../../model/account_data.dart'; import '../../../../state/app/modules/chat/bloc/chat_bloc.dart'; import '../../../../state/app/modules/chatList/bloc/chat_list_bloc.dart'; -import '../../../../widget/confirmDialog.dart'; -import '../../../../widget/debug/debugTile.dart'; -import '../../../../widget/userAvatar.dart'; -import '../chatView.dart'; -import '../talkNavigator.dart'; +import '../../../../widget/confirm_dialog.dart'; +import '../../../../widget/debug/debug_tile.dart'; +import '../../../../widget/user_avatar.dart'; +import '../chat_view.dart'; +import '../talk_navigator.dart'; class ChatTile extends StatefulWidget { final GetRoomResponseObject data; diff --git a/lib/view/pages/talk/components/pollOptionsList.dart b/lib/view/pages/talk/widgets/poll_options_list.dart similarity index 98% rename from lib/view/pages/talk/components/pollOptionsList.dart rename to lib/view/pages/talk/widgets/poll_options_list.dart index c7eb9ea..4bade65 100644 --- a/lib/view/pages/talk/components/pollOptionsList.dart +++ b/lib/view/pages/talk/widgets/poll_options_list.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import '../../../../api/marianumcloud/talk/getPoll/getPollStateResponse.dart'; -import '../../../../utils/UrlOpener.dart'; +import '../../../../utils/url_opener.dart'; class PollOptionsList extends StatefulWidget { final GetPollStateResponseObject pollData; diff --git a/lib/view/pages/talk/components/splitViewPlaceholder.dart b/lib/view/pages/talk/widgets/split_view_placeholder.dart similarity index 94% rename from lib/view/pages/talk/components/splitViewPlaceholder.dart rename to lib/view/pages/talk/widgets/split_view_placeholder.dart index d5c4030..590a1ec 100644 --- a/lib/view/pages/talk/components/splitViewPlaceholder.dart +++ b/lib/view/pages/talk/widgets/split_view_placeholder.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../../../theming/appTheme.dart'; +import '../../../../theming/app_theme.dart'; class SplitViewPlaceholder extends StatelessWidget { const SplitViewPlaceholder({super.key}); diff --git a/lib/view/pages/timetable/custom_events/custom_event_colors.dart b/lib/view/pages/timetable/custom_events/custom_event_colors.dart index a4540fe..04f623a 100644 --- a/lib/view/pages/timetable/custom_events/custom_event_colors.dart +++ b/lib/view/pages/timetable/custom_events/custom_event_colors.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../../../theming/darkAppTheme.dart'; +import '../../../../theming/dark_app_theme.dart'; enum CustomTimetableColors { orange, red, green, blue } diff --git a/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart b/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart index f886131..34479ca 100644 --- a/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart +++ b/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart @@ -7,10 +7,10 @@ import 'package:rrule_generator/rrule_generator.dart'; import 'package:time_range_picker/time_range_picker.dart'; import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart'; -import '../../../../extensions/dateTime.dart'; +import '../../../../extensions/date_time.dart'; import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart'; -import '../../../../widget/focusBehaviour.dart'; -import '../../../../widget/infoDialog.dart'; +import '../../../../widget/focus_behaviour.dart'; +import '../../../../widget/info_dialog.dart'; import 'custom_event_colors.dart'; class CustomEventEditDialog extends StatefulWidget { diff --git a/lib/view/pages/timetable/custom_events/custom_events_view.dart b/lib/view/pages/timetable/custom_events/custom_events_view.dart index 34af737..a1c6595 100644 --- a/lib/view/pages/timetable/custom_events/custom_events_view.dart +++ b/lib/view/pages/timetable/custom_events/custom_events_view.dart @@ -4,8 +4,8 @@ import 'package:jiffy/jiffy.dart'; import '../../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart'; import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart'; import '../../../../state/app/modules/timetable/bloc/timetable_state.dart'; -import '../../../../widget/centeredLeading.dart'; -import '../../../../widget/placeholderView.dart'; +import '../../../../widget/centered_leading.dart'; +import '../../../../widget/placeholder_view.dart'; import '../details/delete_custom_event.dart'; import 'custom_event_edit_dialog.dart'; diff --git a/lib/view/pages/timetable/details/custom_event_sheet.dart b/lib/view/pages/timetable/details/custom_event_sheet.dart index 9f11099..3675abf 100644 --- a/lib/view/pages/timetable/details/custom_event_sheet.dart +++ b/lib/view/pages/timetable/details/custom_event_sheet.dart @@ -3,8 +3,8 @@ import 'package:jiffy/jiffy.dart'; import 'package:rrule/rrule.dart'; import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart'; -import '../../../../widget/centeredLeading.dart'; -import '../../../../widget/debug/debugTile.dart'; +import '../../../../widget/centered_leading.dart'; +import '../../../../widget/debug/debug_tile.dart'; import '../custom_events/custom_event_edit_dialog.dart'; import '_bottom_sheet.dart'; import 'delete_custom_event.dart'; diff --git a/lib/view/pages/timetable/details/delete_custom_event.dart b/lib/view/pages/timetable/details/delete_custom_event.dart index ad362d5..3161e93 100644 --- a/lib/view/pages/timetable/details/delete_custom_event.dart +++ b/lib/view/pages/timetable/details/delete_custom_event.dart @@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart'; import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart'; -import '../../../../widget/confirmDialog.dart'; +import '../../../../widget/confirm_dialog.dart'; Completer showDeleteCustomEventDialog(BuildContext context, CustomTimetableEvent event) { final completer = Completer(); diff --git a/lib/view/pages/timetable/details/webuntis_lesson_sheet.dart b/lib/view/pages/timetable/details/webuntis_lesson_sheet.dart index 1fb0bd7..7adfee6 100644 --- a/lib/view/pages/timetable/details/webuntis_lesson_sheet.dart +++ b/lib/view/pages/timetable/details/webuntis_lesson_sheet.dart @@ -1,17 +1,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; -import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; import '../../../../api/webuntis/queries/getRooms/getRoomsResponse.dart'; import '../../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart'; import '../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart'; +import '../../../../routing/app_routes.dart'; import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart'; import '../../../../state/app/modules/timetable/bloc/timetable_state.dart'; -import '../../../../widget/debug/debugTile.dart'; -import '../../../../widget/unimplementedDialog.dart'; -import '../../more/roomplan/roomplan.dart'; +import '../../../../widget/debug/debug_tile.dart'; +import '../../../../widget/unimplemented_dialog.dart'; import '_bottom_sheet.dart'; class WebuntisLessonSheet { @@ -54,7 +53,7 @@ class WebuntisLessonSheet { title: Text('Raum: ${room.name} (${room.longName})'), trailing: IconButton( icon: const Icon(Icons.house_outlined), - onPressed: () => pushScreen(context, withNavBar: false, screen: const Roomplan()), + onPressed: () => AppRoutes.openRoomplan(context), ), ), ListTile( diff --git a/lib/view/pages/timetable/timetable.dart b/lib/view/pages/timetable/timetable.dart index 5099577..20ae430 100644 --- a/lib/view/pages/timetable/timetable.dart +++ b/lib/view/pages/timetable/timetable.dart @@ -2,13 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; -import '../../../extensions/dateTime.dart'; +import '../../../extensions/date_time.dart'; +import '../../../routing/app_routes.dart'; import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart'; import '../../../state/app/modules/timetable/bloc/timetable_bloc.dart'; import '../../../state/app/modules/timetable/bloc/timetable_state.dart'; import '../../../state/app/modules/settings/bloc/settings_cubit.dart'; import 'custom_events/custom_event_edit_dialog.dart'; -import 'custom_events/custom_events_view.dart'; import 'data/arbitrary_appointment.dart'; import 'data/lesson_period_schedule.dart'; import 'data/timetable_appointment_factory.dart'; @@ -46,7 +46,7 @@ class _TimetableState extends State { barrierDismissible: false, ); case _CalendarAction.viewEvents: - Navigator.of(context).push(MaterialPageRoute(builder: (_) => const CustomEventsView())); + AppRoutes.openCustomEvents(context); } } diff --git a/lib/view/pages/timetable/widgets/special_regions_builder.dart b/lib/view/pages/timetable/widgets/special_regions_builder.dart index 6587b93..2007f46 100644 --- a/lib/view/pages/timetable/widgets/special_regions_builder.dart +++ b/lib/view/pages/timetable/widgets/special_regions_builder.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; import '../../../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart'; -import '../../../../extensions/dateTime.dart'; +import '../../../../extensions/date_time.dart'; import '../data/calendar_layout.dart'; import '../data/lesson_period_schedule.dart'; import '../data/webuntis_time.dart'; diff --git a/lib/view/settings/settings.dart b/lib/view/settings/settings.dart deleted file mode 100644 index 85cfbc5..0000000 --- a/lib/view/settings/settings.dart +++ /dev/null @@ -1,318 +0,0 @@ - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../../model/accountData.dart'; -import '../../notification/notifyUpdater.dart'; -import '../../state/app/modules/settings/bloc/settings_cubit.dart'; -import '../../state/app/modules/timetable/bloc/timetable_bloc.dart'; -import '../../storage/base/settings.dart' as model; -import '../../theming/appTheme.dart'; -import '../../widget/centeredLeading.dart'; -import '../../widget/confirmDialog.dart'; -import '../../widget/debug/cacheView.dart'; -import '../../storage/timetable/timetable_name_mode.dart'; -import 'defaultSettings.dart'; -import 'devToolsSettings.dart'; -import 'privacyInfo.dart'; - -class Settings extends StatefulWidget { - const Settings({super.key}); - - @override - State createState() => _SettingsState(); -} - -class _SettingsState extends State { - - @override - void initState() { - super.initState(); - } - - bool developerMode = false; - - @override - Widget build(BuildContext context) => BlocBuilder(builder: (context, _) { - final settings = context.read(); - return Scaffold( - appBar: AppBar( - title: const Text('Einstellungen'), - ), - body: ListView( - children: [ - ListTile( - leading: const CenteredLeading(Icon(Icons.logout_outlined)), - title: const Text('Konto abmelden'), - subtitle: Text('Angemeldet als ${AccountData().getUsername()}'), - onTap: () { - showDialog( - context: context, - builder: (context) => ConfirmDialog( - title: 'Abmelden?', - content: 'Möchtest du dich wirklich abmelden?', - confirmButton: 'Abmelden', - onConfirm: () { - SharedPreferences.getInstance().then((value) => { - value.clear(), - }).then((value) async { - PaintingBinding.instance.imageCache.clear(); - if (!context.mounted) return; - context.read().reset(); - const CacheView().clear(); - AccountData().removeData(context: context); - Navigator.popUntil(context, (route) => !Navigator.canPop(context)); - }); - }, - ), - ); - }, - ), - - const Divider(), - - ListTile( - leading: const Icon(Icons.dark_mode_outlined), - title: const Text('Farbgebung'), - trailing: DropdownButton( - value: settings.val().appTheme, - icon: const Icon(Icons.arrow_drop_down), - items: ThemeMode.values.map((e) => DropdownMenuItem( - value: e, - enabled: e != settings.val().appTheme, - child: Row( - children: [ - Icon(AppTheme.getDisplayOptions(e).icon), - const SizedBox(width: 10), - Text(AppTheme.getDisplayOptions(e).displayName), - ], - ), - )).toList(), - onChanged: (e) { - settings.val(write: true).appTheme = e!; - }, - ), - ), - - const Divider(), - - ListTile( - leading: const Icon(Icons.abc_outlined), - title: const Text('Fachbezeichnung'), - trailing: DropdownButton( - value: settings.val().timetableSettings.timetableNameMode, - icon: Icon(Icons.arrow_drop_down), - items: TimetableNameMode.values.map((e) => DropdownMenuItem( - value: e, - enabled: e != settings.val().timetableSettings.timetableNameMode, - child: Row( - children: [ - Icon(TimetableNameModes.getDisplayOptions(e).icon), - const SizedBox(width: 10), - Text(TimetableNameModes.getDisplayOptions(e).displayName), - ], - ), - )).toList(), - onChanged: (value) { - settings.val(write: true).timetableSettings.timetableNameMode = value!; - context.read().refresh(); - }, - ) - ), - ListTile( - leading: const Icon(Icons.calendar_view_day_outlined), - title: const Text('Doppelstunden zusammenhängend anzeigen'), - trailing: Checkbox( - value: settings.val().timetableSettings.connectDoubleLessons, - onChanged: (e) { - settings.val(write: true).timetableSettings.connectDoubleLessons = e!; - context.read().refresh(); - }, - ), - ), - - const Divider(), - - ListTile( - leading: const Icon(Icons.star_border), - title: const Text('Favoriten im Talk nach oben sortieren'), - trailing: Checkbox( - value: settings.val().talkSettings.sortFavoritesToTop, - onChanged: (e) { - settings.val(write: true).talkSettings.sortFavoritesToTop = e!; - }, - ), - ), - - ListTile( - leading: const Icon(Icons.mark_email_unread_outlined), - title: const Text('Ungelesene Chats nach oben sortieren'), - trailing: Checkbox( - value: settings.val().talkSettings.sortUnreadToTop, - onChanged: (e) { - settings.val(write: true).talkSettings.sortUnreadToTop = e!; - }, - ), - ), - - ListTile( - leading: const CenteredLeading(Icon(Icons.notifications_active_outlined)), - title: const Text('Push-Benachrichtigungen aktivieren'), - subtitle: const Text('Lange tippen für mehr Informationen'), - trailing: Checkbox( - value: settings.val().notificationSettings.enabled, - onChanged: (e) { - if(e!) { - NotifyUpdater.enableAfterDisclaimer(settings).asDialog(context); - } else { - settings.val(write: true).notificationSettings.enabled = e; - } - }, - ), - onLongPress: () => showDialog(context: context, builder: (context) => AlertDialog( - title: const Text('Info über Push'), - content: const SingleChildScrollView(child: Text('' - "Aufgrund technischer Limitationen müssen Push-Nachrichten über einen externen Server - hier 'mhsl.eu' (Author dieser App) - erfolgen.\n\n" - 'Wenn Push aktiviert wird, werden deine Zugangsdaten und ein Token verschlüsselt an den Betreiber gesendet und von ihm unverschlüsselt gespeichert.\n\n' - 'Der extene Server verwendet die Zugangsdaten um sich maschinell in Nextcloud Talk anzumelden und via Websockets auf neue Nachrichten zu warten.\n\n' - 'Wenn eine neue Nachricht eintrifft wird dein Telefon via FBC-Messaging (Google Firebase Push) vom externen Server benachrichtigt.\n\n' - 'Behalte im Hinterkopf, dass deine Zugangsdaten auf einem externen Server gespeichert werden und dies trotz bester Absichten ein Sicherheitsrisiko sein kann!' - )), - actions: [ - TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Zurück')) - ], - )), - ), - - const Divider(), - - ListTile( - leading: const Icon(Icons.drive_folder_upload_outlined), - title: const Text('Ordner in Dateien nach oben sortieren'), - trailing: Checkbox( - value: settings.val().fileSettings.sortFoldersToTop, - onChanged: (e) { - settings.val(write: true).fileSettings.sortFoldersToTop = e!; - }, - ), - ), - - ListTile( - leading: const Icon(Icons.open_in_new_outlined), - title: const Text('Dateien immer mit Systemdialog öffnen'), - trailing: Checkbox( - value: settings.val().fileViewSettings.alwaysOpenExternally, - onChanged: (e) { - settings.val(write: true).fileViewSettings.alwaysOpenExternally = e!; - }, - ), - ), - - const Divider(), - - ListTile( - leading: const Icon(Icons.live_help_outlined), - title: const Text('Informationen und Lizenzen'), - onTap: () { - PackageInfo.fromPlatform().then((appInfo) { - if (!context.mounted) return; - showAboutDialog( - context: context, - applicationIcon: const Icon(Icons.apps), - applicationName: 'MarianumMobile', - applicationVersion: '${appInfo.appName}\n\nPackage: ${appInfo.packageName}\nVersion: ${appInfo.version}\nBuild: ${appInfo.buildNumber}', - applicationLegalese: 'Dies ist ein Inoffizieller Nextcloud & Webuntis Client und wird nicht vom Marianum selbst betrieben.\n' - 'Keinerlei Gewähr für Vollständigkeit, Richtigkeit und Aktualität!\n\n' - "${kReleaseMode ? "Production" : "Development"} build\n" - 'Marianum Fulda 2023-${Jiffy.now().year}\nElias Müller', - ); - }); - }, - trailing: const Icon(Icons.arrow_right), - ), - - ListTile( - leading: const Icon(Icons.policy_outlined), - title: const Text('Impressum & Datenschutz'), - onTap: () { - showDialog(context: context, builder: (context) => SimpleDialog( - children: [ - ListTile( - leading: const CenteredLeading(Icon(Icons.school_outlined)), - title: const Text('Infos zum Marianum Fulda'), - subtitle: const Text('Für Talk-Chats und Dateien'), - trailing: const Icon(Icons.arrow_right), - onTap: () => PrivacyInfo(providerText: 'Marianum', imprintUrl: 'https://www.marianum-fulda.de/impressum', privacyUrl: 'https://www.marianum-fulda.de/datenschutz').showPopup(context) - ), - ListTile( - leading: const CenteredLeading(Icon(Icons.date_range_outlined)), - title: const Text('Infos zu Web-/ Untis'), - subtitle: const Text('Für den Stundenplan'), - trailing: const Icon(Icons.arrow_right), - onTap: () => PrivacyInfo(providerText: 'Untis', imprintUrl: 'https://www.untis.at/impressum', privacyUrl: 'https://www.untis.at/datenschutz-wu-apps').showPopup(context) - ), - ListTile( - leading: const CenteredLeading(Icon(Icons.send_time_extension_outlined)), - title: const Text('Infos zu mhsl'), - subtitle: const Text('Für Countdowns, Marianum Message und mehr'), - trailing: const Icon(Icons.arrow_right), - onTap: () => PrivacyInfo(providerText: 'mhsl', imprintUrl: 'https://mhsl.eu/id.html', privacyUrl: 'https://mhsl.eu/datenschutz.html').showPopup(context), - ), - ], - )); - }, - trailing: const Icon(Icons.arrow_right), - ), - - const Divider(), - - ListTile( - leading: const CenteredLeading(Icon(Icons.code)), - title: const Text('Quellcode MarianumMobile/Client'), - subtitle: const Text('GNU GPL v3'), - onTap: () => ConfirmDialog.openBrowser(context, 'https://mhsl.eu/gitea/MarianumMobile/Client'), - ), - - ListTile( - leading: const Icon(Icons.developer_mode_outlined), - title: const Text('Entwicklermodus'), - trailing: Checkbox( - value: settings.val().devToolsEnabled, - onChanged: (state) { - changeView() { - var enabled = state ?? false; - settings.val(write: true).devToolsEnabled = enabled; - if(!enabled) settings.val(write: true).devToolsSettings = DefaultSettings.get().devToolsSettings; - } - - if(!state!) { - changeView(); - return; - } - - ConfirmDialog( - title: 'Entwicklermodus', - content: '' - 'Die Entwickleransicht bietet erweiterte Funktionen, die für den üblichen Gebrauch nicht benötigt werden.\n\nDie Verwendung der Tools kann darüber hinaus bei falscher Verwendung zu Fehlern führen.\n\n' - 'Aktivieren auf eigene Verantwortung.', - confirmButton: 'Ja, ich verstehe das Risiko', - cancelButton: 'Nein, zurück zur App', - onConfirm: changeView, - ).asDialog(context); - }, - ), - ), - - Visibility( - visible: settings.val().devToolsEnabled, - child: DevToolsSettings(settings: settings), - ), - ], - ), - ); - }); -} diff --git a/lib/widget/animatedTime.dart b/lib/widget/animated_time.dart similarity index 100% rename from lib/widget/animatedTime.dart rename to lib/widget/animated_time.dart diff --git a/lib/widget/breaker/breaker.dart b/lib/widget/breaker/breaker.dart index 9c66de2..9c9186e 100644 --- a/lib/widget/breaker/breaker.dart +++ b/lib/widget/breaker/breaker.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart'; import '../../state/app/modules/breaker/bloc/breaker_bloc.dart'; -import '../../widget/placeholderView.dart'; +import '../../widget/placeholder_view.dart'; class Breaker extends StatelessWidget { final BreakerArea breaker; diff --git a/lib/widget/centeredLeading.dart b/lib/widget/centered_leading.dart similarity index 100% rename from lib/widget/centeredLeading.dart rename to lib/widget/centered_leading.dart diff --git a/lib/widget/clickableAppBar.dart b/lib/widget/clickable_app_bar.dart similarity index 100% rename from lib/widget/clickableAppBar.dart rename to lib/widget/clickable_app_bar.dart diff --git a/lib/widget/confirmDialog.dart b/lib/widget/confirm_dialog.dart similarity index 100% rename from lib/widget/confirmDialog.dart rename to lib/widget/confirm_dialog.dart diff --git a/lib/widget/debug/cacheView.dart b/lib/widget/debug/cache_view.dart similarity index 97% rename from lib/widget/debug/cacheView.dart rename to lib/widget/debug/cache_view.dart index ab66605..c92132b 100644 --- a/lib/widget/debug/cacheView.dart +++ b/lib/widget/debug/cache_view.dart @@ -6,9 +6,9 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:localstore/localstore.dart'; -import '../../../widget/placeholderView.dart'; +import '../../../widget/placeholder_view.dart'; import '../../api/requestCache.dart'; -import 'jsonViewer.dart'; +import 'json_viewer.dart'; class CacheView extends StatefulWidget { const CacheView({super.key}); diff --git a/lib/widget/debug/debugTile.dart b/lib/widget/debug/debug_tile.dart similarity index 94% rename from lib/widget/debug/debugTile.dart rename to lib/widget/debug/debug_tile.dart index 7a29fcc..50bf937 100644 --- a/lib/widget/debug/debugTile.dart +++ b/lib/widget/debug/debug_tile.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../state/app/modules/settings/bloc/settings_cubit.dart'; -import '../centeredLeading.dart'; -import 'jsonViewer.dart'; +import '../centered_leading.dart'; +import 'json_viewer.dart'; class DebugTile { BuildContext context; diff --git a/lib/widget/debug/jsonViewer.dart b/lib/widget/debug/json_viewer.dart similarity index 100% rename from lib/widget/debug/jsonViewer.dart rename to lib/widget/debug/json_viewer.dart diff --git a/lib/widget/dropdownDisplay.dart b/lib/widget/dropdown_display.dart similarity index 100% rename from lib/widget/dropdownDisplay.dart rename to lib/widget/dropdown_display.dart diff --git a/lib/widget/filePick.dart b/lib/widget/file_pick.dart similarity index 100% rename from lib/widget/filePick.dart rename to lib/widget/file_pick.dart diff --git a/lib/widget/fileViewer.dart b/lib/widget/file_viewer.dart similarity index 94% rename from lib/widget/fileViewer.dart rename to lib/widget/file_viewer.dart index 8ab5c58..88bb8f8 100644 --- a/lib/widget/fileViewer.dart +++ b/lib/widget/file_viewer.dart @@ -8,11 +8,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:share_plus/share_plus.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; +import '../routing/app_routes.dart'; import '../state/app/modules/settings/bloc/settings_cubit.dart'; -import '../utils/FileSaver.dart'; -import 'infoDialog.dart'; -import 'placeholderView.dart'; -import 'sharePositionOrigin.dart'; +import '../utils/file_saver.dart'; +import 'info_dialog.dart'; +import 'placeholder_view.dart'; +import 'share_position_origin.dart'; class FileViewer extends StatefulWidget { final String path; @@ -51,9 +52,7 @@ class _FileViewerState extends State { onSelected: (value) async { switch(value) { case FileViewingActions.openExternal: - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => FileViewer(path: widget.path, openExternal: true)) - ); + AppRoutes.openFileViewer(context, widget.path, openExternal: true); break; case FileViewingActions.share: SharePlus.instance.share( diff --git a/lib/widget/focusBehaviour.dart b/lib/widget/focus_behaviour.dart similarity index 100% rename from lib/widget/focusBehaviour.dart rename to lib/widget/focus_behaviour.dart diff --git a/lib/widget/infoDialog.dart b/lib/widget/info_dialog.dart similarity index 100% rename from lib/widget/infoDialog.dart rename to lib/widget/info_dialog.dart diff --git a/lib/widget/largeProfilePictureView.dart b/lib/widget/large_profile_picture_view.dart similarity index 94% rename from lib/widget/largeProfilePictureView.dart rename to lib/widget/large_profile_picture_view.dart index 65a9a4f..c8abe23 100644 --- a/lib/widget/largeProfilePictureView.dart +++ b/lib/widget/large_profile_picture_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; -import '../model/endpointData.dart'; +import '../model/endpoint_data.dart'; class LargeProfilePictureView extends StatelessWidget { final String username; diff --git a/lib/widget/loadingSpinner.dart b/lib/widget/loading_spinner.dart similarity index 100% rename from lib/widget/loadingSpinner.dart rename to lib/widget/loading_spinner.dart diff --git a/lib/widget/placeholderView.dart b/lib/widget/placeholder_view.dart similarity index 100% rename from lib/widget/placeholderView.dart rename to lib/widget/placeholder_view.dart diff --git a/lib/widget/sharePositionOrigin.dart b/lib/widget/share_position_origin.dart similarity index 100% rename from lib/widget/sharePositionOrigin.dart rename to lib/widget/share_position_origin.dart diff --git a/lib/widget/unimplementedDialog.dart b/lib/widget/unimplemented_dialog.dart similarity index 100% rename from lib/widget/unimplementedDialog.dart rename to lib/widget/unimplemented_dialog.dart diff --git a/lib/widget/userAvatar.dart b/lib/widget/user_avatar.dart similarity index 97% rename from lib/widget/userAvatar.dart rename to lib/widget/user_avatar.dart index bab843b..bcabac6 100644 --- a/lib/widget/userAvatar.dart +++ b/lib/widget/user_avatar.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:http/http.dart' as http; -import '../model/accountData.dart'; -import '../model/endpointData.dart'; +import '../model/account_data.dart'; +import '../model/endpoint_data.dart'; class UserAvatar extends StatefulWidget { final String id;