folder restructuring

This commit is contained in:
2026-05-05 21:44:23 +02:00
parent db9c3386f1
commit 4f796dac2e
102 changed files with 1254 additions and 879 deletions
@@ -3,8 +3,8 @@ import 'dart:io';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../../../model/accountData.dart'; import '../../../model/account_data.dart';
import '../../../model/endpointData.dart'; import '../../../model/endpoint_data.dart';
import 'autocompleteResponse.dart'; import 'autocompleteResponse.dart';
class AutocompleteApi { class AutocompleteApi {
@@ -2,8 +2,8 @@ import 'dart:io';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../../../model/accountData.dart'; import '../../../model/account_data.dart';
import '../../../model/endpointData.dart'; import '../../../model/endpoint_data.dart';
import 'fileSharingApiParams.dart'; import 'fileSharingApiParams.dart';
class FileSharingApi { class FileSharingApi {
+2 -2
View File
@@ -2,8 +2,8 @@ import 'dart:developer';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../../../model/accountData.dart'; import '../../../model/account_data.dart';
import '../../../model/endpointData.dart'; import '../../../model/endpoint_data.dart';
import '../../apiError.dart'; import '../../apiError.dart';
import '../../apiParams.dart'; import '../../apiParams.dart';
import '../../apiRequest.dart'; import '../../apiRequest.dart';
+2 -2
View File
@@ -1,7 +1,7 @@
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
import '../../../model/accountData.dart'; import '../../../model/account_data.dart';
import '../../../model/endpointData.dart'; import '../../../model/endpoint_data.dart';
import '../../apiRequest.dart'; import '../../apiRequest.dart';
import '../../apiResponse.dart'; import '../../apiResponse.dart';
@@ -6,7 +6,7 @@ import 'package:device_info_plus/device_info_plus.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import '../../../../../model/accountData.dart'; import '../../../../../model/account_data.dart';
import '../../../mhslApi.dart'; import '../../../mhslApi.dart';
import 'updateUserIndexParams.dart'; import 'updateUserIndexParams.dart';
@@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import '../../../../model/accountData.dart'; import '../../../../model/account_data.dart';
import '../../webuntisApi.dart'; import '../../webuntisApi.dart';
import 'authenticateParams.dart'; import 'authenticateParams.dart';
import 'authenticateResponse.dart'; import 'authenticateResponse.dart';
+1 -1
View File
@@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../../model/endpointData.dart'; import '../../model/endpoint_data.dart';
import '../apiParams.dart'; import '../apiParams.dart';
import '../apiRequest.dart'; import '../apiRequest.dart';
import '../apiResponse.dart'; import '../apiResponse.dart';
+5 -5
View File
@@ -11,10 +11,10 @@ import 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
import 'api/mhsl/server/userIndex/update/updateUserindex.dart'; import 'api/mhsl/server/userIndex/update/updateUserindex.dart';
import 'main.dart'; import 'main.dart';
import 'widget/breaker/breaker.dart'; import 'widget/breaker/breaker.dart';
import 'model/dataCleaner.dart'; import 'model/data_cleaner.dart';
import 'notification/notificationController.dart'; import 'notification/notification_controller.dart';
import 'notification/notificationTasks.dart'; import 'notification/notification_tasks.dart';
import 'notification/notifyUpdater.dart'; import 'notification/notify_updater.dart';
import 'state/app/modules/app_modules.dart'; import 'state/app/modules/app_modules.dart';
import 'state/app/modules/breaker/bloc/breaker_bloc.dart'; import 'state/app/modules/breaker/bloc/breaker_bloc.dart';
import 'state/app/modules/chatList/bloc/chat_list_bloc.dart'; import 'state/app/modules/chatList/bloc/chat_list_bloc.dart';
@@ -106,7 +106,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
controller: Main.bottomNavigator, controller: Main.bottomNavigator,
navBarOverlap: const NavBarOverlap.none(), navBarOverlap: const NavBarOverlap.none(),
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,
handleAndroidBackButtonPress: false, handleAndroidBackButtonPress: true,
screenTransitionAnimation: const ScreenTransitionAnimation( screenTransitionAnimation: const ScreenTransitionAnimation(
curve: Curves.easeOutQuad, curve: Curves.easeOutQuad,
duration: Duration(milliseconds: 200), duration: Duration(milliseconds: 200),
+4 -4
View File
@@ -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 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
import 'app.dart'; import 'app.dart';
import 'firebase_options.dart'; import 'firebase_options.dart';
import 'model/accountData.dart'; import 'model/account_data.dart';
import 'widget/breaker/breaker.dart'; import 'widget/breaker/breaker.dart';
import 'state/app/modules/account/bloc/account_bloc.dart'; import 'state/app/modules/account/bloc/account_bloc.dart';
import 'state/app/modules/account/bloc/account_state.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/settings/bloc/settings_cubit.dart';
import 'state/app/modules/timetable/bloc/timetable_bloc.dart'; import 'state/app/modules/timetable/bloc/timetable_bloc.dart';
import 'storage/base/settings.dart'; import 'storage/base/settings.dart';
import 'theming/darkAppTheme.dart'; import 'theming/dark_app_theme.dart';
import 'theming/lightAppTheme.dart'; import 'theming/light_app_theme.dart';
import 'view/login/login.dart'; import 'view/login/login.dart';
import 'widget/placeholderView.dart'; import 'widget/placeholder_view.dart';
Future<void> main() async { Future<void> main() async {
log('MarianumMobile started'); log('MarianumMobile started');
@@ -1,5 +1,5 @@
import 'accountData.dart'; import 'account_data.dart';
enum EndpointMode { enum EndpointMode {
live, live,
@@ -1,9 +1,9 @@
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../widget/debug/debugTile.dart'; import '../widget/debug/debug_tile.dart';
import '../widget/debug/jsonViewer.dart'; import '../widget/debug/json_viewer.dart';
import 'notificationTasks.dart'; import 'notification_tasks.dart';
class NotificationController { class NotificationController {
@pragma('vm:entry-point') @pragma('vm:entry-point')
@@ -44,7 +44,7 @@ class NotificationController {
} }
static Future<void> onAppOpenedByNotification(RemoteMessage message, BuildContext context) async { static Future<void> onAppOpenedByNotification(RemoteMessage message, BuildContext context) async {
NotificationTasks.navigateToTalk(context); NotificationTasks.navigateToTalk(context, chatToken: _extractChatToken(message));
NotificationTasks.updateProviders(context); NotificationTasks.updateProviders(context);
DebugTile(context).run(() { 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;
}
} }
@@ -3,8 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter_app_badge/flutter_app_badge.dart'; import 'package:flutter_app_badge/flutter_app_badge.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../main.dart'; import '../routing/app_routes.dart';
import '../state/app/modules/app_modules.dart';
import '../state/app/modules/chat/bloc/chat_bloc.dart'; import '../state/app/modules/chat/bloc/chat_bloc.dart';
import '../state/app/modules/chatList/bloc/chat_list_bloc.dart'; import '../state/app/modules/chatList/bloc/chat_list_bloc.dart';
@@ -18,9 +17,14 @@ class NotificationTasks {
context.read<ChatBloc>().refresh(); context.read<ChatBloc>().refresh();
} }
static void navigateToTalk(BuildContext context) { /// Switches to the Talk tab. If [chatToken] is provided, also schedules
final talkTab = AppModule.getBottomBarModules(context).map((e) => e.module).toList().indexOf(Modules.talk); /// the matching chat to be opened automatically once the chat list view
if (talkTab == -1) return; /// resolves the token (handled inside [ChatList]).
Main.bottomNavigator.jumpToTab(talkTab); static void navigateToTalk(BuildContext context, {String? chatToken}) {
if (chatToken != null && chatToken.isNotEmpty) {
AppRoutes.openChatByToken(context, chatToken);
} else {
AppRoutes.goToTalkTab(context);
}
} }
} }
@@ -3,9 +3,9 @@ import 'package:flutter/material.dart';
import '../api/mhsl/notify/register/notifyRegister.dart'; import '../api/mhsl/notify/register/notifyRegister.dart';
import '../api/mhsl/notify/register/notifyRegisterParams.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 '../state/app/modules/settings/bloc/settings_cubit.dart';
import '../widget/confirmDialog.dart'; import '../widget/confirm_dialog.dart';
class NotifyUpdater { class NotifyUpdater {
static ConfirmDialog enableAfterDisclaimer(SettingsCubit settings) => ConfirmDialog( static ConfirmDialog enableAfterDisclaimer(SettingsCubit settings) => ConfirmDialog(
+202
View File
@@ -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<String?> pendingChatToken = ValueNotifier(null);
// -- Files --------------------------------------------------------------
static void openFolder(BuildContext context, List<String> 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<ChatBloc>().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<ChatListBloc>().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<ChatListBloc>().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,
});
}
@@ -3,7 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../widget/infoDialog.dart'; import '../../../../../widget/info_dialog.dart';
import '../bloc/loadable_state_bloc.dart'; import '../bloc/loadable_state_bloc.dart';
class LoadableStateErrorBar extends StatelessWidget { class LoadableStateErrorBar extends StatelessWidget {
+12 -11
View File
@@ -2,23 +2,24 @@ import 'package:flutter/material.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:badges/badges.dart' as badges;
import '../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart'; 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/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/more/roomplan/roomplan.dart';
import '../../../view/pages/talk/chatList.dart'; import '../../../view/pages/talk/chat_list.dart';
import '../../../view/pages/timetable/timetable.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_bloc.dart';
import 'chatList/bloc/chat_list_state.dart'; import 'chatList/bloc/chat_list_state.dart';
import 'settings/bloc/settings_cubit.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 { class AppModule {
Modules module; Modules module;
@@ -120,7 +121,7 @@ class AppModule {
key: key, key: key,
leading: CenteredLeading(icon()), leading: CenteredLeading(icon()),
title: Text(name), title: Text(name),
onTap: isReorder ? null : () => pushScreen(context, withNavBar: false, screen: create()), onTap: isReorder ? null : () => AppRoutes.openModule(context, this),
trailing: isReorder trailing: isReorder
? Row(mainAxisSize: MainAxisSize.min, children: [ ? Row(mainAxisSize: MainAxisSize.min, children: [
IconButton(onPressed: onVisibleChange, icon: Icon(isVisible ? Icons.visibility_outlined : Icons.visibility_off_outlined)), IconButton(onPressed: onVisibleChange, icon: Icon(isVisible ? Icons.visibility_outlined : Icons.visibility_off_outlined)),
@@ -4,7 +4,7 @@ import 'package:easy_debounce/easy_debounce.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import '../../../../../storage/base/settings.dart'; import '../../../../../storage/base/settings.dart';
import '../../../../../view/settings/defaultSettings.dart'; import '../../../../../view/pages/settings/data/default_settings.dart';
import '../../app_modules.dart'; import '../../app_modules.dart';
class SettingsCubit extends HydratedCubit<Settings> { class SettingsCubit extends HydratedCubit<Settings> {
@@ -20,7 +20,7 @@ import '../../../../../api/webuntis/queries/getTimegridUnits/getTimegridUnitsCac
import '../../../../../api/webuntis/queries/getTimegridUnits/getTimegridUnitsResponse.dart'; import '../../../../../api/webuntis/queries/getTimegridUnits/getTimegridUnitsResponse.dart';
import '../../../../../api/webuntis/queries/getTimetable/getTimetableCache.dart'; import '../../../../../api/webuntis/queries/getTimetable/getTimetableCache.dart';
import '../../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart'; import '../../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
import '../../../../../model/accountData.dart'; import '../../../../../model/account_data.dart';
class TimetableDataProvider { class TimetableDataProvider {
static final DateFormat _dateFormat = DateFormat('yyyyMMdd'); static final DateFormat _dateFormat = DateFormat('yyyyMMdd');
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../widget/dropdownDisplay.dart'; import '../../widget/dropdown_display.dart';
enum TimetableNameMode { name, longName, alternateName } enum TimetableNameMode { name, longName, alternateName }
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../widget/dropdownDisplay.dart'; import '../widget/dropdown_display.dart';
class AppTheme { class AppTheme {
static DropdownDisplay getDisplayOptions(ThemeMode theme) { static DropdownDisplay getDisplayOptions(ThemeMode theme) {
+1 -1
View File
@@ -7,7 +7,7 @@ import 'package:flutter_login/flutter_login.dart';
import '../../api/marianumcloud/talk/room/getRoom.dart'; import '../../api/marianumcloud/talk/room/getRoom.dart';
import '../../api/marianumcloud/talk/room/getRoomParams.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_bloc.dart';
import '../../state/app/modules/account/bloc/account_state.dart'; import '../../state/app/modules/account/bloc/account_state.dart';
+4 -4
View File
@@ -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_bloc.dart';
import '../../../state/app/modules/files/bloc/files_state.dart'; import '../../../state/app/modules/files/bloc/files_state.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../widget/filePick.dart'; import '../../../widget/file_pick.dart';
import '../../../widget/placeholderView.dart'; import '../../../widget/placeholder_view.dart';
import 'fileElement.dart'; import 'widgets/file_element.dart';
import 'filesUploadDialog.dart'; import 'files_upload_dialog.dart';
class BetterSortOption { class BetterSortOption {
String displayName; String displayName;
@@ -6,8 +6,8 @@ import 'package:nextcloud/nextcloud.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../../../api/marianumcloud/webdav/webdavApi.dart'; import '../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../widget/confirmDialog.dart'; import '../../../widget/confirm_dialog.dart';
import '../../../widget/focusBehaviour.dart'; import '../../../widget/focus_behaviour.dart';
class FilesUploadDialog extends StatefulWidget { class FilesUploadDialog extends StatefulWidget {
final List<String> filePaths; final List<String> filePaths;
@@ -7,19 +7,18 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:open_filex/open_filex.dart'; import 'package:open_filex/open_filex.dart';
import '../../../widget/infoDialog.dart'; import '../../../../widget/info_dialog.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart'; import '../../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
import '../../../api/marianumcloud/webdav/webdavApi.dart'; import '../../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../model/accountData.dart'; import '../../../../model/account_data.dart';
import '../../../model/endpointData.dart'; import '../../../../model/endpoint_data.dart';
import '../../../widget/centeredLeading.dart'; import '../../../../routing/app_routes.dart';
import '../../../widget/confirmDialog.dart'; import '../../../../widget/centered_leading.dart';
import '../../../widget/fileViewer.dart'; import '../../../../widget/confirm_dialog.dart';
import '../../../widget/unimplementedDialog.dart'; import '../../../../widget/unimplemented_dialog.dart';
import 'files.dart';
class FileElement extends StatefulWidget { class FileElement extends StatefulWidget {
final CacheableFile file; final CacheableFile file;
@@ -45,12 +44,8 @@ class FileElement extends StatefulWidget {
deleteOnCancel: true, deleteOnCancel: true,
client: Dio(BaseOptions(headers: AccountData().authHeaders())), client: Dio(BaseOptions(headers: AccountData().authHeaders())),
onDone: () { onDone: () {
//Future<OpenResult> result = OpenFile.open(local); // TODO legacy - refactor: remove onDone parameter AppRoutes.openFileViewer(context, local);
Navigator.of(context).push(MaterialPageRoute(builder: (context) => FileViewer(path: local)));
onDone(OpenResult(message: 'File viewer opened', type: ResultType.done)); onDone(OpenResult(message: 'File viewer opened', type: ResultType.done));
// result.then((value) => {
// onDone(value)
// });
}, },
); );
@@ -101,9 +96,7 @@ class _FileElementState extends State<FileElement> {
trailing: Icon(widget.file.isDirectory ? Icons.arrow_right : null), trailing: Icon(widget.file.isDirectory ? Icons.arrow_right : null),
onTap: () { onTap: () {
if(widget.file.isDirectory) { if(widget.file.isDirectory) {
Navigator.of(context).push(MaterialPageRoute( AppRoutes.openFolder(context, widget.path.toList()..add(widget.file.name));
builder: (context) => Files(path: widget.path.toList()..add(widget.file.name)),
));
} else { } else {
if(EndpointData().getEndpointMode() == EndpointMode.stage) { if(EndpointData().getEndpointMode() == EndpointMode.stage) {
InfoDialog.show(context, 'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!'); InfoDialog.show(context, 'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!');
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../bloc/grade_averages_bloc.dart'; import '../../../state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart';
import '../bloc/grade_averages_event.dart'; import '../../../state/app/modules/gradeAverages/bloc/grade_averages_event.dart';
class GradeAveragesListView extends StatelessWidget { class GradeAveragesListView extends StatelessWidget {
const GradeAveragesListView({super.key}); const GradeAveragesListView({super.key});
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../widget/confirmDialog.dart'; import '../../../state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart';
import '../bloc/grade_averages_bloc.dart'; import '../../../state/app/modules/gradeAverages/bloc/grade_averages_event.dart';
import '../bloc/grade_averages_event.dart'; import '../../../state/app/modules/gradeAverages/bloc/grade_averages_state.dart';
import '../bloc/grade_averages_state.dart'; import '../../../widget/confirm_dialog.dart';
import 'grade_averages_list_view.dart'; import 'grade_averages_list_view.dart';
class GradeAveragesView extends StatelessWidget { class GradeAveragesView extends StatelessWidget {
@@ -1,17 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import '../../../../../widget/animatedTime.dart'; import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../../../widget/list_view_util.dart'; import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../../../widget/centeredLeading.dart'; import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../../../widget/debug/debugTile.dart'; import '../../../state/app/modules/holidays/bloc/holidays_bloc.dart';
import '../../../../../widget/string_extensions.dart'; import '../../../state/app/modules/holidays/bloc/holidays_event.dart';
import '../../../infrastructure/loadableState/loadable_state.dart'; import '../../../state/app/modules/holidays/bloc/holidays_state.dart';
import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart'; import '../../../widget/animated_time.dart';
import '../../../infrastructure/utilityWidgets/bloc_module.dart'; import '../../../widget/centered_leading.dart';
import '../bloc/holidays_bloc.dart'; import '../../../widget/debug/debug_tile.dart';
import '../bloc/holidays_event.dart'; import '../../../widget/list_view_util.dart';
import '../bloc/holidays_state.dart'; import '../../../widget/string_extensions.dart';
class HolidaysView extends StatelessWidget { class HolidaysView extends StatelessWidget {
const HolidaysView({super.key}); const HolidaysView({super.key});
@@ -1,17 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import '../../../../../view/pages/timetable/custom_events/custom_event_edit_dialog.dart'; import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../../../widget/animatedTime.dart'; import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../../../widget/centeredLeading.dart'; import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../../../widget/debug/debugTile.dart'; import '../../../state/app/modules/marianumDates/bloc/marianum_dates_bloc.dart';
import '../../../../../widget/list_view_util.dart'; import '../../../state/app/modules/marianumDates/bloc/marianum_dates_event.dart';
import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart'; import '../../../state/app/modules/marianumDates/bloc/marianum_dates_state.dart';
import '../../../infrastructure/utilityWidgets/bloc_module.dart'; import '../../../widget/animated_time.dart';
import '../../../infrastructure/loadableState/loadable_state.dart'; import '../../../widget/centered_leading.dart';
import '../bloc/marianum_dates_bloc.dart'; import '../../../widget/debug/debug_tile.dart';
import '../bloc/marianum_dates_event.dart'; import '../../../widget/list_view_util.dart';
import '../bloc/marianum_dates_state.dart'; import '../timetable/custom_events/custom_event_edit_dialog.dart';
class MarianumDatesView extends StatelessWidget { class MarianumDatesView extends StatelessWidget {
const MarianumDatesView({super.key}); const MarianumDatesView({super.key});
@@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'marianum_message_view.dart'; import '../../../routing/app_routes.dart';
import '../../../infrastructure/loadableState/loadable_state.dart'; import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart'; import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../infrastructure/utilityWidgets/bloc_module.dart'; import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../bloc/marianum_message_bloc.dart'; import '../../../state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart';
import '../bloc/marianum_message_state.dart'; import '../../../state/app/modules/marianumMessage/bloc/marianum_message_state.dart';
class MarianumMessageListView extends StatelessWidget { class MarianumMessageListView extends StatelessWidget {
const MarianumMessageListView({super.key}); const MarianumMessageListView({super.key});
@@ -31,7 +31,7 @@ class MarianumMessageListView extends StatelessWidget {
subtitle: Text('vom ${message.date}'), subtitle: Text('vom ${message.date}'),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () { onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: state.messageList.base, message: message))); AppRoutes.openMarianumMessage(context, state.messageList.base, message);
}, },
); );
} }
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../bloc/marianum_message_state.dart'; import '../../../state/app/modules/marianumMessage/bloc/marianum_message_state.dart';
import '../../../../../widget/confirmDialog.dart'; import '../../../widget/confirm_dialog.dart';
class MessageView extends StatefulWidget { class MessageView extends StatefulWidget {
final String basePath; final String basePath;
@@ -11,11 +11,11 @@ import 'package:badges/badges.dart' as badges;
import '../../../../api/mhsl/server/feedback/addFeedback.dart'; import '../../../../api/mhsl/server/feedback/addFeedback.dart';
import '../../../../api/mhsl/server/feedback/addFeedbackParams.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 '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../widget/filePick.dart'; import '../../../../widget/file_pick.dart';
import '../../../../widget/focusBehaviour.dart'; import '../../../../widget/focus_behaviour.dart';
import '../../../../widget/infoDialog.dart'; import '../../../../widget/info_dialog.dart';
class FeedbackDialog extends StatefulWidget { class FeedbackDialog extends StatefulWidget {
const FeedbackDialog({super.key}); const FeedbackDialog({super.key});
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
import 'appSharePlatformView.dart'; import 'app_share_platform_view.dart';
class QrShareView extends StatefulWidget { class QrShareView extends StatefulWidget {
const QrShareView({super.key}); const QrShareView({super.key});
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../../../../widget/sharePositionOrigin.dart'; import '../../../../widget/share_position_origin.dart';
import 'qrShareView.dart';
enum ShareTargetType { qr }
class SelectShareTypeDialog extends StatelessWidget { class SelectShareTypeDialog extends StatelessWidget {
const SelectShareTypeDialog({super.key}); const SelectShareTypeDialog({super.key});
@@ -14,15 +15,14 @@ class SelectShareTypeDialog extends StatelessWidget {
leading: const Icon(Icons.qr_code_2_outlined), leading: const Icon(Icons.qr_code_2_outlined),
title: const Text('Per QR-Code'), title: const Text('Per QR-Code'),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () { onTap: () => Navigator.of(context).pop(ShareTargetType.qr),
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const QrShareView()));
},
), ),
ListTile( ListTile(
leading: const Icon(Icons.link_outlined), leading: const Icon(Icons.link_outlined),
title: const Text('Per Link teilen'), title: const Text('Per Link teilen'),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () { onTap: () {
Navigator.of(context).pop();
SharePlus.instance.share(ShareParams( SharePlus.instance.share(ShareParams(
sharePositionOrigin: SharePositionOrigin.get(context), sharePositionOrigin: SharePositionOrigin.get(context),
subject: 'App Teilen', subject: 'App Teilen',
+16 -11
View File
@@ -4,18 +4,16 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:in_app_review/in_app_review.dart'; import 'package:in_app_review/in_app_review.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../extensions/renderNotNull.dart'; import '../../extensions/render_not_null.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import '../../routing/app_routes.dart';
import '../../state/app/modules/app_modules.dart'; import '../../state/app/modules/app_modules.dart';
import '../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../storage/base/settings.dart' as model; import '../../storage/base/settings.dart' as model;
import '../../widget/centeredLeading.dart'; import '../../widget/centered_leading.dart';
import '../../widget/infoDialog.dart'; import '../../widget/info_dialog.dart';
import '../settings/defaultSettings.dart'; import 'settings/data/default_settings.dart';
import '../settings/settings.dart'; import 'more/share/select_share_type_dialog.dart';
import 'more/feedback/feedbackDialog.dart';
import 'more/share/selectShareTypeDialog.dart';
class Overhang extends StatefulWidget { class Overhang extends StatefulWidget {
const Overhang({super.key}); const Overhang({super.key});
@@ -41,7 +39,7 @@ class _OverhangState extends State<Overhang> {
icon: Icon(Icons.undo_outlined) 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: () => 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(), body: editMode ? _sorting() : _overhang(),
@@ -92,7 +90,14 @@ class _OverhangState extends State<Overhang> {
title: const Text('Teile die App'), title: const Text('Teile die App'),
subtitle: const Text('Mit Freunden und deiner Klasse teilen'), subtitle: const Text('Mit Freunden und deiner Klasse teilen'),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () => showDialog(context: context, builder: (context) => const SelectShareTypeDialog()) onTap: () async {
final result = await showDialog<ShareTargetType>(
context: context,
builder: (_) => const SelectShareTypeDialog(),
);
if (!mounted || result != ShareTargetType.qr) return;
if (context.mounted) AppRoutes.openQrShare(context);
},
), ),
FutureBuilder( FutureBuilder(
future: InAppReview.instance.isAvailable(), future: InAppReview.instance.isAvailable(),
@@ -130,7 +135,7 @@ class _OverhangState extends State<Overhang> {
title: const Text('Du hast eine Idee?'), title: const Text('Du hast eine Idee?'),
subtitle: const Text('Fehler und Verbessungsvorschläge'), subtitle: const Text('Fehler und Verbessungsvorschläge'),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () => pushScreen(context, withNavBar: false, screen: const FeedbackDialog()), onTap: () => AppRoutes.openFeedback(context),
), ),
], ],
); );
@@ -2,18 +2,18 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../state/app/modules/app_modules.dart'; import '../../../../state/app/modules/app_modules.dart';
import '../../storage/base/settings.dart'; import '../../../../storage/base/settings.dart';
import '../../storage/devTools/devToolsSettings.dart'; import '../../../../storage/devTools/devToolsSettings.dart';
import '../../storage/file/fileSettings.dart'; import '../../../../storage/file/fileSettings.dart';
import '../../storage/fileView/fileViewSettings.dart'; import '../../../../storage/fileView/fileViewSettings.dart';
import '../../storage/general/modulesSettings.dart'; import '../../../../storage/general/modulesSettings.dart';
import '../../storage/holidays/holidaysSettings.dart'; import '../../../../storage/holidays/holidaysSettings.dart';
import '../../storage/notification/notificationSettings.dart'; import '../../../../storage/notification/notificationSettings.dart';
import '../../storage/talk/talkSettings.dart'; import '../../../../storage/talk/talkSettings.dart';
import '../../storage/timetable/timetableSettings.dart'; import '../../../../storage/timetable/timetable_name_mode.dart';
import '../pages/files/files.dart'; import '../../../../storage/timetable/timetableSettings.dart';
import '../../storage/timetable/timetable_name_mode.dart'; import '../../files/files.dart';
class DefaultSettings { class DefaultSettings {
static Settings get() => Settings( static Settings get() => Settings(
@@ -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<SettingsCubit>();
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<void> _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);
}
}
@@ -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<SettingsCubit>().reset();
const CacheView().clear();
AccountData().removeData(context: context);
},
),
);
}
}
@@ -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<SettingsCubit>();
return ListTile(
leading: const Icon(Icons.dark_mode_outlined),
title: const Text('Farbgebung'),
trailing: DropdownButton<ThemeMode>(
value: settings.val().appTheme,
icon: const Icon(Icons.arrow_drop_down),
items: ThemeMode.values
.map((e) => DropdownMenuItem<ThemeMode>(
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!,
),
);
}
}
@@ -4,21 +4,22 @@ import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../../../routing/app_routes.dart';
import '../../widget/centeredLeading.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../widget/confirmDialog.dart'; import '../../../../widget/centered_leading.dart';
import '../../widget/debug/cacheView.dart'; import '../../../../widget/confirm_dialog.dart';
import '../../widget/debug/jsonViewer.dart'; import '../../../../widget/debug/cache_view.dart';
import '../../../../widget/debug/json_viewer.dart';
class DevToolsSettings extends StatefulWidget { class DevToolsSection extends StatefulWidget {
final SettingsCubit settings; final SettingsCubit settings;
const DevToolsSettings({required this.settings, super.key}); const DevToolsSection({required this.settings, super.key});
@override @override
State<DevToolsSettings> createState() => _DevToolsSettingsState(); State<DevToolsSection> createState() => _DevToolsSectionState();
} }
class _DevToolsSettingsState extends State<DevToolsSettings> { class _DevToolsSectionState extends State<DevToolsSection> {
@override @override
Widget build(BuildContext context) => Column( Widget build(BuildContext context) => Column(
children: [ children: [
@@ -96,9 +97,7 @@ class _DevToolsSettingsState extends State<DevToolsSettings> {
future: const CacheView().totalSize(), future: const CacheView().totalSize(),
builder: (context, snapshot) => Text("etwa ${snapshot.hasError ? "?" : snapshot.hasData ? filesize(snapshot.data) : "..."}\nLange tippen um zu löschen"), builder: (context, snapshot) => Text("etwa ${snapshot.hasError ? "?" : snapshot.hasData ? filesize(snapshot.data) : "..."}\nLange tippen um zu löschen"),
), ),
onTap: () { onTap: () => AppRoutes.openCacheView(context),
Navigator.push(context, MaterialPageRoute(builder: (context) => const CacheView()));
},
onLongPress: () { onLongPress: () {
ConfirmDialog( ConfirmDialog(
title: 'App-Cache löschen', title: 'App-Cache löschen',
@@ -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<SettingsCubit>();
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!,
),
),
],
);
}
}
@@ -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<SettingsCubit>();
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')),
],
),
);
}
@@ -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<SettingsCubit>();
final timetableBloc = context.read<TimetableBloc>();
final timetableSettings = settings.val().timetableSettings;
return Column(
children: [
ListTile(
leading: const Icon(Icons.abc_outlined),
title: const Text('Fachbezeichnung'),
trailing: DropdownButton<TimetableNameMode>(
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();
},
),
),
],
);
}
}
+37
View File
@@ -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<SettingsCubit, model.Settings>(
builder: (context, _) => Scaffold(
appBar: AppBar(title: const Text('Einstellungen')),
body: ListView(
children: const [
AccountSection(),
Divider(),
AppearanceSection(),
Divider(),
TimetableSection(),
Divider(),
TalkSection(),
Divider(),
FilesSection(),
Divider(),
AboutSection(),
],
),
),
);
}
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../widget/centeredLeading.dart'; import '../../../../widget/centered_leading.dart';
import '../../widget/confirmDialog.dart'; import '../../../../widget/confirm_dialog.dart';
class PrivacyInfo { class PrivacyInfo {
String providerText; String providerText;
-142
View File
@@ -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<ChatListBloc, LoadableState<ChatListState>>(
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<SettingsCubit>();
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<ChatListBloc>();
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<ChatListBloc, ChatListState>(
child: (state, _) {
final rooms = state.rooms;
if (rooms == null) return const SizedBox.shrink();
final talkSettings = context.watch<SettingsCubit>().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(),
);
},
),
),
);
}
}
+180
View File
@@ -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<ChatListBloc, LoadableState<ChatListState>>(
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<SettingsCubit>();
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<ChatListBloc>();
return SplitView.material(
placeholder: const SplitViewPlaceholder(),
breakpoint: 1000,
child: BlocListener<ChatListBloc, LoadableState<ChatListState>>(
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<ChatListBloc, ChatListState>(
child: (state, _) {
final rooms = state.rooms;
if (rooms == null) return const SizedBox.shrink();
final talkSettings = context.watch<SettingsCubit>().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(),
);
},
),
),
),
);
}
}
@@ -3,17 +3,17 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../api/marianumcloud/talk/room/getRoomResponse.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/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/modules/chat/bloc/chat_bloc.dart'; import '../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../state/app/modules/chat/bloc/chat_state.dart'; import '../../../state/app/modules/chat/bloc/chat_state.dart';
import '../../../theming/appTheme.dart'; import '../../../theming/app_theme.dart';
import '../../../widget/clickableAppBar.dart'; import '../../../widget/clickable_app_bar.dart';
import '../../../widget/userAvatar.dart'; import '../../../widget/user_avatar.dart';
import 'chatDetails/chatInfo.dart'; import 'details/chat_info.dart';
import 'components/chatBubble.dart'; import 'widgets/chat_bubble.dart';
import 'components/chatTextfield.dart'; import 'widgets/chat_textfield.dart';
import 'talkNavigator.dart'; import 'talk_navigator.dart';
class ChatView extends StatefulWidget { class ChatView extends StatefulWidget {
final GetRoomResponseObject room; final GetRoomResponseObject room;
@@ -1,7 +1,7 @@
import 'package:bubble/bubble.dart'; import 'package:bubble/bubble.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../theming/appTheme.dart'; import '../../../../theming/app_theme.dart';
extension ColorExtensions on Color { extension ColorExtensions on Color {
Color invert() { Color invert() {
@@ -5,9 +5,9 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart'; import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
import '../../../../model/accountData.dart'; import '../../../../model/account_data.dart';
import '../../../../model/endpointData.dart'; import '../../../../model/endpoint_data.dart';
import '../../../../utils/UrlOpener.dart'; import '../../../../utils/url_opener.dart';
class ChatMessage { class ChatMessage {
String originalMessage; String originalMessage;
@@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsCache.dart'; import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsCache.dart';
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart'; import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart';
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart'; import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../widget/largeProfilePictureView.dart'; import '../../../../widget/large_profile_picture_view.dart';
import '../../../../widget/loadingSpinner.dart'; import '../../../../widget/loading_spinner.dart';
import '../../../../widget/userAvatar.dart'; import '../../../../widget/user_avatar.dart';
import '../talkNavigator.dart'; import '../talk_navigator.dart';
import 'participants/participantsListView.dart'; import 'participants_list_view.dart';
class ChatInfo extends StatefulWidget { class ChatInfo extends StatefulWidget {
final GetRoomResponseObject room; final GetRoomResponseObject room;
@@ -1,14 +1,14 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../api/marianumcloud/talk/getReactions/getReactions.dart'; import '../../../../api/marianumcloud/talk/getReactions/getReactions.dart';
import '../../../api/marianumcloud/talk/getReactions/getReactionsResponse.dart'; import '../../../../api/marianumcloud/talk/getReactions/getReactionsResponse.dart';
import '../../../model/accountData.dart'; import '../../../../model/account_data.dart';
import '../../../widget/centeredLeading.dart'; import '../../../../widget/centered_leading.dart';
import '../../../widget/loadingSpinner.dart'; import '../../../../widget/loading_spinner.dart';
import '../../../widget/placeholderView.dart'; import '../../../../widget/placeholder_view.dart';
import '../../../widget/unimplementedDialog.dart'; import '../../../../widget/unimplemented_dialog.dart';
import '../../../widget/userAvatar.dart'; import '../../../../widget/user_avatar.dart';
class MessageReactions extends StatefulWidget { class MessageReactions extends StatefulWidget {
final String token; final String token;
@@ -1,8 +1,8 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart'; import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart';
import '../../../../../widget/userAvatar.dart'; import '../../../../widget/user_avatar.dart';
class ParticipantsListView extends StatelessWidget { class ParticipantsListView extends StatelessWidget {
final GetParticipantsResponse participantsResponse; final GetParticipantsResponse participantsResponse;
@@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
import '../../../api/marianumcloud/autocomplete/autocompleteApi.dart'; import '../../../api/marianumcloud/autocomplete/autocompleteApi.dart';
import '../../../api/marianumcloud/autocomplete/autocompleteResponse.dart'; import '../../../api/marianumcloud/autocomplete/autocompleteResponse.dart';
import '../../../model/endpointData.dart'; import '../../../model/endpoint_data.dart';
import '../../../widget/placeholderView.dart'; import '../../../widget/placeholder_view.dart';
class JoinChat extends SearchDelegate<String> { class JoinChat extends SearchDelegate<String> {
CancelableOperation<AutocompleteResponse>? future; CancelableOperation<AutocompleteResponse>? future;
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../api/marianumcloud/talk/room/getRoomResponse.dart'; import '../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import 'components/chatTile.dart'; import 'widgets/chat_tile.dart';
class SearchChat extends SearchDelegate { class SearchChat extends SearchDelegate {
List<GetRoomResponseObject> chats; List<GetRoomResponseObject> chats;
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart'; import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
import 'chatBubbleStyles.dart'; import '../data/chat_bubble_styles.dart';
class AnswerReference extends StatelessWidget { class AnswerReference extends StatelessWidget {
final BuildContext context; final BuildContext context;
@@ -1,31 +1,26 @@
import 'package:bubble/bubble.dart'; import 'package:bubble/bubble.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis;
import 'package:flowder/flowder.dart'; import 'package:flowder/flowder.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.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:jiffy/jiffy.dart';
import 'package:open_filex/open_filex.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/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/deleteMessage/deleteMessage.dart';
import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessage.dart'; import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessage.dart';
import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessageParams.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/reactMessage.dart';
import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart'; import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart';
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart'; import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../extensions/text.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart'; import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../widget/debug/debugTile.dart'; import '../../../../widget/loading_spinner.dart';
import '../../../../widget/loadingSpinner.dart'; import '../../files/widgets/file_element.dart';
import '../../files/fileElement.dart'; import '../data/chat_bubble_styles.dart';
import 'answerReference.dart'; import '../data/chat_message.dart';
import 'chatBubbleStyles.dart'; import 'answer_reference.dart';
import 'chatMessage.dart'; import 'chat_message_options_dialog.dart';
import '../messageReactions.dart'; import 'poll_options_list.dart';
import 'pollOptionsList.dart';
class ChatBubble extends StatefulWidget { class ChatBubble extends StatefulWidget {
final BuildContext context; final BuildContext context;
@@ -77,176 +72,13 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
} }
void showOptionsDialog() { void showOptionsDialog() {
showDialog(context: context, builder: (context) { showChatMessageOptionsDialog(
var commonReactions = <String>['👍', '👎', '😆', '❤️', '👀']; context,
var canReact = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment; chatData: widget.chatData,
return SimpleDialog( bubbleData: widget.bubbleData,
children: [ isSender: widget.isSender,
Visibility( onRefetch: widget.refetch,
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<ChatBloc>().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<ChatBloc>().refresh();
Navigator.of(context).pop();
});
},
),
),
DebugTile(context).jsonData(widget.bubbleData.toJson()),
],
);
});
} }
@@ -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 = <String>['👍', '👎', '😆', '❤️', '👀'];
/// 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<void> 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<ChatBloc>().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<ChatBloc>().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);
},
),
),
),
);
}
}
@@ -12,10 +12,10 @@ import '../../../../api/marianumcloud/talk/sendMessage/sendMessageParams.dart';
import '../../../../api/marianumcloud/webdav/webdavApi.dart'; import '../../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart'; import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../widget/filePick.dart'; import '../../../../widget/file_pick.dart';
import '../../../../widget/focusBehaviour.dart'; import '../../../../widget/focus_behaviour.dart';
import '../../files/filesUploadDialog.dart'; import '../../files/files_upload_dialog.dart';
import 'answerReference.dart'; import 'answer_reference.dart';
class ChatTextfield extends StatefulWidget { class ChatTextfield extends StatefulWidget {
final String sendToToken; final String sendToToken;
@@ -9,14 +9,14 @@ import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../api/marianumcloud/talk/setFavorite/setFavorite.dart'; import '../../../../api/marianumcloud/talk/setFavorite/setFavorite.dart';
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarker.dart'; import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarker.dart';
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarkerParams.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/chat/bloc/chat_bloc.dart';
import '../../../../state/app/modules/chatList/bloc/chat_list_bloc.dart'; import '../../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
import '../../../../widget/confirmDialog.dart'; import '../../../../widget/confirm_dialog.dart';
import '../../../../widget/debug/debugTile.dart'; import '../../../../widget/debug/debug_tile.dart';
import '../../../../widget/userAvatar.dart'; import '../../../../widget/user_avatar.dart';
import '../chatView.dart'; import '../chat_view.dart';
import '../talkNavigator.dart'; import '../talk_navigator.dart';
class ChatTile extends StatefulWidget { class ChatTile extends StatefulWidget {
final GetRoomResponseObject data; final GetRoomResponseObject data;
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_linkify/flutter_linkify.dart';
import '../../../../api/marianumcloud/talk/getPoll/getPollStateResponse.dart'; import '../../../../api/marianumcloud/talk/getPoll/getPollStateResponse.dart';
import '../../../../utils/UrlOpener.dart'; import '../../../../utils/url_opener.dart';
class PollOptionsList extends StatefulWidget { class PollOptionsList extends StatefulWidget {
final GetPollStateResponseObject pollData; final GetPollStateResponseObject pollData;
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../theming/appTheme.dart'; import '../../../../theming/app_theme.dart';
class SplitViewPlaceholder extends StatelessWidget { class SplitViewPlaceholder extends StatelessWidget {
const SplitViewPlaceholder({super.key}); const SplitViewPlaceholder({super.key});
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../theming/darkAppTheme.dart'; import '../../../../theming/dark_app_theme.dart';
enum CustomTimetableColors { orange, red, green, blue } enum CustomTimetableColors { orange, red, green, blue }
@@ -7,10 +7,10 @@ import 'package:rrule_generator/rrule_generator.dart';
import 'package:time_range_picker/time_range_picker.dart'; import 'package:time_range_picker/time_range_picker.dart';
import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.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 '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../../../widget/focusBehaviour.dart'; import '../../../../widget/focus_behaviour.dart';
import '../../../../widget/infoDialog.dart'; import '../../../../widget/info_dialog.dart';
import 'custom_event_colors.dart'; import 'custom_event_colors.dart';
class CustomEventEditDialog extends StatefulWidget { class CustomEventEditDialog extends StatefulWidget {
@@ -4,8 +4,8 @@ import 'package:jiffy/jiffy.dart';
import '../../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.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_bloc.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_state.dart'; import '../../../../state/app/modules/timetable/bloc/timetable_state.dart';
import '../../../../widget/centeredLeading.dart'; import '../../../../widget/centered_leading.dart';
import '../../../../widget/placeholderView.dart'; import '../../../../widget/placeholder_view.dart';
import '../details/delete_custom_event.dart'; import '../details/delete_custom_event.dart';
import 'custom_event_edit_dialog.dart'; import 'custom_event_edit_dialog.dart';
@@ -3,8 +3,8 @@ import 'package:jiffy/jiffy.dart';
import 'package:rrule/rrule.dart'; import 'package:rrule/rrule.dart';
import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart'; import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
import '../../../../widget/centeredLeading.dart'; import '../../../../widget/centered_leading.dart';
import '../../../../widget/debug/debugTile.dart'; import '../../../../widget/debug/debug_tile.dart';
import '../custom_events/custom_event_edit_dialog.dart'; import '../custom_events/custom_event_edit_dialog.dart';
import '_bottom_sheet.dart'; import '_bottom_sheet.dart';
import 'delete_custom_event.dart'; import 'delete_custom_event.dart';
@@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart'; import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart'; import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../../../widget/confirmDialog.dart'; import '../../../../widget/confirm_dialog.dart';
Completer<void> showDeleteCustomEventDialog(BuildContext context, CustomTimetableEvent event) { Completer<void> showDeleteCustomEventDialog(BuildContext context, CustomTimetableEvent event) {
final completer = Completer<void>(); final completer = Completer<void>();
@@ -1,17 +1,16 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.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 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../../api/webuntis/queries/getRooms/getRoomsResponse.dart'; import '../../../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
import '../../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart'; import '../../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart';
import '../../../../api/webuntis/queries/getTimetable/getTimetableResponse.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_bloc.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_state.dart'; import '../../../../state/app/modules/timetable/bloc/timetable_state.dart';
import '../../../../widget/debug/debugTile.dart'; import '../../../../widget/debug/debug_tile.dart';
import '../../../../widget/unimplementedDialog.dart'; import '../../../../widget/unimplemented_dialog.dart';
import '../../more/roomplan/roomplan.dart';
import '_bottom_sheet.dart'; import '_bottom_sheet.dart';
class WebuntisLessonSheet { class WebuntisLessonSheet {
@@ -54,7 +53,7 @@ class WebuntisLessonSheet {
title: Text('Raum: ${room.name} (${room.longName})'), title: Text('Raum: ${room.name} (${room.longName})'),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.house_outlined), icon: const Icon(Icons.house_outlined),
onPressed: () => pushScreen(context, withNavBar: false, screen: const Roomplan()), onPressed: () => AppRoutes.openRoomplan(context),
), ),
), ),
ListTile( ListTile(
+3 -3
View File
@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncfusion_flutter_calendar/calendar.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/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/modules/timetable/bloc/timetable_bloc.dart'; import '../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../../state/app/modules/timetable/bloc/timetable_state.dart'; import '../../../state/app/modules/timetable/bloc/timetable_state.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import 'custom_events/custom_event_edit_dialog.dart'; import 'custom_events/custom_event_edit_dialog.dart';
import 'custom_events/custom_events_view.dart';
import 'data/arbitrary_appointment.dart'; import 'data/arbitrary_appointment.dart';
import 'data/lesson_period_schedule.dart'; import 'data/lesson_period_schedule.dart';
import 'data/timetable_appointment_factory.dart'; import 'data/timetable_appointment_factory.dart';
@@ -46,7 +46,7 @@ class _TimetableState extends State<Timetable> {
barrierDismissible: false, barrierDismissible: false,
); );
case _CalendarAction.viewEvents: case _CalendarAction.viewEvents:
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const CustomEventsView())); AppRoutes.openCustomEvents(context);
} }
} }
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart'; import '../../../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart';
import '../../../../extensions/dateTime.dart'; import '../../../../extensions/date_time.dart';
import '../data/calendar_layout.dart'; import '../data/calendar_layout.dart';
import '../data/lesson_period_schedule.dart'; import '../data/lesson_period_schedule.dart';
import '../data/webuntis_time.dart'; import '../data/webuntis_time.dart';
-318
View File
@@ -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<Settings> createState() => _SettingsState();
}
class _SettingsState extends State<Settings> {
@override
void initState() {
super.initState();
}
bool developerMode = false;
@override
Widget build(BuildContext context) => BlocBuilder<SettingsCubit, model.Settings>(builder: (context, _) {
final settings = context.read<SettingsCubit>();
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<SettingsCubit>().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<ThemeMode>(
value: settings.val().appTheme,
icon: const Icon(Icons.arrow_drop_down),
items: ThemeMode.values.map((e) => DropdownMenuItem<ThemeMode>(
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<TimetableNameMode>(
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<TimetableBloc>().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<TimetableBloc>().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),
),
],
),
);
});
}
+1 -1
View File
@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart'; import '../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
import '../../state/app/modules/breaker/bloc/breaker_bloc.dart'; import '../../state/app/modules/breaker/bloc/breaker_bloc.dart';
import '../../widget/placeholderView.dart'; import '../../widget/placeholder_view.dart';
class Breaker extends StatelessWidget { class Breaker extends StatelessWidget {
final BreakerArea breaker; final BreakerArea breaker;
@@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:localstore/localstore.dart'; import 'package:localstore/localstore.dart';
import '../../../widget/placeholderView.dart'; import '../../../widget/placeholder_view.dart';
import '../../api/requestCache.dart'; import '../../api/requestCache.dart';
import 'jsonViewer.dart'; import 'json_viewer.dart';
class CacheView extends StatefulWidget { class CacheView extends StatefulWidget {
const CacheView({super.key}); const CacheView({super.key});
@@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../centeredLeading.dart'; import '../centered_leading.dart';
import 'jsonViewer.dart'; import 'json_viewer.dart';
class DebugTile { class DebugTile {
BuildContext context; BuildContext context;
@@ -8,11 +8,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
import '../routing/app_routes.dart';
import '../state/app/modules/settings/bloc/settings_cubit.dart'; import '../state/app/modules/settings/bloc/settings_cubit.dart';
import '../utils/FileSaver.dart'; import '../utils/file_saver.dart';
import 'infoDialog.dart'; import 'info_dialog.dart';
import 'placeholderView.dart'; import 'placeholder_view.dart';
import 'sharePositionOrigin.dart'; import 'share_position_origin.dart';
class FileViewer extends StatefulWidget { class FileViewer extends StatefulWidget {
final String path; final String path;
@@ -51,9 +52,7 @@ class _FileViewerState extends State<FileViewer> {
onSelected: (value) async { onSelected: (value) async {
switch(value) { switch(value) {
case FileViewingActions.openExternal: case FileViewingActions.openExternal:
Navigator.of(context).push( AppRoutes.openFileViewer(context, widget.path, openExternal: true);
MaterialPageRoute(builder: (context) => FileViewer(path: widget.path, openExternal: true))
);
break; break;
case FileViewingActions.share: case FileViewingActions.share:
SharePlus.instance.share( SharePlus.instance.share(
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import '../model/endpointData.dart'; import '../model/endpoint_data.dart';
class LargeProfilePictureView extends StatelessWidget { class LargeProfilePictureView extends StatelessWidget {
final String username; final String username;

Some files were not shown because too many files have changed in this diff Show More