claude refactor

This commit is contained in:
2026-05-04 13:54:39 +02:00
parent 9973f12733
commit 551c1bf1fa
125 changed files with 4484 additions and 2544 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ pluginManagement {
return flutterSdkPath return flutterSdkPath
} }
settings.ext.flutterSdkPath = flutterSdkPath() settings.ext.flutterSdkPath = flutterSdkPath()
0
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories { repositories {
+1 -1
View File
@@ -5,7 +5,7 @@ import 'getHolidays.dart';
import 'getHolidaysResponse.dart'; import 'getHolidaysResponse.dart';
class GetHolidaysCache extends RequestCache<GetHolidaysResponse> { class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
GetHolidaysCache({onUpdate, renew}) : super(RequestCache.cacheDay, onUpdate, renew: renew) { GetHolidaysCache({void Function(GetHolidaysResponse)? onUpdate, bool? renew}) : super(RequestCache.cacheDay, onUpdate, renew: renew) {
start('state-holidays'); start('state-holidays');
} }
@@ -8,7 +8,7 @@ import 'getChatResponse.dart';
class GetChatCache extends RequestCache<GetChatResponse> { class GetChatCache extends RequestCache<GetChatResponse> {
String chatToken; String chatToken;
GetChatCache({required onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) { GetChatCache({required void Function(GetChatResponse) onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) {
start('nc-chat-$chatToken'); start('nc-chat-$chatToken');
} }
@@ -86,7 +86,7 @@ class GetChatResponseObject {
} }
Map<String, RichObjectString>? _fromJson(json) { Map<String, RichObjectString>? _fromJson(dynamic json) {
if(json is Map<String, dynamic>) { if(json is Map<String, dynamic>) {
var data = <String, RichObjectString>{}; var data = <String, RichObjectString>{};
for (var element in json.keys) { for (var element in json.keys) {
@@ -7,7 +7,7 @@ import 'getParticipantsResponse.dart';
class GetParticipantsCache extends RequestCache<GetParticipantsResponse> { class GetParticipantsCache extends RequestCache<GetParticipantsResponse> {
String chatToken; String chatToken;
GetParticipantsCache({required onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) { GetParticipantsCache({required void Function(GetParticipantsResponse) onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) {
start('nc-chat-participants-$chatToken'); start('nc-chat-participants-$chatToken');
} }
@@ -7,7 +7,7 @@ import 'getRoomParams.dart';
import 'getRoomResponse.dart'; import 'getRoomResponse.dart';
class GetRoomCache extends RequestCache<GetRoomResponse> { class GetRoomCache extends RequestCache<GetRoomResponse> {
GetRoomCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) { GetRoomCache({void Function(GetRoomResponse)? onUpdate, bool? renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) {
start('nc-rooms'); start('nc-rooms');
} }
@@ -9,7 +9,7 @@ import 'listFilesResponse.dart';
class ListFilesCache extends RequestCache<ListFilesResponse> { class ListFilesCache extends RequestCache<ListFilesResponse> {
String path; String path;
ListFilesCache({required onUpdate, required this.path}) : super(RequestCache.cacheNothing, onUpdate) { ListFilesCache({required void Function(ListFilesResponse) onUpdate, required this.path}) : super(RequestCache.cacheNothing, onUpdate) {
var bytes = utf8.encode('MarianumMobile-$path'); var bytes = utf8.encode('MarianumMobile-$path');
var cacheName = md5.convert(bytes).toString(); var cacheName = md5.convert(bytes).toString();
start('wd-folder-$cacheName'); start('wd-folder-$cacheName');
@@ -5,7 +5,7 @@ import 'getBreakersResponse.dart';
class GetBreakersCache extends RequestCache<GetBreakersResponse> { class GetBreakersCache extends RequestCache<GetBreakersResponse> {
GetBreakersCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) { GetBreakersCache({void Function(GetBreakersResponse)? onUpdate, bool? renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) {
start('breakers'); start('breakers');
} }
+11 -12
View File
@@ -13,8 +13,8 @@ abstract class RequestCache<T extends ApiResponse?> {
static String collection = 'MarianumMobile'; static String collection = 'MarianumMobile';
int maxCacheTime; int maxCacheTime;
Function(T) onUpdate; void Function(T)? onUpdate;
Function(Exception) onError; void Function(Exception) onError;
bool? renew; bool? renew;
RequestCache(this.maxCacheTime, this.onUpdate, {this.onError = ignore, this.renew = false}); RequestCache(this.maxCacheTime, this.onUpdate, {this.onError = ignore, this.renew = false});
@@ -22,24 +22,23 @@ abstract class RequestCache<T extends ApiResponse?> {
static void ignore(Exception e) {} static void ignore(Exception e) {}
Future<void> start(String document) async { Future<void> start(String document) async {
var tableData = await Localstore.instance.collection(collection).doc(document).get(); final tableData = await Localstore.instance.collection(collection).doc(document).get();
if(tableData != null) { if (tableData != null) {
onUpdate(onLocalData(tableData['json'])); onUpdate?.call(onLocalData(tableData['json']));
} }
if(DateTime.now().millisecondsSinceEpoch - (maxCacheTime * 1000) < (tableData?['lastupdate'] ?? 0)) { if (DateTime.now().millisecondsSinceEpoch - (maxCacheTime * 1000) < (tableData?['lastupdate'] ?? 0)) {
if(renew == null || !renew!) return; if (renew == null || !renew!) return;
} }
try { try {
var newValue = await onLoad(); final newValue = await onLoad();
onUpdate(newValue); onUpdate?.call(newValue);
Localstore.instance.collection(collection).doc(document).set({ Localstore.instance.collection(collection).doc(document).set({
'json': jsonEncode(newValue), 'json': jsonEncode(newValue),
'lastupdate': DateTime.now().millisecondsSinceEpoch 'lastupdate': DateTime.now().millisecondsSinceEpoch,
}); });
} on Exception catch(e) { } on Exception catch (e) {
onError(e); onError(e);
} }
} }
@@ -5,7 +5,7 @@ import 'getHolidays.dart';
import 'getHolidaysResponse.dart'; import 'getHolidaysResponse.dart';
class GetHolidaysCache extends RequestCache<GetHolidaysResponse> { class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
GetHolidaysCache({onUpdate}) : super(RequestCache.cacheDay, onUpdate) { GetHolidaysCache({void Function(GetHolidaysResponse)? onUpdate}) : super(RequestCache.cacheDay, onUpdate) {
start('wu-holidays'); start('wu-holidays');
} }
@@ -5,7 +5,7 @@ import 'getRooms.dart';
import 'getRoomsResponse.dart'; import 'getRoomsResponse.dart';
class GetRoomsCache extends RequestCache<GetRoomsResponse> { class GetRoomsCache extends RequestCache<GetRoomsResponse> {
GetRoomsCache({onUpdate}) : super(RequestCache.cacheHour, onUpdate) { GetRoomsCache({void Function(GetRoomsResponse)? onUpdate}) : super(RequestCache.cacheHour, onUpdate) {
start('wu-rooms'); start('wu-rooms');
} }
@@ -5,7 +5,7 @@ import 'getSubjects.dart';
import 'getSubjectsResponse.dart'; import 'getSubjectsResponse.dart';
class GetSubjectsCache extends RequestCache<GetSubjectsResponse> { class GetSubjectsCache extends RequestCache<GetSubjectsResponse> {
GetSubjectsCache({onUpdate}) : super(RequestCache.cacheHour, onUpdate) { GetSubjectsCache({void Function(GetSubjectsResponse)? onUpdate}) : super(RequestCache.cacheHour, onUpdate) {
start('wu-subjects'); start('wu-subjects');
} }
@@ -10,7 +10,12 @@ class GetTimetableCache extends RequestCache<GetTimetableResponse> {
int startdate; int startdate;
int enddate; int enddate;
GetTimetableCache({required onUpdate, onError, required this.startdate, required this.enddate}) : super(RequestCache.cacheMinute, onUpdate, onError: onError) { GetTimetableCache({
required void Function(GetTimetableResponse) onUpdate,
void Function(Exception)? onError,
required this.startdate,
required this.enddate,
}) : super(RequestCache.cacheMinute, onUpdate, onError: onError ?? RequestCache.ignore) {
start('wu-timetable-$startdate-$enddate'); start('wu-timetable-$startdate-$enddate');
} }
+77 -74
View File
@@ -1,26 +1,24 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
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 'state/app/modules/app_modules.dart'; import 'package:flutter_bloc/flutter_bloc.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:provider/provider.dart';
import 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart'; 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 'model/breakers/Breaker.dart'; import 'widget/breaker/breaker.dart';
import 'model/breakers/BreakerProps.dart';
import 'model/chatList/chatListProps.dart';
import 'model/dataCleaner.dart'; import 'model/dataCleaner.dart';
import 'model/timetable/timetableProps.dart';
import 'notification/notificationController.dart'; import 'notification/notificationController.dart';
import 'notification/notificationTasks.dart'; import 'notification/notificationTasks.dart';
import 'notification/notifyUpdater.dart'; import 'notification/notifyUpdater.dart';
import 'storage/base/settingsProvider.dart'; import 'state/app/modules/app_modules.dart';
import 'state/app/modules/breaker/bloc/breaker_bloc.dart';
import 'state/app/modules/chatList/bloc/chat_list_bloc.dart';
import 'state/app/modules/settings/bloc/settings_cubit.dart';
import 'view/pages/overhang.dart'; import 'view/pages/overhang.dart';
class App extends StatefulWidget { class App extends StatefulWidget {
@@ -31,101 +29,106 @@ class App extends StatefulWidget {
} }
class _AppState extends State<App> with WidgetsBindingObserver { class _AppState extends State<App> with WidgetsBindingObserver {
late Timer _refetchChats;
late Timer refetchChats; late Timer _updateTimings;
late Timer updateTimings;
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
log('AppLifecycle: ${state.toString()}'); log('AppLifecycle: $state');
if (state == AppLifecycleState.resumed) {
if(state == AppLifecycleState.resumed) { EasyThrottle.throttle('appLifecycleState', const Duration(seconds: 10), () {
EasyThrottle.throttle( if (!mounted) return;
'appLifecycleState', log('Refreshing due to LifecycleChange');
const Duration(seconds: 10), NotificationTasks.updateProviders(context);
() { });
log('Refreshing due to LifecycleChange');
NotificationTasks.updateProviders(context);
Provider.of<TimetableProps>(context, listen: false).run();
}
);
} }
} }
@override @override
void initState() { void initState() {
super.initState();
Main.bottomNavigator = PersistentTabController(initialIndex: 0); Main.bottomNavigator = PersistentTabController(initialIndex: 0);
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Provider.of<BreakerProps>(context, listen: false).run(); WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<ChatListProps>(context, listen: false).run(); if (!mounted) return;
context.read<BreakerBloc>().refresh();
context.read<ChatListBloc>().refresh();
}); });
updateTimings = Timer.periodic(const Duration(seconds: 30), (Timer t) => setState((){})); _updateTimings = Timer.periodic(const Duration(seconds: 30), (_) {
if (mounted) setState(() {});
});
refetchChats = Timer.periodic(const Duration(seconds: 60), (timer) { _refetchChats = Timer.periodic(const Duration(seconds: 60), (_) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<ChatListProps>(context, listen: false).run(); if (!mounted) return;
context.read<ChatListBloc>().refresh();
}); });
}); });
// User index
UpdateUserIndex.index(); UpdateUserIndex.index();
// User Notifications if (context.read<SettingsCubit>().val().notificationSettings.enabled) {
if(Provider.of<SettingsProvider>(context, listen: false).val().notificationSettings.enabled) { void update() => NotifyUpdater.registerToServer();
update() => NotifyUpdater.registerToServer(); FirebaseMessaging.instance.onTokenRefresh.listen((_) => update());
FirebaseMessaging.instance.onTokenRefresh.listen((event) => update());
update(); update();
} }
FirebaseMessaging.onMessage.listen((message) => NotificationController.onForegroundMessageHandler(message, context)); FirebaseMessaging.onMessage.listen((message) {
if (!mounted) return;
NotificationController.onForegroundMessageHandler(message, context);
});
FirebaseMessaging.onBackgroundMessage(NotificationController.onBackgroundMessageHandler); FirebaseMessaging.onBackgroundMessage(NotificationController.onBackgroundMessageHandler);
FirebaseMessaging.onMessageOpenedApp.listen((message) => NotificationController.onAppOpenedByNotification(message, context)); FirebaseMessaging.onMessageOpenedApp.listen((message) {
FirebaseMessaging.instance.getInitialMessage().then((message) => message == null ? null : NotificationController.onAppOpenedByNotification(message, context)); if (!mounted) return;
NotificationController.onAppOpenedByNotification(message, context);
});
FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message == null || !mounted) return;
NotificationController.onAppOpenedByNotification(message, context);
});
DataCleaner.cleanOldCache(); DataCleaner.cleanOldCache();
super.initState();
} }
@override
Widget build(BuildContext context) => Consumer<SettingsProvider>(builder: (context, settings, child) => PersistentTabView(
controller: Main.bottomNavigator,
navBarOverlap: const NavBarOverlap.none(),
backgroundColor: Theme.of(context).colorScheme.primary,
handleAndroidBackButtonPress: false,
screenTransitionAnimation: const ScreenTransitionAnimation(curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200)),
tabs: [
...AppModule.getBottomBarModules(context).map((e) => e.toBottomTab(context)),
PersistentTabConfig(
screen: const Breaker(breaker: BreakerArea.more, child: Overhang()),
item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.apps),
title: 'Mehr'
),
),
],
navBarBuilder: (config) => Style6BottomNavBar(
navBarConfig: config,
navBarDecoration: NavBarDecoration(
border: const Border(top: BorderSide(width: 1, color: Colors.grey)),
color: Theme.of(context).colorScheme.surface,
),
),
));
@override @override
void dispose() { void dispose() {
refetchChats.cancel(); _refetchChats.cancel();
updateTimings.cancel(); _updateTimings.cancel();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@override
Widget build(BuildContext context) => PersistentTabView(
controller: Main.bottomNavigator,
navBarOverlap: const NavBarOverlap.none(),
backgroundColor: Theme.of(context).colorScheme.primary,
handleAndroidBackButtonPress: false,
screenTransitionAnimation: const ScreenTransitionAnimation(
curve: Curves.easeOutQuad,
duration: Duration(milliseconds: 200),
),
tabs: [
...AppModule.getBottomBarModules(context).map((e) => e.toBottomTab(context)),
PersistentTabConfig(
screen: const Breaker(breaker: BreakerArea.more, child: Overhang()),
item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.apps),
title: 'Mehr',
),
),
],
navBarBuilder: (config) => Style6BottomNavBar(
navBarConfig: config,
navBarDecoration: NavBarDecoration(
border: const Border(top: BorderSide(width: 1, color: Colors.grey)),
color: Theme.of(context).colorScheme.surface,
),
),
);
} }
+87 -96
View File
@@ -1,33 +1,34 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'dart:ui';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:loader_overlay/loader_overlay.dart'; import 'package:loader_overlay/loader_overlay.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.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:provider/provider.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/accountData.dart';
import 'model/accountModel.dart'; import 'widget/breaker/breaker.dart';
import 'model/breakers/Breaker.dart'; import 'state/app/modules/account/bloc/account_bloc.dart';
import 'model/breakers/BreakerProps.dart'; import 'state/app/modules/account/bloc/account_state.dart';
import 'model/chatList/chatListProps.dart'; import 'state/app/modules/breaker/bloc/breaker_bloc.dart';
import 'model/chatList/chatProps.dart'; import 'state/app/modules/chat/bloc/chat_bloc.dart';
import 'model/files/filesProps.dart'; import 'state/app/modules/chatList/bloc/chat_list_bloc.dart';
import 'model/holidays/holidaysProps.dart'; import 'state/app/modules/settings/bloc/settings_cubit.dart';
import 'model/timetable/timetableProps.dart'; import 'state/app/modules/timetable/bloc/timetable_bloc.dart';
import 'storage/base/settingsProvider.dart'; import 'storage/base/settings.dart';
import 'theming/darkAppTheme.dart'; import 'theming/darkAppTheme.dart';
import 'theming/lightAppTheme.dart'; import 'theming/lightAppTheme.dart';
import 'view/login/login.dart'; import 'view/login/login.dart';
@@ -37,133 +38,123 @@ Future<void> main() async {
log('MarianumMobile started'); log('MarianumMobile started');
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
addCertificateAsTrusted(ByteData certificate) => SecurityContext.defaultContext.setTrustedCertificatesBytes(certificate.buffer.asUint8List()); void addCertificateAsTrusted(ByteData certificate) =>
SecurityContext.defaultContext.setTrustedCertificatesBytes(certificate.buffer.asUint8List());
var initialisationTasks = [ final initialisationTasks = [
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform) Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform)
.then((value) async => log("Firebase token: ${await FirebaseMessaging.instance.getToken() ?? "Error: no Firebase token!"}")) .then((_) async => log('Firebase token: ${await FirebaseMessaging.instance.getToken() ?? "Error: no Firebase token!"}'))
.onError((error, stackTrace) => log('Error initializing Firebase: $error')), .onError((error, _) => log('Error initializing Firebase: $error')),
PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem').then(addCertificateAsTrusted), PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem').then(addCertificateAsTrusted),
PlatformAssetBundle().load('assets/ca/lets-encrypt-r10.pem').then(addCertificateAsTrusted), PlatformAssetBundle().load('assets/ca/lets-encrypt-r10.pem').then(addCertificateAsTrusted),
PlatformAssetBundle().load('assets/ca/lets-encrypt-r13.pem').then(addCertificateAsTrusted), PlatformAssetBundle().load('assets/ca/lets-encrypt-r13.pem').then(addCertificateAsTrusted),
Future(() async { Future(() async {
await HydratedStorage.build( final storage = await HydratedStorage.build(
storageDirectory: HydratedStorageDirectory((await getTemporaryDirectory()).path) storageDirectory: HydratedStorageDirectory((await getTemporaryDirectory()).path),
).then((storage) => HydratedBloc.storage = storage); );
}) HydratedBloc.storage = storage;
}),
]; ];
log('starting app initialisation...'); log('starting app initialisation...');
await Future.wait(initialisationTasks); await Future.wait(initialisationTasks);
log('app initialisation done!'); log('app initialisation done!');
if(kReleaseMode) { if (kReleaseMode) {
ErrorWidget.builder = (error) => PlaceholderView( ErrorWidget.builder = (error) => PlaceholderView(
icon: Icons.phonelink_erase_rounded, icon: Icons.phonelink_erase_rounded,
text: error.toStringShort(), text: error.toStringShort(),
); );
} }
// Capture uncaught Flutter and platform errors so they show up in logs
// instead of being silently swallowed.
FlutterError.onError = (details) {
log('Uncaught Flutter error: ${details.exception}', stackTrace: details.stack);
FlutterError.presentError(details);
};
PlatformDispatcher.instance.onError = (error, stack) {
log('Uncaught platform error: $error', stackTrace: stack);
return true;
};
log('running app...'); log('running app...');
runApp( runApp(
MultiProvider( MultiBlocProvider(
providers: [ providers: [
ChangeNotifierProvider(create: (context) => BreakerProps()), BlocProvider<SettingsCubit>(create: (_) => SettingsCubit()),
BlocProvider<AccountBloc>(create: (_) => AccountBloc()),
ChangeNotifierProvider(create: (context) => SettingsProvider()), BlocProvider<BreakerBloc>(create: (_) => BreakerBloc()),
ChangeNotifierProvider(create: (context) => AccountModel()), BlocProvider<ChatListBloc>(create: (_) => ChatListBloc()),
BlocProvider<ChatBloc>(create: (_) => ChatBloc()),
ChangeNotifierProvider(create: (context) => TimetableProps()), BlocProvider<TimetableBloc>(create: (_) => TimetableBloc()),
ChangeNotifierProvider(create: (context) => ChatListProps()), ],
ChangeNotifierProvider(create: (context) => ChatProps()), child: const Main(),
ChangeNotifierProvider(create: (context) => FilesProps()), ),
ChangeNotifierProvider(create: (context) => HolidaysProps()),
],
child: const Main(),
)
); );
} }
class Main extends StatefulWidget { class Main extends StatefulWidget {
const Main({super.key}); const Main({super.key});
static PersistentTabController bottomNavigator = PersistentTabController(initialIndex: 0);
static PersistentTabController bottomNavigator = PersistentTabController(initialIndex: 0);
@override @override
State<Main> createState() => _MainState(); State<Main> createState() => _MainState();
} }
class _MainState extends State<Main> { class _MainState extends State<Main> {
late Timer refetchProps;
@override @override
void initState() { void initState() {
super.initState();
Jiffy.setLocale('de'); Jiffy.setLocale('de');
AccountData().waitForPopulation().then((value) { AccountData().waitForPopulation().then((value) {
Provider.of<AccountModel>(context, listen: false) if (!mounted) return;
.setState(value ? AccountModelState.loggedIn : AccountModelState.loggedOut); context.read<AccountBloc>().setStatus(value ? AccountStatus.loggedIn : AccountStatus.loggedOut);
}); });
refetchProps = Timer.periodic(const Duration(seconds: 60), (timer) {
Provider.of<BreakerProps>(context, listen: false).run();
});
super.initState();
} }
@override @override
Widget build(BuildContext context) => Directionality( Widget build(BuildContext context) => Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Consumer<SettingsProvider>( child: BlocBuilder<SettingsCubit, Settings>(
builder: (context, settings, child) { builder: (context, settings) {
var devToolsSettings = settings.val().devToolsSettings; final devToolsSettings = settings.devToolsSettings;
return MaterialApp( return MaterialApp(
showPerformanceOverlay: devToolsSettings.showPerformanceOverlay, showPerformanceOverlay: devToolsSettings.showPerformanceOverlay,
checkerboardOffscreenLayers: devToolsSettings.checkerboardOffscreenLayers, checkerboardOffscreenLayers: devToolsSettings.checkerboardOffscreenLayers,
checkerboardRasterCacheImages: devToolsSettings.checkerboardRasterCacheImages, checkerboardRasterCacheImages: devToolsSettings.checkerboardRasterCacheImages,
debugShowCheckedModeBanner: false,
debugShowCheckedModeBanner: false, localizationsDelegates: const [
localizationsDelegates: const [ ...GlobalMaterialLocalizations.delegates,
...GlobalMaterialLocalizations.delegates, GlobalWidgetsLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, ],
], supportedLocales: const [Locale('de'), Locale('en')],
supportedLocales: const [ locale: const Locale('de'),
Locale('de'), title: 'Marianum Fulda',
Locale('en'), themeMode: settings.appTheme,
], theme: LightAppTheme.theme,
locale: const Locale('de'), darkTheme: DarkAppTheme.theme,
home: LoaderOverlay(
title: 'Marianum Fulda', child: Breaker(
themeMode: settings.val().appTheme,
theme: LightAppTheme.theme,
darkTheme: DarkAppTheme.theme,
home: LoaderOverlay(
child: Breaker(
breaker: BreakerArea.global, breaker: BreakerArea.global,
child: Consumer<AccountModel>( child: BlocBuilder<AccountBloc, AccountState>(
builder: (context, accountModel, child) { builder: (context, accountState) {
switch(accountModel.state) { switch (accountState.status) {
case AccountModelState.loggedIn: return const App(); case AccountStatus.loggedIn:
case AccountModelState.loggedOut: return const Login(); return const App();
case AccountModelState.undefined: return const PlaceholderView(icon: Icons.timer, text: 'Daten werden geladen'); case AccountStatus.loggedOut:
return const Login();
case AccountStatus.undefined:
return const PlaceholderView(icon: Icons.timer, text: 'Daten werden geladen');
} }
}, },
) ),
),
), ),
), );
); },
}, ),
), );
);
@override
void dispose() {
refetchProps.cancel();
super.dispose();
}
} }
+51 -28
View File
@@ -4,68 +4,91 @@ import 'dart:convert';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'accountModel.dart'; import '../state/app/modules/account/bloc/account_bloc.dart';
import '../state/app/modules/account/bloc/account_state.dart';
class AccountData { class AccountData {
static const _usernameField = 'username'; static const _usernameField = 'username';
static const _passwordField = 'password'; static const _passwordField = 'password';
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
static final AccountData _instance = AccountData._construct(); static final AccountData _instance = AccountData._construct();
final Future<SharedPreferences> _storage = SharedPreferences.getInstance();
Completer<void> _populated = Completer(); Completer<void> _populated = Completer();
factory AccountData() => _instance; factory AccountData() => _instance;
AccountData._construct() { AccountData._construct() {
_updateFromStorage(); _migrateAndLoad();
} }
String? _username; String? _username;
String? _password; String? _password;
String getUsername() { String getUsername() {
if(_username == null) throw Exception('Username not initialized'); if (_username == null) throw Exception('Username not initialized');
return _username!; return _username!;
} }
String getPassword() { String getPassword() {
if(_password == null) throw Exception('Password not initialized'); if (_password == null) throw Exception('Password not initialized');
return _password!; return _password!;
} }
String getUserSecret() => sha512.convert(utf8.encode('${AccountData().getUsername()}:${AccountData().getPassword()}')).toString(); String getUserSecret() =>
sha512.convert(utf8.encode('${getUsername()}:${getPassword()}')).toString();
Future<String> getDeviceId() async => sha512.convert(utf8.encode('${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}')).toString(); Future<String> getDeviceId() async => sha512
.convert(utf8.encode('${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}'))
.toString();
Future<void> setData(String username, String password) async { Future<void> setData(String username, String password) async {
var storage = await _storage; await _secureStorage.write(key: _usernameField, value: username);
await _secureStorage.write(key: _passwordField, value: password);
storage.setString(_usernameField, username); _username = username;
storage.setString(_passwordField, password); _password = password;
await _updateFromStorage(); if (!_populated.isCompleted) _populated.complete();
} }
Future<void> removeData({BuildContext? context}) async { Future<void> removeData({BuildContext? context}) async {
_populated = Completer(); _populated = Completer();
if (context != null) {
if(context != null) Provider.of<AccountModel>(context, listen: false).setState(AccountModelState.loggedOut); context.read<AccountBloc>().setStatus(AccountStatus.loggedOut);
}
var storage = await _storage; _username = null;
await storage.remove(_usernameField); _password = null;
await storage.remove(_passwordField); await _secureStorage.delete(key: _usernameField);
await _secureStorage.delete(key: _passwordField);
} }
Future<void> _updateFromStorage() async { Future<void> _migrateAndLoad() async {
var storage = await _storage; await _migrateFromLegacyStorage();
//await storage.reload(); // This line was the cause of the first rejected google play upload :( _username = await _secureStorage.read(key: _usernameField);
if(storage.containsKey(_usernameField) && storage.containsKey(_passwordField)) { _password = await _secureStorage.read(key: _passwordField);
_username = storage.getString(_usernameField); if (!_populated.isCompleted) _populated.complete();
_password = storage.getString(_passwordField); }
// Move credentials from the old SharedPreferences plain-text storage into the
// platform's secure keystore. Run once per install and clear the legacy keys.
Future<void> _migrateFromLegacyStorage() async {
final prefs = await SharedPreferences.getInstance();
final legacyUsername = prefs.getString(_usernameField);
final legacyPassword = prefs.getString(_passwordField);
if (legacyUsername == null || legacyPassword == null) return;
final hasSecure = (await _secureStorage.read(key: _usernameField)) != null;
if (!hasSecure) {
await _secureStorage.write(key: _usernameField, value: legacyUsername);
await _secureStorage.write(key: _passwordField, value: legacyPassword);
} }
if(!_populated.isCompleted) _populated.complete(); await prefs.remove(_usernameField);
await prefs.remove(_passwordField);
} }
Future<bool> waitForPopulation() async { Future<bool> waitForPopulation() async {
@@ -76,7 +99,7 @@ class AccountData {
bool isPopulated() => _username != null && _password != null; bool isPopulated() => _username != null && _password != null;
String buildHttpAuthString() { String buildHttpAuthString() {
if(!isPopulated()) throw Exception('AccountData (e.g. username or password) is not initialized!'); if (!isPopulated()) throw Exception('AccountData (e.g. username or password) is not initialized!');
return '$_username:$_password'; return '$_username:$_password';
} }
} }
-17
View File
@@ -1,17 +0,0 @@
import 'package:flutter/cupertino.dart';
class AccountModel extends ChangeNotifier {
AccountModelState _accountState = AccountModelState.undefined;
AccountModelState get state => _accountState;
void setState(AccountModelState state) {
_accountState = state;
notifyListeners();
}
}
enum AccountModelState {
undefined,
loggedIn,
loggedOut,
}
-36
View File
@@ -1,36 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
import '../../widget/placeholderView.dart';
import 'BreakerProps.dart';
class Breaker extends StatefulWidget {
final BreakerArea breaker;
final Widget child;
const Breaker({required this.breaker, required this.child, super.key});
@override
State<Breaker> createState() => _BreakerState();
}
class _BreakerState extends State<Breaker> {
@override
Widget build(BuildContext context) => Consumer<BreakerProps>(
builder: (context, value, child) {
var blocked = value.isBlocked(widget.breaker);
if(blocked != null) {
return PlaceholderView(
icon: Icons.app_blocking_outlined,
text: 'Die App / Dieser Bereich ist zurzeit nicht verfügbar!\n\n'
"${blocked.isEmpty ? "Es wurde vom Server kein Grund übermittelt.\nAktualisiere die App und versuche es später erneut" : blocked}"
);
}
return widget.child;
},
);
}
-52
View File
@@ -1,52 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../../api/apiResponse.dart';
import '../../api/mhsl/breaker/getBreakers/getBreakersCache.dart';
import '../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
import '../dataHolder.dart';
class BreakerProps extends DataHolder {
GetBreakersResponse? _getBreakersResponse;
GetBreakersResponse get getBreakersResponse => _getBreakersResponse!;
PackageInfo? packageInfo;
String? isBlocked(BreakerArea? type) {
if(kDebugMode) return null;
if(packageInfo == null) {
PackageInfo.fromPlatform().then((value) => packageInfo = value);
return null;
}
if(primaryLoading()) return null;
var breakers = _getBreakersResponse!;
if(breakers.global.areas.contains(type)) return breakers.global.message;
var selfVersion = int.parse(packageInfo!.buildNumber);
for(var key in breakers.regional.keys) {
var value = breakers.regional[key]!;
if(int.parse(key.split('b')[1]) >= selfVersion) {
if(value.areas.contains(type)) return value.message;
}
}
return null;
}
@override
List<ApiResponse?> properties() => [_getBreakersResponse];
@override
void run() {
GetBreakersCache(
onUpdate: (GetBreakersResponse getBreakersResponse) {
_getBreakersResponse = getBreakersResponse;
notifyListeners();
}
);
}
}
-28
View File
@@ -1,28 +0,0 @@
import 'package:flutter_app_badge/flutter_app_badge.dart';
import '../../api/apiResponse.dart';
import '../../api/marianumcloud/talk/room/getRoomCache.dart';
import '../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../dataHolder.dart';
class ChatListProps extends DataHolder {
GetRoomResponse? _getRoomResponse;
GetRoomResponse get getRoomsResponse => _getRoomResponse!;
@override
List<ApiResponse?> properties() => [_getRoomResponse];
@override
void run({renew}) {
GetRoomCache(
renew: renew,
onUpdate: (GetRoomResponse data) => {
_getRoomResponse = data,
notifyListeners(),
FlutterAppBadge.count(data.data.map((e) => e.unreadMessages).reduce((a, b) => a+b))
}
);
}
}
-63
View File
@@ -1,63 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import '../../api/apiResponse.dart';
import '../../api/marianumcloud/talk/chat/getChatCache.dart';
import '../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../storage/base/settingsProvider.dart';
import '../dataHolder.dart';
class ChatProps extends DataHolder {
String _queryToken = '';
DateTime _lastTokenSet = DateTime.now();
int? _referenceMessageId;
GetChatResponse? _getChatResponse;
GetChatResponse get getChatResponse => _getChatResponse!;
int? get getReferenceMessageId => _referenceMessageId;
set unsafeInternalSetReferenceMessageId(int? reference) => _referenceMessageId = reference;
@override
List<ApiResponse?> properties() => [_getChatResponse];
@override
void run() {
notifyListeners();
if(_queryToken.isEmpty) return;
var requestStart = DateTime.now();
GetChatCache(
chatToken: _queryToken,
onUpdate: (GetChatResponse data) {
if(_lastTokenSet.isAfter(requestStart)) return; // Another request was faster
_getChatResponse = data;
notifyListeners();
}
);
}
void setReferenceMessageId(int? messageId, BuildContext context, String sendToToken) {
Future.microtask(() {
_referenceMessageId = messageId;
notifyListeners();
var settings = Provider.of<SettingsProvider>(context, listen: false);
if(messageId != null) {
settings.val(write: true).talkSettings.draftReplies[sendToToken] = messageId;
} else {
settings.val(write: true).talkSettings.draftReplies.removeWhere((key, value) => key == sendToToken);
}
});
}
void setQueryToken(String token) {
_queryToken = token;
_getChatResponse = null;
_lastTokenSet = DateTime.now();
run();
}
String currentToken() => _queryToken;
}
-22
View File
@@ -1,22 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:localstore/localstore.dart';
import '../api/apiResponse.dart';
abstract class DataHolder extends ChangeNotifier {
CollectionRef storage(String path) => Localstore.instance.collection(path);
void run();
List<ApiResponse?> properties();
bool primaryLoading() {
// log("${toString()} ${properties().map((e) => e != null ? "1" : "0").join(", ")}");
for(var element in properties()) {
if(element == null) return true;
}
return false;
//return properties().where((element) => element != null).isEmpty;
}
}
-52
View File
@@ -1,52 +0,0 @@
import '../../api/apiResponse.dart';
import '../../api/marianumcloud/webdav/queries/listFiles/listFilesCache.dart';
import '../../api/marianumcloud/webdav/queries/listFiles/listFilesResponse.dart';
import '../dataHolder.dart';
extension ExtendedList on List {
T indexOrNull<T>(int index) => index +1 <= length ? this[index] : null;
T firstOrNull<T>() => isEmpty ? null : first;
T lastOrNull<T>() => isEmpty ? null : last;
}
class FilesProps extends DataHolder {
List<String> folderPath = List<String>.empty(growable: true);
String currentFolderName = 'Home';
ListFilesResponse? _listFilesResponse;
ListFilesResponse get listFilesResponse => _listFilesResponse!;
void runPath(List<String> path) {
folderPath = path;
run();
}
@override
List<ApiResponse?> properties() => [_listFilesResponse];
@override
void run() {
_listFilesResponse = null;
notifyListeners();
ListFilesCache(
path: folderPath.isEmpty ? '/' : folderPath.join('/'),
onUpdate: (ListFilesResponse data) => {
_listFilesResponse = data,
notifyListeners(),
}
);
}
void enterFolder(String name) {
folderPath.add(name);
currentFolderName = name;
run();
}
void popFolder() {
folderPath.removeLast();
if(folderPath.isEmpty) currentFolderName = 'Home';
run();
}
}
-24
View File
@@ -1,24 +0,0 @@
import '../../api/apiResponse.dart';
import '../../api/holidays/getHolidaysCache.dart';
import '../../api/holidays/getHolidaysResponse.dart';
import '../dataHolder.dart';
class HolidaysProps extends DataHolder {
GetHolidaysResponse? _getHolidaysResponse;
GetHolidaysResponse get getHolidaysResponse => _getHolidaysResponse!;
@override
List<ApiResponse?> properties() => [_getHolidaysResponse];
@override
void run() {
GetHolidaysCache(
onUpdate: (GetHolidaysResponse data) => {
_getHolidaysResponse = data,
notifyListeners(),
},
);
}
}
-130
View File
@@ -1,130 +0,0 @@
import 'package:intl/intl.dart';
import '../../api/apiResponse.dart';
import '../../api/mhsl/customTimetableEvent/get/getCustomTimetableEventCache.dart';
import '../../api/mhsl/customTimetableEvent/get/getCustomTimetableEventParams.dart';
import '../../api/mhsl/customTimetableEvent/get/getCustomTimetableEventResponse.dart';
import '../../api/webuntis/queries/getHolidays/getHolidaysCache.dart';
import '../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart';
import '../../api/webuntis/queries/getRooms/getRoomsCache.dart';
import '../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
import '../../api/webuntis/queries/getSubjects/getSubjectsCache.dart';
import '../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart';
import '../../api/webuntis/queries/getTimetable/getTimetableCache.dart';
import '../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
import '../../api/webuntis/webuntisError.dart';
import '../accountData.dart';
import '../dataHolder.dart';
class TimetableProps extends DataHolder {
final _queryWeek = DateTime.now().add(const Duration(days: 2));
late DateTime startDate = getDate(_queryWeek.subtract(Duration(days: _queryWeek.weekday - 1)));
late DateTime endDate = getDate(_queryWeek.add(Duration(days: DateTime.daysPerWeek - _queryWeek.weekday - 2)));
GetTimetableResponse? _getTimetableResponse;
GetTimetableResponse get getTimetableResponse => _getTimetableResponse!;
GetRoomsResponse? _getRoomsResponse;
GetRoomsResponse get getRoomsResponse => _getRoomsResponse!;
GetSubjectsResponse? _getSubjectsResponse;
GetSubjectsResponse get getSubjectsResponse => _getSubjectsResponse!;
GetHolidaysResponse? _getHolidaysResponse;
GetHolidaysResponse get getHolidaysResponse => _getHolidaysResponse!;
GetCustomTimetableEventResponse? _getCustomTimetableEventResponse;
GetCustomTimetableEventResponse get getCustomTimetableEventResponse => _getCustomTimetableEventResponse!;
WebuntisError? error;
WebuntisError? get getError => error;
bool get hasError => error != null;
@override
List<ApiResponse?> properties() => [_getTimetableResponse, _getRoomsResponse, _getSubjectsResponse, _getHolidaysResponse, _getCustomTimetableEventResponse];
@override
void run({renew}) {
GetTimetableCache(
startdate: int.parse(DateFormat('yyyyMMdd').format(startDate)),
enddate: int.parse(DateFormat('yyyyMMdd').format(endDate)),
onUpdate: (GetTimetableResponse data) => {
_getTimetableResponse = data,
notifyListeners(),
},
onError: (Exception e) => {
error = e as WebuntisError?,
notifyListeners(),
}
);
GetRoomsCache(
onUpdate: (GetRoomsResponse data) => {
_getRoomsResponse = data,
notifyListeners(),
}
);
GetSubjectsCache(
onUpdate: (GetSubjectsResponse data) => {
_getSubjectsResponse = data,
notifyListeners(),
}
);
GetHolidaysCache( // This broke in the past. Below here is backup simulation for an empty holiday block
onUpdate: (GetHolidaysResponse data) => {
_getHolidaysResponse = data,
notifyListeners(),
}
);
// _getHolidaysResponse = GetHolidaysResponse.fromJson(jsonDecode("""
// {"jsonrpc":"2.0","id":"ID","result":[]}
// """));
GetCustomTimetableEventCache(
renew: renew,
GetCustomTimetableEventParams(
AccountData().getUserSecret()
),
onUpdate: (GetCustomTimetableEventResponse data) => {
_getCustomTimetableEventResponse = data,
notifyListeners(),
}
);
notifyListeners();
}
DateTime getDate(DateTime d) => DateTime(d.year, d.month, d.day);
bool isWeekend(DateTime queryDate) => queryDate.weekday == DateTime.saturday || queryDate.weekday == DateTime.sunday;
void updateWeek(DateTime start, DateTime end) {
properties().forEach((element) => element = null);
error = null;
notifyListeners();
startDate = start;
endDate = end;
try {
run();
} on WebuntisError catch(e) {
error = e;
notifyListeners();
}
}
void resetWeek() {
error = null;
notifyListeners();
var queryWeek = DateTime.now().add(const Duration(days: 2));
startDate = getDate(queryWeek.subtract(Duration(days: queryWeek.weekday - 1)));
endDate = getDate(queryWeek.add(Duration(days: DateTime.daysPerWeek - queryWeek.weekday)));
run();
notifyListeners();
}
}
+8 -8
View File
@@ -1,26 +1,26 @@
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/cupertino.dart'; 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:provider/provider.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../main.dart'; import '../main.dart';
import '../model/chatList/chatListProps.dart';
import '../model/chatList/chatProps.dart';
import '../state/app/modules/app_modules.dart'; import '../state/app/modules/app_modules.dart';
import '../state/app/modules/chat/bloc/chat_bloc.dart';
import '../state/app/modules/chatList/bloc/chat_list_bloc.dart';
class NotificationTasks { class NotificationTasks {
static void updateBadgeCount(RemoteMessage notification) { static void updateBadgeCount(RemoteMessage notification) {
FlutterAppBadge.count(int.parse(notification.data['unreadCount'] ?? 0)); FlutterAppBadge.count(int.parse(notification.data['unreadCount'] ?? '0'));
} }
static void updateProviders(BuildContext context) { static void updateProviders(BuildContext context) {
Provider.of<ChatListProps>(context, listen: false).run(renew: true); context.read<ChatListBloc>().refresh();
Provider.of<ChatProps>(context, listen: false).run(); context.read<ChatBloc>().refresh();
} }
static void navigateToTalk(BuildContext context) { static void navigateToTalk(BuildContext context) {
var talkTab = AppModule.getBottomBarModules(context).map((e) => e.module).toList().indexOf(Modules.talk); final talkTab = AppModule.getBottomBarModules(context).map((e) => e.module).toList().indexOf(Modules.talk);
if(talkTab == -1) return; if (talkTab == -1) return;
Main.bottomNavigator.jumpToTab(talkTab); Main.bottomNavigator.jumpToTab(talkTab);
} }
} }
+20 -21
View File
@@ -1,34 +1,33 @@
import 'package:flutter/material.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
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/accountData.dart';
import '../storage/base/settingsProvider.dart'; import '../state/app/modules/settings/bloc/settings_cubit.dart';
import '../widget/confirmDialog.dart'; import '../widget/confirmDialog.dart';
class NotifyUpdater { class NotifyUpdater {
static ConfirmDialog enableAfterDisclaimer(SettingsProvider settings) => ConfirmDialog( static ConfirmDialog enableAfterDisclaimer(SettingsCubit settings) => ConfirmDialog(
title: 'Warnung', title: 'Warnung',
icon: Icons.warning_amber, icon: Icons.warning_amber,
content: '' content: ''
'Die Push-Benachrichtigungen werden durch mhsl.eu versendet.\n\n' 'Die Push-Benachrichtigungen werden durch mhsl.eu versendet.\n\n'
'Durch das aktivieren dieser Funktion wird dein Nutzername, dein Password und eine Geräte-ID von mhsl dauerhaft gespeichert und verarbeitet.\n\n' 'Durch das aktivieren dieser Funktion wird dein Nutzername, dein Password und eine Geräte-ID von mhsl dauerhaft gespeichert und verarbeitet.\n\n'
'Für mehr Informationen drücke lange auf die Einstellungsoption!', 'Für mehr Informationen drücke lange auf die Einstellungsoption!',
confirmButton: 'Aktivieren', confirmButton: 'Aktivieren',
onConfirm: () { onConfirm: () {
FirebaseMessaging.instance.requestPermission( FirebaseMessaging.instance.requestPermission(provisional: false);
provisional: false settings.val(write: true).notificationSettings.enabled = true;
); NotifyUpdater.registerToServer();
settings.val(write: true).notificationSettings.enabled = true; },
NotifyUpdater.registerToServer(); );
},
);
static Future<void> registerToServer() async { static Future<void> registerToServer() async {
var fcmToken = await FirebaseMessaging.instance.getToken(); final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken == null) {
if(fcmToken == null) throw Exception('Failed to register push notification because there is no FBC token!'); throw Exception('Failed to register push notification because there is no FBC token!');
}
NotifyRegister( NotifyRegister(
NotifyRegisterParams( NotifyRegisterParams(
@@ -25,8 +25,9 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
Widget build(BuildContext context) { Widget build(BuildContext context) {
var loadableState = context.watch<TController>().state; var loadableState = context.watch<TController>().state;
if(!loadableState.isLoading && onLoad != null) { final loadedData = loadableState.data;
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => onLoad!(loadableState.data!)); if(!loadableState.isLoading && onLoad != null && loadedData is TState) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => onLoad!(loadedData));
} }
var childWidget = ConditionalWrapper( var childWidget = ConditionalWrapper(
@@ -47,8 +48,8 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
), ),
child: SizedBox( child: SizedBox(
height: MediaQuery.of(context).size.height, height: MediaQuery.of(context).size.height,
child: loadableState.showContent() child: loadableState.showContent() && loadedData is TState
? child(loadableState.data!, loadableState.isLoading) ? child(loadedData, loadableState.isLoading)
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
); );
@@ -14,8 +14,8 @@ class BlocModule<TBloc extends StateStreamableSource<TState>, TState> extends St
@override @override
Widget build(BuildContext context) => BlocProvider<TBloc>( Widget build(BuildContext context) => BlocProvider<TBloc>(
create: (context) { create: (context) {
var bloc = create(context); final bloc = create(context);
this.onInitialisation != null ? this.onInitialisation!(context, bloc) : null; onInitialisation?.call(context, bloc);
return bloc; return bloc;
}, },
child: Builder( child: Builder(
@@ -103,7 +103,8 @@ abstract class LoadableHydratedBloc<
Map<String, dynamic>? toJson(LoadableState<TState> state) { Map<String, dynamic>? toJson(LoadableState<TState> state) {
Map<String, dynamic>? data; Map<String, dynamic>? data;
try { try {
data = state.data == null ? null : toStorage(state.data!); final stateData = state.data;
data = stateData is TState ? toStorage(stateData) : null;
} catch(e) { } catch(e) {
log('Failed to save state ${TState.toString()}: ${e.toString()}'); log('Failed to save state ${TState.toString()}: ${e.toString()}');
} }
@@ -0,0 +1,12 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'account_event.dart';
import 'account_state.dart';
class AccountBloc extends Bloc<AccountEvent, AccountState> {
AccountBloc() : super(const AccountState()) {
on<AccountStatusChanged>((event, emit) => emit(state.copyWith(status: event.status)));
}
void setStatus(AccountStatus status) => add(AccountStatusChanged(status));
}
@@ -0,0 +1,10 @@
import 'account_state.dart';
sealed class AccountEvent {
const AccountEvent();
}
class AccountStatusChanged extends AccountEvent {
final AccountStatus status;
const AccountStatusChanged(this.status);
}
@@ -0,0 +1,8 @@
enum AccountStatus { undefined, loggedIn, loggedOut }
class AccountState {
final AccountStatus status;
const AccountState({this.status = AccountStatus.undefined});
AccountState copyWith({AccountStatus? status}) => AccountState(status: status ?? this.status);
}
+13 -10
View File
@@ -1,16 +1,18 @@
import 'package:flutter/material.dart'; 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:provider/provider.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart'; import '../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
import '../../../model/breakers/Breaker.dart'; import '../../../widget/breaker/breaker.dart';
import '../../../model/chatList/chatListProps.dart';
import '../../../storage/base/settingsProvider.dart';
import '../../../view/pages/files/files.dart'; import '../../../view/pages/files/files.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/chatList.dart';
import '../../../view/pages/timetable/timetable.dart'; import '../../../view/pages/timetable/timetable.dart';
import '../../../widget/centeredLeading.dart'; import '../../../widget/centeredLeading.dart';
import 'chatList/bloc/chat_list_bloc.dart';
import 'chatList/bloc/chat_list_state.dart';
import 'settings/bloc/settings_cubit.dart';
import '../infrastructure/loadableState/loadable_state.dart';
import 'gradeAverages/view/grade_averages_view.dart'; import 'gradeAverages/view/grade_averages_view.dart';
import 'holidays/view/holidays_view.dart'; import 'holidays/view/holidays_view.dart';
import 'marianumMessage/view/marianum_message_list_view.dart'; import 'marianumMessage/view/marianum_message_list_view.dart';
@@ -27,7 +29,7 @@ class AppModule {
AppModule(this.module, {required this.name, required this.icon, this.breakerArea = BreakerArea.global, required this.create}); AppModule(this.module, {required this.name, required this.icon, this.breakerArea = BreakerArea.global, required this.create});
static Map<Modules, AppModule> modules(BuildContext context, { showFiltered = false }) { static Map<Modules, AppModule> modules(BuildContext context, { showFiltered = false }) {
var settings = Provider.of<SettingsProvider>(context, listen: false); var settings = context.read<SettingsCubit>();
var available = { var available = {
Modules.timetable: AppModule( Modules.timetable: AppModule(
Modules.timetable, Modules.timetable,
@@ -39,10 +41,11 @@ class AppModule {
Modules.talk: AppModule( Modules.talk: AppModule(
Modules.talk, Modules.talk,
name: 'Talk', name: 'Talk',
icon: () => Consumer<ChatListProps>( icon: () => BlocBuilder<ChatListBloc, LoadableState<ChatListState>>(
builder: (context, value, child) { builder: (context, state) {
if(value.primaryLoading()) return Icon(Icons.chat); final rooms = state.data?.rooms;
var messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b); if (rooms == null || rooms.data.isEmpty) return const Icon(Icons.chat);
final messages = rooms.data.map((e) => e.unreadMessages).reduce((a, b) => a + b);
return badges.Badge( return badges.Badge(
showBadge: messages > 0, showBadge: messages > 0,
position: badges.BadgePosition.topEnd(top: -3, end: -3), position: badges.BadgePosition.topEnd(top: -3, end: -3),
@@ -53,7 +56,7 @@ class AppModule {
elevation: 1, elevation: 1,
), ),
badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)), badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
child: Icon(Icons.chat), child: const Icon(Icons.chat),
); );
}, },
), ),
@@ -0,0 +1,51 @@
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../../../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import '../repository/breaker_repository.dart';
import 'breaker_event.dart';
import 'breaker_state.dart';
class BreakerBloc extends LoadableHydratedBloc<BreakerEvent, BreakerState, BreakerRepository> {
PackageInfo? _packageInfo;
@override
BreakerRepository repository() => BreakerRepository();
@override
BreakerState fromNothing() => const BreakerState();
@override
BreakerState fromStorage(Map<String, dynamic> json) => BreakerState.fromJson(json);
@override
Map<String, dynamic>? toStorage(BreakerState state) => state.toJson();
@override
Future<void> gatherData() async {
_packageInfo ??= await PackageInfo.fromPlatform();
final response = await repo.data.getBreakers();
add(DataGathered((s) => s.copyWith(response: response)));
}
void refresh() => fetch();
String? isBlocked(BreakerArea? type) {
if (kDebugMode) return null;
final response = innerState?.response;
if (response == null || _packageInfo == null) return null;
if (response.global.areas.contains(type)) return response.global.message;
final selfBuild = int.parse(_packageInfo!.buildNumber);
for (final entry in response.regional.entries) {
final affectedBuild = int.parse(entry.key.split('b')[1]);
if (affectedBuild >= selfBuild && entry.value.areas.contains(type)) {
return entry.value.message;
}
}
return null;
}
}
@@ -0,0 +1,4 @@
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import 'breaker_state.dart';
sealed class BreakerEvent extends LoadableHydratedBlocEvent<BreakerState> {}
@@ -0,0 +1,15 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
part 'breaker_state.freezed.dart';
part 'breaker_state.g.dart';
@freezed
abstract class BreakerState with _$BreakerState {
const factory BreakerState({
GetBreakersResponse? response,
}) = _BreakerState;
factory BreakerState.fromJson(Map<String, Object?> json) => _$BreakerStateFromJson(json);
}
@@ -0,0 +1,277 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'breaker_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$BreakerState {
GetBreakersResponse? get response;
/// Create a copy of BreakerState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$BreakerStateCopyWith<BreakerState> get copyWith => _$BreakerStateCopyWithImpl<BreakerState>(this as BreakerState, _$identity);
/// Serializes this BreakerState to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BreakerState&&(identical(other.response, response) || other.response == response));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,response);
@override
String toString() {
return 'BreakerState(response: $response)';
}
}
/// @nodoc
abstract mixin class $BreakerStateCopyWith<$Res> {
factory $BreakerStateCopyWith(BreakerState value, $Res Function(BreakerState) _then) = _$BreakerStateCopyWithImpl;
@useResult
$Res call({
GetBreakersResponse? response
});
}
/// @nodoc
class _$BreakerStateCopyWithImpl<$Res>
implements $BreakerStateCopyWith<$Res> {
_$BreakerStateCopyWithImpl(this._self, this._then);
final BreakerState _self;
final $Res Function(BreakerState) _then;
/// Create a copy of BreakerState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? response = freezed,}) {
return _then(_self.copyWith(
response: freezed == response ? _self.response : response // ignore: cast_nullable_to_non_nullable
as GetBreakersResponse?,
));
}
}
/// Adds pattern-matching-related methods to [BreakerState].
extension BreakerStatePatterns on BreakerState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _BreakerState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _BreakerState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _BreakerState value) $default,){
final _that = this;
switch (_that) {
case _BreakerState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _BreakerState value)? $default,){
final _that = this;
switch (_that) {
case _BreakerState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( GetBreakersResponse? response)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _BreakerState() when $default != null:
return $default(_that.response);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( GetBreakersResponse? response) $default,) {final _that = this;
switch (_that) {
case _BreakerState():
return $default(_that.response);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( GetBreakersResponse? response)? $default,) {final _that = this;
switch (_that) {
case _BreakerState() when $default != null:
return $default(_that.response);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _BreakerState implements BreakerState {
const _BreakerState({this.response});
factory _BreakerState.fromJson(Map<String, dynamic> json) => _$BreakerStateFromJson(json);
@override final GetBreakersResponse? response;
/// Create a copy of BreakerState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$BreakerStateCopyWith<_BreakerState> get copyWith => __$BreakerStateCopyWithImpl<_BreakerState>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$BreakerStateToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BreakerState&&(identical(other.response, response) || other.response == response));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,response);
@override
String toString() {
return 'BreakerState(response: $response)';
}
}
/// @nodoc
abstract mixin class _$BreakerStateCopyWith<$Res> implements $BreakerStateCopyWith<$Res> {
factory _$BreakerStateCopyWith(_BreakerState value, $Res Function(_BreakerState) _then) = __$BreakerStateCopyWithImpl;
@override @useResult
$Res call({
GetBreakersResponse? response
});
}
/// @nodoc
class __$BreakerStateCopyWithImpl<$Res>
implements _$BreakerStateCopyWith<$Res> {
__$BreakerStateCopyWithImpl(this._self, this._then);
final _BreakerState _self;
final $Res Function(_BreakerState) _then;
/// Create a copy of BreakerState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? response = freezed,}) {
return _then(_BreakerState(
response: freezed == response ? _self.response : response // ignore: cast_nullable_to_non_nullable
as GetBreakersResponse?,
));
}
}
// dart format on
@@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'breaker_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_BreakerState _$BreakerStateFromJson(Map<String, dynamic> json) =>
_BreakerState(
response: json['response'] == null
? null
: GetBreakersResponse.fromJson(
json['response'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$BreakerStateToJson(_BreakerState instance) =>
<String, dynamic>{'response': instance.response};
@@ -0,0 +1,14 @@
import 'dart:async';
import '../../../../../api/mhsl/breaker/getBreakers/getBreakersCache.dart';
import '../../../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
class BreakerDataProvider {
Future<GetBreakersResponse> getBreakers() {
final completer = Completer<GetBreakersResponse>();
GetBreakersCache(onUpdate: (data) {
if (!completer.isCompleted) completer.complete(data);
});
return completer.future;
}
}
@@ -0,0 +1,11 @@
import '../../../infrastructure/repository/repository.dart';
import '../bloc/breaker_state.dart';
import '../dataProvider/breaker_data_provider.dart';
class BreakerRepository extends Repository<BreakerState> {
final BreakerDataProvider _provider;
BreakerRepository([BreakerDataProvider? provider]) : _provider = provider ?? BreakerDataProvider();
BreakerDataProvider get data => _provider;
}
@@ -0,0 +1,59 @@
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import '../repository/chat_repository.dart';
import 'chat_event.dart';
import 'chat_state.dart';
class ChatBloc extends LoadableHydratedBloc<ChatEvent, ChatState, ChatRepository> {
DateTime _lastTokenSet = DateTime.fromMillisecondsSinceEpoch(0);
@override
ChatRepository repository() => ChatRepository();
@override
ChatState fromNothing() => const ChatState();
@override
ChatState fromStorage(Map<String, dynamic> json) => ChatState.fromJson(json);
@override
Map<String, dynamic>? toStorage(ChatState state) => state.toJson();
@override
Future<void> gatherData() async {
final token = innerState?.currentToken ?? '';
if (token.isEmpty) return;
_loadChat(token);
}
void setToken(String token) {
if (token == (innerState?.currentToken ?? '')) {
refresh();
return;
}
add(Emit((s) => s.copyWith(currentToken: token, chatResponse: null)));
_loadChat(token);
}
void setReferenceMessageId(int? messageId) {
add(Emit((s) => s.copyWith(referenceMessageId: messageId)));
}
void refresh() {
final token = innerState?.currentToken ?? '';
if (token.isNotEmpty) _loadChat(token);
}
void _loadChat(String token) {
final requestStart = DateTime.now();
_lastTokenSet = requestStart;
repo.data.getChat(
token: token,
onUpdate: (data) {
if (_lastTokenSet.isAfter(requestStart)) return;
if ((innerState?.currentToken ?? '') != token) return;
add(DataGathered((s) => s.copyWith(chatResponse: data)));
},
);
}
}
@@ -0,0 +1,4 @@
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import 'chat_state.dart';
sealed class ChatEvent extends LoadableHydratedBlocEvent<ChatState> {}
@@ -0,0 +1,17 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
part 'chat_state.freezed.dart';
part 'chat_state.g.dart';
@freezed
abstract class ChatState with _$ChatState {
const factory ChatState({
@Default('') String currentToken,
GetChatResponse? chatResponse,
int? referenceMessageId,
}) = _ChatState;
factory ChatState.fromJson(Map<String, Object?> json) => _$ChatStateFromJson(json);
}
@@ -0,0 +1,283 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'chat_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ChatState {
String get currentToken; GetChatResponse? get chatResponse; int? get referenceMessageId;
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ChatStateCopyWith<ChatState> get copyWith => _$ChatStateCopyWithImpl<ChatState>(this as ChatState, _$identity);
/// Serializes this ChatState to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChatState&&(identical(other.currentToken, currentToken) || other.currentToken == currentToken)&&(identical(other.chatResponse, chatResponse) || other.chatResponse == chatResponse)&&(identical(other.referenceMessageId, referenceMessageId) || other.referenceMessageId == referenceMessageId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,currentToken,chatResponse,referenceMessageId);
@override
String toString() {
return 'ChatState(currentToken: $currentToken, chatResponse: $chatResponse, referenceMessageId: $referenceMessageId)';
}
}
/// @nodoc
abstract mixin class $ChatStateCopyWith<$Res> {
factory $ChatStateCopyWith(ChatState value, $Res Function(ChatState) _then) = _$ChatStateCopyWithImpl;
@useResult
$Res call({
String currentToken, GetChatResponse? chatResponse, int? referenceMessageId
});
}
/// @nodoc
class _$ChatStateCopyWithImpl<$Res>
implements $ChatStateCopyWith<$Res> {
_$ChatStateCopyWithImpl(this._self, this._then);
final ChatState _self;
final $Res Function(ChatState) _then;
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? currentToken = null,Object? chatResponse = freezed,Object? referenceMessageId = freezed,}) {
return _then(_self.copyWith(
currentToken: null == currentToken ? _self.currentToken : currentToken // ignore: cast_nullable_to_non_nullable
as String,chatResponse: freezed == chatResponse ? _self.chatResponse : chatResponse // ignore: cast_nullable_to_non_nullable
as GetChatResponse?,referenceMessageId: freezed == referenceMessageId ? _self.referenceMessageId : referenceMessageId // ignore: cast_nullable_to_non_nullable
as int?,
));
}
}
/// Adds pattern-matching-related methods to [ChatState].
extension ChatStatePatterns on ChatState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ChatState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ChatState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ChatState value) $default,){
final _that = this;
switch (_that) {
case _ChatState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ChatState value)? $default,){
final _that = this;
switch (_that) {
case _ChatState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String currentToken, GetChatResponse? chatResponse, int? referenceMessageId)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ChatState() when $default != null:
return $default(_that.currentToken,_that.chatResponse,_that.referenceMessageId);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String currentToken, GetChatResponse? chatResponse, int? referenceMessageId) $default,) {final _that = this;
switch (_that) {
case _ChatState():
return $default(_that.currentToken,_that.chatResponse,_that.referenceMessageId);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String currentToken, GetChatResponse? chatResponse, int? referenceMessageId)? $default,) {final _that = this;
switch (_that) {
case _ChatState() when $default != null:
return $default(_that.currentToken,_that.chatResponse,_that.referenceMessageId);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ChatState implements ChatState {
const _ChatState({this.currentToken = '', this.chatResponse, this.referenceMessageId});
factory _ChatState.fromJson(Map<String, dynamic> json) => _$ChatStateFromJson(json);
@override@JsonKey() final String currentToken;
@override final GetChatResponse? chatResponse;
@override final int? referenceMessageId;
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ChatStateCopyWith<_ChatState> get copyWith => __$ChatStateCopyWithImpl<_ChatState>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ChatStateToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatState&&(identical(other.currentToken, currentToken) || other.currentToken == currentToken)&&(identical(other.chatResponse, chatResponse) || other.chatResponse == chatResponse)&&(identical(other.referenceMessageId, referenceMessageId) || other.referenceMessageId == referenceMessageId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,currentToken,chatResponse,referenceMessageId);
@override
String toString() {
return 'ChatState(currentToken: $currentToken, chatResponse: $chatResponse, referenceMessageId: $referenceMessageId)';
}
}
/// @nodoc
abstract mixin class _$ChatStateCopyWith<$Res> implements $ChatStateCopyWith<$Res> {
factory _$ChatStateCopyWith(_ChatState value, $Res Function(_ChatState) _then) = __$ChatStateCopyWithImpl;
@override @useResult
$Res call({
String currentToken, GetChatResponse? chatResponse, int? referenceMessageId
});
}
/// @nodoc
class __$ChatStateCopyWithImpl<$Res>
implements _$ChatStateCopyWith<$Res> {
__$ChatStateCopyWithImpl(this._self, this._then);
final _ChatState _self;
final $Res Function(_ChatState) _then;
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? currentToken = null,Object? chatResponse = freezed,Object? referenceMessageId = freezed,}) {
return _then(_ChatState(
currentToken: null == currentToken ? _self.currentToken : currentToken // ignore: cast_nullable_to_non_nullable
as String,chatResponse: freezed == chatResponse ? _self.chatResponse : chatResponse // ignore: cast_nullable_to_non_nullable
as GetChatResponse?,referenceMessageId: freezed == referenceMessageId ? _self.referenceMessageId : referenceMessageId // ignore: cast_nullable_to_non_nullable
as int?,
));
}
}
// dart format on
@@ -0,0 +1,22 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_ChatState _$ChatStateFromJson(Map<String, dynamic> json) => _ChatState(
currentToken: json['currentToken'] as String? ?? '',
chatResponse: json['chatResponse'] == null
? null
: GetChatResponse.fromJson(json['chatResponse'] as Map<String, dynamic>),
referenceMessageId: (json['referenceMessageId'] as num?)?.toInt(),
);
Map<String, dynamic> _$ChatStateToJson(_ChatState instance) =>
<String, dynamic>{
'currentToken': instance.currentToken,
'chatResponse': instance.chatResponse,
'referenceMessageId': instance.referenceMessageId,
};
@@ -0,0 +1,11 @@
import '../../../../../api/marianumcloud/talk/chat/getChatCache.dart';
import '../../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
class ChatDataProvider {
void getChat({
required String token,
required void Function(GetChatResponse data) onUpdate,
}) {
GetChatCache(chatToken: token, onUpdate: onUpdate);
}
}
@@ -0,0 +1,11 @@
import '../../../infrastructure/repository/repository.dart';
import '../bloc/chat_state.dart';
import '../dataProvider/chat_data_provider.dart';
class ChatRepository extends Repository<ChatState> {
final ChatDataProvider _provider;
ChatRepository([ChatDataProvider? provider]) : _provider = provider ?? ChatDataProvider();
ChatDataProvider get data => _provider;
}
@@ -0,0 +1,46 @@
import 'package:flutter_app_badge/flutter_app_badge.dart';
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import '../repository/chat_list_repository.dart';
import 'chat_list_event.dart';
import 'chat_list_state.dart';
class ChatListBloc extends LoadableHydratedBloc<ChatListEvent, ChatListState, ChatListRepository> {
@override
ChatListRepository repository() => ChatListRepository();
@override
ChatListState fromNothing() => const ChatListState();
@override
ChatListState fromStorage(Map<String, dynamic> json) => ChatListState.fromJson(json);
@override
Map<String, dynamic>? toStorage(ChatListState state) => state.toJson();
@override
Future<void> gatherData() async {
final rooms = await repo.data.getRooms();
add(DataGathered((s) => s.copyWith(rooms: rooms)));
_updateAppBadge(rooms);
}
Future<void> refresh({bool renew = true}) async {
final rooms = await repo.data.getRooms(renew: renew);
add(DataGathered((s) => s.copyWith(rooms: rooms)));
_updateAppBadge(rooms);
}
Future<void> createDirectChat(String invite) async {
await repo.data.createDirectRoom(invite);
await refresh();
}
void _updateAppBadge(dynamic rooms) {
try {
final unread = rooms.data.map((e) => e.unreadMessages).fold<int>(0, (a, b) => a + b as int);
FlutterAppBadge.count(unread);
} catch (_) {}
}
}
@@ -0,0 +1,4 @@
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import 'chat_list_state.dart';
sealed class ChatListEvent extends LoadableHydratedBlocEvent<ChatListState> {}
@@ -0,0 +1,15 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
part 'chat_list_state.freezed.dart';
part 'chat_list_state.g.dart';
@freezed
abstract class ChatListState with _$ChatListState {
const factory ChatListState({
GetRoomResponse? rooms,
}) = _ChatListState;
factory ChatListState.fromJson(Map<String, Object?> json) => _$ChatListStateFromJson(json);
}
@@ -0,0 +1,277 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'chat_list_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ChatListState {
GetRoomResponse? get rooms;
/// Create a copy of ChatListState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ChatListStateCopyWith<ChatListState> get copyWith => _$ChatListStateCopyWithImpl<ChatListState>(this as ChatListState, _$identity);
/// Serializes this ChatListState to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChatListState&&(identical(other.rooms, rooms) || other.rooms == rooms));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,rooms);
@override
String toString() {
return 'ChatListState(rooms: $rooms)';
}
}
/// @nodoc
abstract mixin class $ChatListStateCopyWith<$Res> {
factory $ChatListStateCopyWith(ChatListState value, $Res Function(ChatListState) _then) = _$ChatListStateCopyWithImpl;
@useResult
$Res call({
GetRoomResponse? rooms
});
}
/// @nodoc
class _$ChatListStateCopyWithImpl<$Res>
implements $ChatListStateCopyWith<$Res> {
_$ChatListStateCopyWithImpl(this._self, this._then);
final ChatListState _self;
final $Res Function(ChatListState) _then;
/// Create a copy of ChatListState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? rooms = freezed,}) {
return _then(_self.copyWith(
rooms: freezed == rooms ? _self.rooms : rooms // ignore: cast_nullable_to_non_nullable
as GetRoomResponse?,
));
}
}
/// Adds pattern-matching-related methods to [ChatListState].
extension ChatListStatePatterns on ChatListState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ChatListState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ChatListState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ChatListState value) $default,){
final _that = this;
switch (_that) {
case _ChatListState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ChatListState value)? $default,){
final _that = this;
switch (_that) {
case _ChatListState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( GetRoomResponse? rooms)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ChatListState() when $default != null:
return $default(_that.rooms);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( GetRoomResponse? rooms) $default,) {final _that = this;
switch (_that) {
case _ChatListState():
return $default(_that.rooms);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( GetRoomResponse? rooms)? $default,) {final _that = this;
switch (_that) {
case _ChatListState() when $default != null:
return $default(_that.rooms);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ChatListState implements ChatListState {
const _ChatListState({this.rooms});
factory _ChatListState.fromJson(Map<String, dynamic> json) => _$ChatListStateFromJson(json);
@override final GetRoomResponse? rooms;
/// Create a copy of ChatListState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ChatListStateCopyWith<_ChatListState> get copyWith => __$ChatListStateCopyWithImpl<_ChatListState>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ChatListStateToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatListState&&(identical(other.rooms, rooms) || other.rooms == rooms));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,rooms);
@override
String toString() {
return 'ChatListState(rooms: $rooms)';
}
}
/// @nodoc
abstract mixin class _$ChatListStateCopyWith<$Res> implements $ChatListStateCopyWith<$Res> {
factory _$ChatListStateCopyWith(_ChatListState value, $Res Function(_ChatListState) _then) = __$ChatListStateCopyWithImpl;
@override @useResult
$Res call({
GetRoomResponse? rooms
});
}
/// @nodoc
class __$ChatListStateCopyWithImpl<$Res>
implements _$ChatListStateCopyWith<$Res> {
__$ChatListStateCopyWithImpl(this._self, this._then);
final _ChatListState _self;
final $Res Function(_ChatListState) _then;
/// Create a copy of ChatListState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? rooms = freezed,}) {
return _then(_ChatListState(
rooms: freezed == rooms ? _self.rooms : rooms // ignore: cast_nullable_to_non_nullable
as GetRoomResponse?,
));
}
}
// dart format on
@@ -0,0 +1,17 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_list_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_ChatListState _$ChatListStateFromJson(Map<String, dynamic> json) =>
_ChatListState(
rooms: json['rooms'] == null
? null
: GetRoomResponse.fromJson(json['rooms'] as Map<String, dynamic>),
);
Map<String, dynamic> _$ChatListStateToJson(_ChatListState instance) =>
<String, dynamic>{'rooms': instance.rooms};
@@ -0,0 +1,22 @@
import 'dart:async';
import '../../../../../api/marianumcloud/talk/room/getRoomCache.dart';
import '../../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../../api/marianumcloud/talk/createRoom/createRoom.dart';
import '../../../../../api/marianumcloud/talk/createRoom/createRoomParams.dart';
class ChatListDataProvider {
Future<GetRoomResponse> getRooms({bool renew = false}) {
final completer = Completer<GetRoomResponse>();
GetRoomCache(
renew: renew,
onUpdate: (data) {
if (!completer.isCompleted) completer.complete(data);
},
);
return completer.future;
}
Future<void> createDirectRoom(String invite) =>
CreateRoom(CreateRoomParams(roomType: 1, invite: invite)).run();
}
@@ -0,0 +1,11 @@
import '../../../infrastructure/repository/repository.dart';
import '../bloc/chat_list_state.dart';
import '../dataProvider/chat_list_data_provider.dart';
class ChatListRepository extends Repository<ChatListState> {
final ChatListDataProvider _provider;
ChatListRepository([ChatListDataProvider? provider]) : _provider = provider ?? ChatListDataProvider();
ChatListDataProvider get data => _provider;
}
@@ -0,0 +1,52 @@
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import '../repository/files_repository.dart';
import 'files_event.dart';
import 'files_state.dart';
class FilesBloc extends LoadableHydratedBloc<FilesEvent, FilesState, FilesRepository> {
final List<String> initialPath;
FilesBloc({this.initialPath = const []});
@override
FilesRepository repository() => FilesRepository();
@override
FilesState fromNothing() => FilesState(currentPath: initialPath);
@override
FilesState fromStorage(Map<String, dynamic> json) => FilesState.fromJson(json);
@override
Map<String, dynamic>? toStorage(FilesState state) => null;
@override
Future<void> gatherData() async {
final path = innerState?.currentPath ?? initialPath;
await _query(path);
}
Future<void> refresh() async {
final path = innerState?.currentPath ?? initialPath;
await _query(path);
}
Future<void> setPath(List<String> path) async {
add(Emit((s) => s.copyWith(currentPath: path, listing: null)));
await _query(path);
}
Future<void> createFolder(String name) async {
final path = innerState?.currentPath ?? initialPath;
await repo.data.createFolder('${path.join('/')}/$name');
await refresh();
}
Future<void> _query(List<String> path) async {
final pathString = path.isEmpty ? '/' : path.join('/');
final listing = await repo.data.listFiles(pathString);
listing.files.removeWhere((file) => file.name.isEmpty || file.name == path.lastOrNull);
add(DataGathered((s) => s.copyWith(listing: listing)));
}
}
@@ -0,0 +1,4 @@
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import 'files_state.dart';
sealed class FilesEvent extends LoadableHydratedBlocEvent<FilesState> {}
@@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../../../api/marianumcloud/webdav/queries/listFiles/listFilesResponse.dart';
part 'files_state.freezed.dart';
part 'files_state.g.dart';
@freezed
abstract class FilesState with _$FilesState {
const factory FilesState({
@Default(<String>[]) List<String> currentPath,
ListFilesResponse? listing,
}) = _FilesState;
factory FilesState.fromJson(Map<String, Object?> json) => _$FilesStateFromJson(json);
}
@@ -0,0 +1,286 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'files_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$FilesState {
List<String> get currentPath; ListFilesResponse? get listing;
/// Create a copy of FilesState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$FilesStateCopyWith<FilesState> get copyWith => _$FilesStateCopyWithImpl<FilesState>(this as FilesState, _$identity);
/// Serializes this FilesState to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is FilesState&&const DeepCollectionEquality().equals(other.currentPath, currentPath)&&(identical(other.listing, listing) || other.listing == listing));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(currentPath),listing);
@override
String toString() {
return 'FilesState(currentPath: $currentPath, listing: $listing)';
}
}
/// @nodoc
abstract mixin class $FilesStateCopyWith<$Res> {
factory $FilesStateCopyWith(FilesState value, $Res Function(FilesState) _then) = _$FilesStateCopyWithImpl;
@useResult
$Res call({
List<String> currentPath, ListFilesResponse? listing
});
}
/// @nodoc
class _$FilesStateCopyWithImpl<$Res>
implements $FilesStateCopyWith<$Res> {
_$FilesStateCopyWithImpl(this._self, this._then);
final FilesState _self;
final $Res Function(FilesState) _then;
/// Create a copy of FilesState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? currentPath = null,Object? listing = freezed,}) {
return _then(_self.copyWith(
currentPath: null == currentPath ? _self.currentPath : currentPath // ignore: cast_nullable_to_non_nullable
as List<String>,listing: freezed == listing ? _self.listing : listing // ignore: cast_nullable_to_non_nullable
as ListFilesResponse?,
));
}
}
/// Adds pattern-matching-related methods to [FilesState].
extension FilesStatePatterns on FilesState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _FilesState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _FilesState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _FilesState value) $default,){
final _that = this;
switch (_that) {
case _FilesState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _FilesState value)? $default,){
final _that = this;
switch (_that) {
case _FilesState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<String> currentPath, ListFilesResponse? listing)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _FilesState() when $default != null:
return $default(_that.currentPath,_that.listing);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<String> currentPath, ListFilesResponse? listing) $default,) {final _that = this;
switch (_that) {
case _FilesState():
return $default(_that.currentPath,_that.listing);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<String> currentPath, ListFilesResponse? listing)? $default,) {final _that = this;
switch (_that) {
case _FilesState() when $default != null:
return $default(_that.currentPath,_that.listing);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _FilesState implements FilesState {
const _FilesState({final List<String> currentPath = const <String>[], this.listing}): _currentPath = currentPath;
factory _FilesState.fromJson(Map<String, dynamic> json) => _$FilesStateFromJson(json);
final List<String> _currentPath;
@override@JsonKey() List<String> get currentPath {
if (_currentPath is EqualUnmodifiableListView) return _currentPath;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_currentPath);
}
@override final ListFilesResponse? listing;
/// Create a copy of FilesState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$FilesStateCopyWith<_FilesState> get copyWith => __$FilesStateCopyWithImpl<_FilesState>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$FilesStateToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _FilesState&&const DeepCollectionEquality().equals(other._currentPath, _currentPath)&&(identical(other.listing, listing) || other.listing == listing));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_currentPath),listing);
@override
String toString() {
return 'FilesState(currentPath: $currentPath, listing: $listing)';
}
}
/// @nodoc
abstract mixin class _$FilesStateCopyWith<$Res> implements $FilesStateCopyWith<$Res> {
factory _$FilesStateCopyWith(_FilesState value, $Res Function(_FilesState) _then) = __$FilesStateCopyWithImpl;
@override @useResult
$Res call({
List<String> currentPath, ListFilesResponse? listing
});
}
/// @nodoc
class __$FilesStateCopyWithImpl<$Res>
implements _$FilesStateCopyWith<$Res> {
__$FilesStateCopyWithImpl(this._self, this._then);
final _FilesState _self;
final $Res Function(_FilesState) _then;
/// Create a copy of FilesState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? currentPath = null,Object? listing = freezed,}) {
return _then(_FilesState(
currentPath: null == currentPath ? _self._currentPath : currentPath // ignore: cast_nullable_to_non_nullable
as List<String>,listing: freezed == listing ? _self.listing : listing // ignore: cast_nullable_to_non_nullable
as ListFilesResponse?,
));
}
}
// dart format on
@@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'files_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_FilesState _$FilesStateFromJson(Map<String, dynamic> json) => _FilesState(
currentPath:
(json['currentPath'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const <String>[],
listing: json['listing'] == null
? null
: ListFilesResponse.fromJson(json['listing'] as Map<String, dynamic>),
);
Map<String, dynamic> _$FilesStateToJson(_FilesState instance) =>
<String, dynamic>{
'currentPath': instance.currentPath,
'listing': instance.listing,
};
@@ -0,0 +1,25 @@
import 'dart:async';
import 'package:nextcloud/nextcloud.dart';
import '../../../../../api/marianumcloud/webdav/queries/listFiles/listFilesCache.dart';
import '../../../../../api/marianumcloud/webdav/queries/listFiles/listFilesResponse.dart';
import '../../../../../api/marianumcloud/webdav/webdavApi.dart';
class FilesDataProvider {
Future<ListFilesResponse> listFiles(String path) {
final completer = Completer<ListFilesResponse>();
ListFilesCache(
path: path,
onUpdate: (data) {
if (!completer.isCompleted) completer.complete(data);
},
);
return completer.future;
}
Future<void> createFolder(String fullPath) async {
final webdav = await WebdavApi.webdav;
await webdav.mkcol(PathUri.parse(fullPath));
}
}
@@ -0,0 +1,11 @@
import '../../../infrastructure/repository/repository.dart';
import '../bloc/files_state.dart';
import '../dataProvider/files_data_provider.dart';
class FilesRepository extends Repository<FilesState> {
final FilesDataProvider _provider;
FilesRepository([FilesDataProvider? provider]) : _provider = provider ?? FilesDataProvider();
FilesDataProvider get data => _provider;
}
@@ -0,0 +1,65 @@
import 'dart:developer';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import '../../../../../storage/base/settings.dart';
import '../../../../../view/settings/defaultSettings.dart';
class SettingsCubit extends HydratedCubit<Settings> {
static const _debounceTag = 'settings_persist';
SettingsCubit() : super(DefaultSettings.get());
Settings val({bool write = false}) {
if (write) {
// Notify listeners immediately so the UI reflects the mutation right away;
// debounce the actual persistence to disk to avoid hammering on rapid edits.
_emitFreshInstance();
EasyDebounce.debounce(_debounceTag, const Duration(milliseconds: 500), _emitFreshInstance);
}
return state;
}
void _emitFreshInstance() {
try {
emit(Settings.fromJson(state.toJson()));
} catch (e) {
log('Failed to refresh settings state: $e');
}
}
Future<void> reset() async {
emit(DefaultSettings.get());
}
@override
Settings fromJson(Map<String, dynamic> json) {
try {
return Settings.fromJson(json);
} catch (_) {
try {
return Settings.fromJson(_mergeSettings(json, DefaultSettings.get().toJson()));
} catch (_) {
return DefaultSettings.get();
}
}
}
@override
Map<String, dynamic>? toJson(Settings state) => state.toJson();
Map<String, dynamic> _mergeSettings(Map<String, dynamic> oldMap, Map<String, dynamic> newMap) {
final merged = Map<String, dynamic>.from(newMap);
oldMap.forEach((key, value) {
if (merged.containsKey(key)) {
if (value is Map<String, dynamic> && merged[key] is Map<String, dynamic>) {
merged[key] = _mergeSettings(value, merged[key]);
} else {
merged[key] = value;
}
}
});
return merged;
}
}
@@ -0,0 +1,138 @@
import 'package:intl/intl.dart';
import '../../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
import '../../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import '../repository/timetable_repository.dart';
import 'timetable_event.dart';
import 'timetable_state.dart';
class TimetableBloc extends LoadableHydratedBloc<TimetableEvent, TimetableState, TimetableRepository> {
static const Duration _weekSpan = Duration(days: 7);
static final DateFormat _weekKeyFormat = DateFormat('yyyyMMdd');
DateTime _lastWeekRequestStart = DateTime.fromMillisecondsSinceEpoch(0);
@override
TimetableRepository repository() => TimetableRepository();
@override
TimetableState fromNothing() {
final reference = DateTime.now().add(const Duration(days: 2));
return TimetableState(
startDate: _startOfWeek(reference),
endDate: _endOfWeek(reference),
);
}
@override
TimetableState fromStorage(Map<String, dynamic> json) => TimetableState.fromJson(json);
@override
Map<String, dynamic>? toStorage(TimetableState state) => state.toJson();
@override
Future<void> gatherData() async {
final initial = innerState ?? fromNothing();
await Future.wait([
_loadCurrentWeek(initial.startDate, initial.endDate),
_loadStaticReferenceData(),
_loadCustomEvents(),
]);
_prefetchAdjacentWeeks(initial.startDate, initial.endDate);
}
void changeWeek(DateTime startDate, DateTime endDate) {
final current = innerState ?? fromNothing();
if (current.startDate == startDate && current.endDate == endDate) return;
add(Emit((s) => s.copyWith(startDate: startDate, endDate: endDate)));
_loadCurrentWeek(startDate, endDate);
_prefetchAdjacentWeeks(startDate, endDate);
}
void resetWeek() {
final reference = DateTime.now().add(const Duration(days: 2));
changeWeek(_startOfWeek(reference), _endOfWeek(reference));
}
void refresh() => fetch();
Future<void> addCustomEvent(CustomTimetableEvent event) async {
await repo.data.addCustomEvent(event);
await _refreshCustomEvents();
}
Future<void> updateCustomEvent(String id, CustomTimetableEvent event) async {
await repo.data.updateCustomEvent(id, event);
await _refreshCustomEvents();
}
Future<void> removeCustomEvent(String id) async {
await repo.data.removeCustomEvent(id);
await _refreshCustomEvents();
}
Future<void> _loadCurrentWeek(DateTime startDate, DateTime endDate) async {
final requestStart = DateTime.now();
_lastWeekRequestStart = requestStart;
try {
final week = await repo.data.getWeek(startDate, endDate);
if (_lastWeekRequestStart.isAfter(requestStart)) return;
_writeWeekToCache(startDate, week);
} catch (_) {
// Errors are surfaced via LoadableHydratedBloc.fetch's catchError.
rethrow;
}
}
Future<void> _loadStaticReferenceData() async {
final (rooms, subjects, schoolHolidays) = await (
repo.data.getRooms(),
repo.data.getSubjects(),
repo.data.getSchoolHolidays(),
).wait;
add(DataGathered((s) => s.copyWith(
rooms: rooms,
subjects: subjects,
schoolHolidays: schoolHolidays,
dataVersion: s.dataVersion + 1,
)));
}
Future<void> _loadCustomEvents({bool renew = false}) async {
final events = await repo.data.getCustomEvents(renew: renew);
add(DataGathered((s) => s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1)));
}
Future<void> _refreshCustomEvents() => _loadCustomEvents(renew: true);
void _prefetchAdjacentWeeks(DateTime start, DateTime end) {
_prefetchWeek(start.subtract(_weekSpan), end.subtract(_weekSpan));
_prefetchWeek(start.add(_weekSpan), end.add(_weekSpan));
}
void _prefetchWeek(DateTime start, DateTime end) {
repo.data.getWeek(start, end).then((week) => _writeWeekToCache(start, week)).catchError((_) {});
}
void _writeWeekToCache(DateTime weekStart, GetTimetableResponse week) {
final key = _weekKeyFormat.format(weekStart);
add(DataGathered((s) {
final updated = Map<String, GetTimetableResponse>.of(s.weekCache);
updated[key] = week;
return s.copyWith(weekCache: updated, dataVersion: s.dataVersion + 1);
}));
}
static DateTime _startOfWeek(DateTime reference) {
final monday = reference.subtract(Duration(days: reference.weekday - 1));
return DateTime(monday.year, monday.month, monday.day);
}
static DateTime _endOfWeek(DateTime reference) {
final friday = reference.add(Duration(days: DateTime.daysPerWeek - reference.weekday - 2));
return DateTime(friday.year, friday.month, friday.day);
}
}
@@ -0,0 +1,4 @@
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import 'timetable_state.dart';
sealed class TimetableEvent extends LoadableHydratedBlocEvent<TimetableState> {}
@@ -0,0 +1,33 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../../../api/mhsl/customTimetableEvent/get/getCustomTimetableEventResponse.dart';
import '../../../../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart';
import '../../../../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
import '../../../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart';
import '../../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
part 'timetable_state.freezed.dart';
part 'timetable_state.g.dart';
@freezed
abstract class TimetableState with _$TimetableState {
const TimetableState._();
const factory TimetableState({
@Default(<String, GetTimetableResponse>{}) Map<String, GetTimetableResponse> weekCache,
GetRoomsResponse? rooms,
GetSubjectsResponse? subjects,
GetHolidaysResponse? schoolHolidays,
GetCustomTimetableEventResponse? customEvents,
required DateTime startDate,
required DateTime endDate,
@Default(0) int dataVersion,
}) = _TimetableState;
factory TimetableState.fromJson(Map<String, Object?> json) => _$TimetableStateFromJson(json);
Iterable<GetTimetableResponseObject> getAllKnownLessons() =>
weekCache.values.expand((response) => response.result);
bool get hasReferenceData => rooms != null && subjects != null && schoolHolidays != null && customEvents != null;
}
@@ -0,0 +1,304 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'timetable_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$TimetableState {
Map<String, GetTimetableResponse> get weekCache; GetRoomsResponse? get rooms; GetSubjectsResponse? get subjects; GetHolidaysResponse? get schoolHolidays; GetCustomTimetableEventResponse? get customEvents; DateTime get startDate; DateTime get endDate; int get dataVersion;
/// Create a copy of TimetableState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$TimetableStateCopyWith<TimetableState> get copyWith => _$TimetableStateCopyWithImpl<TimetableState>(this as TimetableState, _$identity);
/// Serializes this TimetableState to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is TimetableState&&const DeepCollectionEquality().equals(other.weekCache, weekCache)&&(identical(other.rooms, rooms) || other.rooms == rooms)&&(identical(other.subjects, subjects) || other.subjects == subjects)&&(identical(other.schoolHolidays, schoolHolidays) || other.schoolHolidays == schoolHolidays)&&(identical(other.customEvents, customEvents) || other.customEvents == customEvents)&&(identical(other.startDate, startDate) || other.startDate == startDate)&&(identical(other.endDate, endDate) || other.endDate == endDate)&&(identical(other.dataVersion, dataVersion) || other.dataVersion == dataVersion));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(weekCache),rooms,subjects,schoolHolidays,customEvents,startDate,endDate,dataVersion);
@override
String toString() {
return 'TimetableState(weekCache: $weekCache, rooms: $rooms, subjects: $subjects, schoolHolidays: $schoolHolidays, customEvents: $customEvents, startDate: $startDate, endDate: $endDate, dataVersion: $dataVersion)';
}
}
/// @nodoc
abstract mixin class $TimetableStateCopyWith<$Res> {
factory $TimetableStateCopyWith(TimetableState value, $Res Function(TimetableState) _then) = _$TimetableStateCopyWithImpl;
@useResult
$Res call({
Map<String, GetTimetableResponse> weekCache, GetRoomsResponse? rooms, GetSubjectsResponse? subjects, GetHolidaysResponse? schoolHolidays, GetCustomTimetableEventResponse? customEvents, DateTime startDate, DateTime endDate, int dataVersion
});
}
/// @nodoc
class _$TimetableStateCopyWithImpl<$Res>
implements $TimetableStateCopyWith<$Res> {
_$TimetableStateCopyWithImpl(this._self, this._then);
final TimetableState _self;
final $Res Function(TimetableState) _then;
/// Create a copy of TimetableState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? weekCache = null,Object? rooms = freezed,Object? subjects = freezed,Object? schoolHolidays = freezed,Object? customEvents = freezed,Object? startDate = null,Object? endDate = null,Object? dataVersion = null,}) {
return _then(_self.copyWith(
weekCache: null == weekCache ? _self.weekCache : weekCache // ignore: cast_nullable_to_non_nullable
as Map<String, GetTimetableResponse>,rooms: freezed == rooms ? _self.rooms : rooms // ignore: cast_nullable_to_non_nullable
as GetRoomsResponse?,subjects: freezed == subjects ? _self.subjects : subjects // ignore: cast_nullable_to_non_nullable
as GetSubjectsResponse?,schoolHolidays: freezed == schoolHolidays ? _self.schoolHolidays : schoolHolidays // ignore: cast_nullable_to_non_nullable
as GetHolidaysResponse?,customEvents: freezed == customEvents ? _self.customEvents : customEvents // ignore: cast_nullable_to_non_nullable
as GetCustomTimetableEventResponse?,startDate: null == startDate ? _self.startDate : startDate // ignore: cast_nullable_to_non_nullable
as DateTime,endDate: null == endDate ? _self.endDate : endDate // ignore: cast_nullable_to_non_nullable
as DateTime,dataVersion: null == dataVersion ? _self.dataVersion : dataVersion // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// Adds pattern-matching-related methods to [TimetableState].
extension TimetableStatePatterns on TimetableState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _TimetableState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _TimetableState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _TimetableState value) $default,){
final _that = this;
switch (_that) {
case _TimetableState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _TimetableState value)? $default,){
final _that = this;
switch (_that) {
case _TimetableState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Map<String, GetTimetableResponse> weekCache, GetRoomsResponse? rooms, GetSubjectsResponse? subjects, GetHolidaysResponse? schoolHolidays, GetCustomTimetableEventResponse? customEvents, DateTime startDate, DateTime endDate, int dataVersion)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _TimetableState() when $default != null:
return $default(_that.weekCache,_that.rooms,_that.subjects,_that.schoolHolidays,_that.customEvents,_that.startDate,_that.endDate,_that.dataVersion);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Map<String, GetTimetableResponse> weekCache, GetRoomsResponse? rooms, GetSubjectsResponse? subjects, GetHolidaysResponse? schoolHolidays, GetCustomTimetableEventResponse? customEvents, DateTime startDate, DateTime endDate, int dataVersion) $default,) {final _that = this;
switch (_that) {
case _TimetableState():
return $default(_that.weekCache,_that.rooms,_that.subjects,_that.schoolHolidays,_that.customEvents,_that.startDate,_that.endDate,_that.dataVersion);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Map<String, GetTimetableResponse> weekCache, GetRoomsResponse? rooms, GetSubjectsResponse? subjects, GetHolidaysResponse? schoolHolidays, GetCustomTimetableEventResponse? customEvents, DateTime startDate, DateTime endDate, int dataVersion)? $default,) {final _that = this;
switch (_that) {
case _TimetableState() when $default != null:
return $default(_that.weekCache,_that.rooms,_that.subjects,_that.schoolHolidays,_that.customEvents,_that.startDate,_that.endDate,_that.dataVersion);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _TimetableState extends TimetableState {
const _TimetableState({final Map<String, GetTimetableResponse> weekCache = const <String, GetTimetableResponse>{}, this.rooms, this.subjects, this.schoolHolidays, this.customEvents, required this.startDate, required this.endDate, this.dataVersion = 0}): _weekCache = weekCache,super._();
factory _TimetableState.fromJson(Map<String, dynamic> json) => _$TimetableStateFromJson(json);
final Map<String, GetTimetableResponse> _weekCache;
@override@JsonKey() Map<String, GetTimetableResponse> get weekCache {
if (_weekCache is EqualUnmodifiableMapView) return _weekCache;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_weekCache);
}
@override final GetRoomsResponse? rooms;
@override final GetSubjectsResponse? subjects;
@override final GetHolidaysResponse? schoolHolidays;
@override final GetCustomTimetableEventResponse? customEvents;
@override final DateTime startDate;
@override final DateTime endDate;
@override@JsonKey() final int dataVersion;
/// Create a copy of TimetableState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$TimetableStateCopyWith<_TimetableState> get copyWith => __$TimetableStateCopyWithImpl<_TimetableState>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$TimetableStateToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TimetableState&&const DeepCollectionEquality().equals(other._weekCache, _weekCache)&&(identical(other.rooms, rooms) || other.rooms == rooms)&&(identical(other.subjects, subjects) || other.subjects == subjects)&&(identical(other.schoolHolidays, schoolHolidays) || other.schoolHolidays == schoolHolidays)&&(identical(other.customEvents, customEvents) || other.customEvents == customEvents)&&(identical(other.startDate, startDate) || other.startDate == startDate)&&(identical(other.endDate, endDate) || other.endDate == endDate)&&(identical(other.dataVersion, dataVersion) || other.dataVersion == dataVersion));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_weekCache),rooms,subjects,schoolHolidays,customEvents,startDate,endDate,dataVersion);
@override
String toString() {
return 'TimetableState(weekCache: $weekCache, rooms: $rooms, subjects: $subjects, schoolHolidays: $schoolHolidays, customEvents: $customEvents, startDate: $startDate, endDate: $endDate, dataVersion: $dataVersion)';
}
}
/// @nodoc
abstract mixin class _$TimetableStateCopyWith<$Res> implements $TimetableStateCopyWith<$Res> {
factory _$TimetableStateCopyWith(_TimetableState value, $Res Function(_TimetableState) _then) = __$TimetableStateCopyWithImpl;
@override @useResult
$Res call({
Map<String, GetTimetableResponse> weekCache, GetRoomsResponse? rooms, GetSubjectsResponse? subjects, GetHolidaysResponse? schoolHolidays, GetCustomTimetableEventResponse? customEvents, DateTime startDate, DateTime endDate, int dataVersion
});
}
/// @nodoc
class __$TimetableStateCopyWithImpl<$Res>
implements _$TimetableStateCopyWith<$Res> {
__$TimetableStateCopyWithImpl(this._self, this._then);
final _TimetableState _self;
final $Res Function(_TimetableState) _then;
/// Create a copy of TimetableState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? weekCache = null,Object? rooms = freezed,Object? subjects = freezed,Object? schoolHolidays = freezed,Object? customEvents = freezed,Object? startDate = null,Object? endDate = null,Object? dataVersion = null,}) {
return _then(_TimetableState(
weekCache: null == weekCache ? _self._weekCache : weekCache // ignore: cast_nullable_to_non_nullable
as Map<String, GetTimetableResponse>,rooms: freezed == rooms ? _self.rooms : rooms // ignore: cast_nullable_to_non_nullable
as GetRoomsResponse?,subjects: freezed == subjects ? _self.subjects : subjects // ignore: cast_nullable_to_non_nullable
as GetSubjectsResponse?,schoolHolidays: freezed == schoolHolidays ? _self.schoolHolidays : schoolHolidays // ignore: cast_nullable_to_non_nullable
as GetHolidaysResponse?,customEvents: freezed == customEvents ? _self.customEvents : customEvents // ignore: cast_nullable_to_non_nullable
as GetCustomTimetableEventResponse?,startDate: null == startDate ? _self.startDate : startDate // ignore: cast_nullable_to_non_nullable
as DateTime,endDate: null == endDate ? _self.endDate : endDate // ignore: cast_nullable_to_non_nullable
as DateTime,dataVersion: null == dataVersion ? _self.dataVersion : dataVersion // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
// dart format on
@@ -0,0 +1,52 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'timetable_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_TimetableState _$TimetableStateFromJson(Map<String, dynamic> json) =>
_TimetableState(
weekCache:
(json['weekCache'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(
k,
GetTimetableResponse.fromJson(e as Map<String, dynamic>),
),
) ??
const <String, GetTimetableResponse>{},
rooms: json['rooms'] == null
? null
: GetRoomsResponse.fromJson(json['rooms'] as Map<String, dynamic>),
subjects: json['subjects'] == null
? null
: GetSubjectsResponse.fromJson(
json['subjects'] as Map<String, dynamic>,
),
schoolHolidays: json['schoolHolidays'] == null
? null
: GetHolidaysResponse.fromJson(
json['schoolHolidays'] as Map<String, dynamic>,
),
customEvents: json['customEvents'] == null
? null
: GetCustomTimetableEventResponse.fromJson(
json['customEvents'] as Map<String, dynamic>,
),
startDate: DateTime.parse(json['startDate'] as String),
endDate: DateTime.parse(json['endDate'] as String),
dataVersion: (json['dataVersion'] as num?)?.toInt() ?? 0,
);
Map<String, dynamic> _$TimetableStateToJson(_TimetableState instance) =>
<String, dynamic>{
'weekCache': instance.weekCache,
'rooms': instance.rooms,
'subjects': instance.subjects,
'schoolHolidays': instance.schoolHolidays,
'customEvents': instance.customEvents,
'startDate': instance.startDate.toIso8601String(),
'endDate': instance.endDate.toIso8601String(),
'dataVersion': instance.dataVersion,
};
@@ -0,0 +1,87 @@
import 'dart:async';
import 'package:intl/intl.dart';
import '../../../../../api/mhsl/customTimetableEvent/add/addCustomTimetableEvent.dart';
import '../../../../../api/mhsl/customTimetableEvent/add/addCustomTimetableEventParams.dart';
import '../../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
import '../../../../../api/mhsl/customTimetableEvent/get/getCustomTimetableEventCache.dart';
import '../../../../../api/mhsl/customTimetableEvent/get/getCustomTimetableEventParams.dart';
import '../../../../../api/mhsl/customTimetableEvent/get/getCustomTimetableEventResponse.dart';
import '../../../../../api/mhsl/customTimetableEvent/remove/removeCustomTimetableEvent.dart';
import '../../../../../api/mhsl/customTimetableEvent/remove/removeCustomTimetableEventParams.dart';
import '../../../../../api/mhsl/customTimetableEvent/update/updateCustomTimetableEvent.dart';
import '../../../../../api/mhsl/customTimetableEvent/update/updateCustomTimetableEventParams.dart';
import '../../../../../api/webuntis/queries/getHolidays/getHolidaysCache.dart';
import '../../../../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart';
import '../../../../../api/webuntis/queries/getRooms/getRoomsCache.dart';
import '../../../../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
import '../../../../../api/webuntis/queries/getSubjects/getSubjectsCache.dart';
import '../../../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart';
import '../../../../../api/webuntis/queries/getTimetable/getTimetableCache.dart';
import '../../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
import '../../../../../model/accountData.dart';
class TimetableDataProvider {
static final DateFormat _dateFormat = DateFormat('yyyyMMdd');
Future<GetTimetableResponse> getWeek(DateTime startDate, DateTime endDate) {
final completer = Completer<GetTimetableResponse>();
GetTimetableCache(
startdate: int.parse(_dateFormat.format(startDate)),
enddate: int.parse(_dateFormat.format(endDate)),
onUpdate: (data) {
if (!completer.isCompleted) completer.complete(data);
},
onError: (e) {
if (!completer.isCompleted) completer.completeError(e);
},
);
return completer.future;
}
Future<GetRoomsResponse> getRooms() {
final completer = Completer<GetRoomsResponse>();
GetRoomsCache(onUpdate: (data) {
if (!completer.isCompleted) completer.complete(data);
});
return completer.future;
}
Future<GetSubjectsResponse> getSubjects() {
final completer = Completer<GetSubjectsResponse>();
GetSubjectsCache(onUpdate: (data) {
if (!completer.isCompleted) completer.complete(data);
});
return completer.future;
}
Future<GetHolidaysResponse> getSchoolHolidays() {
final completer = Completer<GetHolidaysResponse>();
GetHolidaysCache(onUpdate: (data) {
if (!completer.isCompleted) completer.complete(data);
});
return completer.future;
}
Future<GetCustomTimetableEventResponse> getCustomEvents({bool renew = false}) {
final completer = Completer<GetCustomTimetableEventResponse>();
GetCustomTimetableEventCache(
GetCustomTimetableEventParams(AccountData().getUserSecret()),
renew: renew,
onUpdate: (data) {
if (!completer.isCompleted) completer.complete(data);
},
);
return completer.future;
}
Future<void> addCustomEvent(CustomTimetableEvent event) =>
AddCustomTimetableEvent(AddCustomTimetableEventParams(AccountData().getUserSecret(), event)).run();
Future<void> updateCustomEvent(String id, CustomTimetableEvent event) =>
UpdateCustomTimetableEvent(UpdateCustomTimetableEventParams(id, event)).run();
Future<void> removeCustomEvent(String id) =>
RemoveCustomTimetableEvent(RemoveCustomTimetableEventParams(id)).run();
}
@@ -0,0 +1,11 @@
import '../../../infrastructure/repository/repository.dart';
import '../bloc/timetable_state.dart';
import '../dataProvider/timetable_data_provider.dart';
class TimetableRepository extends Repository<TimetableState> {
final TimetableDataProvider _provider;
TimetableRepository([TimetableDataProvider? provider]) : _provider = provider ?? TimetableDataProvider();
TimetableDataProvider get data => _provider;
}
-80
View File
@@ -1,80 +0,0 @@
import 'dart:convert';
import 'dart:developer';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../view/settings/defaultSettings.dart';
import 'settings.dart';
class SettingsProvider extends ChangeNotifier {
static const String _fieldName = 'settings';
late SharedPreferences _storage;
late Settings _settings = DefaultSettings.get();
Settings val({bool write = false}) {
if(write) {
notifyListeners();
EasyDebounce.debounce(
_fieldName,
const Duration(milliseconds: 500),
update
);
}
return _settings;
}
SettingsProvider() {
_readFromStorage();
}
Future<void> reset() async {
_storage = await SharedPreferences.getInstance();
_storage.remove(_fieldName);
_settings = DefaultSettings.get();
await update();
notifyListeners();
}
Future<void> _readFromStorage() async {
_storage = await SharedPreferences.getInstance();
try {
_settings = Settings.fromJson(jsonDecode(_storage.getString(_fieldName)!));
} catch(exception) {
try {
log('Settings were changed, trying to recover from old Settings: ${exception.toString()}');
_settings = Settings.fromJson(_mergeSettings(jsonDecode(_storage.getString(_fieldName)!), DefaultSettings.get().toJson()));
log('Settings recovered successfully: ${_settings.toJson().toString()}');
} catch(exception) {
log('Settings are defective and not recoverable, using defaults: ${exception.toString()}');
_settings = DefaultSettings.get();
log('Settings were reset to defaults!');
}
}
notifyListeners();
}
Future<void> update() async {
await _storage.setString(_fieldName, jsonEncode(_settings.toJson()));
}
Map<String, dynamic> _mergeSettings(Map<String, dynamic> oldMap, Map<String, dynamic> newMap) {
var mergedMap = Map<String, dynamic>.from(newMap);
oldMap.forEach((key, value) {
if (mergedMap.containsKey(key)) {
if (value is Map<String, dynamic> && mergedMap[key] is Map<String, dynamic>) {
mergedMap[key] = _mergeSettings(value, mergedMap[key]);
} else {
mergedMap[key] = value;
}
}
});
return mergedMap;
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import '../../view/pages/timetable/timetableNameMode.dart'; import 'timetable_name_mode.dart';
part 'timetableSettings.g.dart'; part 'timetableSettings.g.dart';
@@ -1,25 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../widget/dropdownDisplay.dart'; import '../../widget/dropdownDisplay.dart';
enum TimetableNameMode { enum TimetableNameMode { name, longName, alternateName }
name,
longName,
alternateName
}
class TimetableNameModes { class TimetableNameModes {
static DropdownDisplay getDisplayOptions(TimetableNameMode theme) { static DropdownDisplay getDisplayOptions(TimetableNameMode mode) {
switch(theme) { switch (mode) {
case TimetableNameMode.name: case TimetableNameMode.name:
return DropdownDisplay(icon: Icons.device_unknown_outlined, displayName: 'Name'); return DropdownDisplay(icon: Icons.device_unknown_outlined, displayName: 'Name');
case TimetableNameMode.longName: case TimetableNameMode.longName:
return DropdownDisplay(icon: Icons.perm_device_info_outlined, displayName: 'Langname'); return DropdownDisplay(icon: Icons.perm_device_info_outlined, displayName: 'Langname');
case TimetableNameMode.alternateName: case TimetableNameMode.alternateName:
return DropdownDisplay(icon: Icons.on_device_training_outlined, displayName: 'Kurzform'); return DropdownDisplay(icon: Icons.on_device_training_outlined, displayName: 'Kurzform');
} }
} }
} }
+5 -4
View File
@@ -2,13 +2,14 @@
import 'dart:developer'; import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_login/flutter_login.dart'; import 'package:flutter_login/flutter_login.dart';
import 'package:provider/provider.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/accountData.dart';
import '../../model/accountModel.dart'; import '../../state/app/modules/account/bloc/account_bloc.dart';
import '../../state/app/modules/account/bloc/account_state.dart';
class Login extends StatefulWidget { class Login extends StatefulWidget {
const Login({super.key}); const Login({super.key});
@@ -20,7 +21,7 @@ class Login extends StatefulWidget {
class _LoginState extends State<Login> { class _LoginState extends State<Login> {
bool displayDisclaimerText = true; bool displayDisclaimerText = true;
String? _checkInput(value)=> (value ?? '').length == 0 ? 'Eingabe erforderlich' : null; String? _checkInput(String? value) => (value ?? '').isEmpty ? 'Eingabe erforderlich' : null;
Future<String?> _login(LoginData data) async { Future<String?> _login(LoginData data) async {
await AccountData().removeData(); await AccountData().removeData();
@@ -55,7 +56,7 @@ class _LoginState extends State<Login> {
userValidator: _checkInput, userValidator: _checkInput,
passwordValidator: _checkInput, passwordValidator: _checkInput,
onSubmitAnimationCompleted: () => Provider.of<AccountModel>(context, listen: false).setState(AccountModelState.loggedIn), onSubmitAnimationCompleted: () => context.read<AccountBloc>().setStatus(AccountStatus.loggedIn),
onLogin: _login, onLogin: _login,
onSignup: null, onSignup: null,
+157 -159
View File
@@ -1,32 +1,22 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:loader_overlay/loader_overlay.dart'; import 'package:loader_overlay/loader_overlay.dart';
import 'package:nextcloud/nextcloud.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:provider/provider.dart';
import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart'; import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
import '../../../api/marianumcloud/webdav/queries/listFiles/listFilesCache.dart'; import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../api/marianumcloud/webdav/queries/listFiles/listFilesResponse.dart'; import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../api/marianumcloud/webdav/webdavApi.dart'; import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../model/files/filesProps.dart'; import '../../../state/app/modules/files/bloc/files_bloc.dart';
import '../../../storage/base/settingsProvider.dart'; import '../../../state/app/modules/files/bloc/files_state.dart';
import '../../../widget/loadingSpinner.dart'; import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../widget/placeholderView.dart';
import '../../../widget/filePick.dart'; import '../../../widget/filePick.dart';
import '../../../widget/placeholderView.dart';
import 'fileElement.dart'; import 'fileElement.dart';
import 'filesUploadDialog.dart'; import 'filesUploadDialog.dart';
class Files extends StatefulWidget {
final List<String> path;
Files({List<String>? path, super.key}) : path = path ?? [];
@override
State<Files> createState() => _FilesState();
}
class BetterSortOption { class BetterSortOption {
String displayName; String displayName;
int Function(CacheableFile, CacheableFile) compare; int Function(CacheableFile, CacheableFile) compare;
@@ -35,111 +25,107 @@ class BetterSortOption {
BetterSortOption({required this.displayName, required this.icon, required this.compare}); BetterSortOption({required this.displayName, required this.icon, required this.compare});
} }
enum SortOption { enum SortOption { name, date, size }
name,
date,
size
}
class SortOptions { class SortOptions {
static Map<SortOption, BetterSortOption> options = { static Map<SortOption, BetterSortOption> options = {
SortOption.name: BetterSortOption( SortOption.name: BetterSortOption(
displayName: 'Name', displayName: 'Name',
icon: Icons.sort_by_alpha_outlined, icon: Icons.sort_by_alpha_outlined,
compare: (CacheableFile a, CacheableFile b) => a.name.compareTo(b.name) compare: (a, b) => a.name.compareTo(b.name),
), ),
SortOption.date: BetterSortOption( SortOption.date: BetterSortOption(
displayName: 'Datum', displayName: 'Datum',
icon: Icons.history_outlined, icon: Icons.history_outlined,
compare: (CacheableFile a, CacheableFile b) => a.modifiedAt!.compareTo(b.modifiedAt!) compare: (a, b) => a.modifiedAt!.compareTo(b.modifiedAt!),
), ),
SortOption.size: BetterSortOption( SortOption.size: BetterSortOption(
displayName: 'Größe', displayName: 'Größe',
icon: Icons.sd_card_outlined, icon: Icons.sd_card_outlined,
compare: (CacheableFile a, CacheableFile b) { compare: (a, b) {
if(a.isDirectory || b.isDirectory) return a.isDirectory ? 1 : 0; if (a.isDirectory || b.isDirectory) return a.isDirectory ? 1 : 0;
if(a.size == null) return 0; if (a.size == null) return 0;
if(b.size == null) return 1; if (b.size == null) return 1;
return a.size!.compareTo(b.size!); return a.size!.compareTo(b.size!);
} },
) ),
}; };
static BetterSortOption getOption(SortOption option) => options[option]!; static BetterSortOption getOption(SortOption option) => options[option]!;
} }
class _FilesState extends State<Files> { class Files extends StatelessWidget {
FilesProps props = FilesProps(); final List<String> path;
ListFilesResponse? data;
late SettingsProvider settings = Provider.of<SettingsProvider>(context, listen: false); Files({List<String>? path, super.key}) : path = path ?? [];
SortOption currentSort = SortOption.name; @override
bool currentSortDirection = true; Widget build(BuildContext context) => BlocModule<FilesBloc, LoadableState<FilesState>>(
create: (_) => FilesBloc(initialPath: path),
child: (context, _, _) => _FilesView(path: path),
);
}
class _FilesView extends StatefulWidget {
final List<String> path;
const _FilesView({required this.path});
@override
State<_FilesView> createState() => _FilesViewState();
}
class _FilesViewState extends State<_FilesView> {
late final SettingsCubit settings;
late SortOption currentSort;
late bool currentSortDirection;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
settings = context.read<SettingsCubit>();
currentSort = settings.val().fileSettings.sortBy; currentSort = settings.val().fileSettings.sortBy;
currentSortDirection = settings.val().fileSettings.ascending; currentSortDirection = settings.val().fileSettings.ascending;
_query();
}
void _query() {
ListFilesCache(
path: widget.path.isEmpty ? '/' : widget.path.join('/'),
onUpdate: (ListFilesResponse d) {
d.files.removeWhere((element) => element.name.isEmpty || element.name == widget.path.lastOrNull());
setState(() {
data = d;
});
}
);
} }
Future<void> mediaUpload(List<String>? paths) async { Future<void> mediaUpload(List<String>? paths) async {
if(paths == null) return; if (paths == null) return;
final bloc = context.read<FilesBloc>();
pushScreen( pushScreen(
context, context,
withNavBar: false, withNavBar: false,
screen: FilesUploadDialog(filePaths: paths, remotePath: widget.path.join('/'), onUploadFinished: (uploadedFilePaths) => _query()), screen: FilesUploadDialog(
filePaths: paths,
remotePath: widget.path.join('/'),
onUploadFinished: (_) => bloc.refresh(),
),
); );
return;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var files = data?.sortBy( final bloc = context.read<FilesBloc>();
sortOption: currentSort,
foldersToTop: Provider.of<SettingsProvider>(context).val().fileSettings.sortFoldersToTop,
reversed: currentSortDirection
) ?? List<CacheableFile>.empty();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(widget.path.isNotEmpty ? widget.path.last : 'Dateien'), title: Text(widget.path.isNotEmpty ? widget.path.last : 'Dateien'),
actions: [ actions: [
// IconButton(
// icon: const Icon(Icons.search),
// onPressed: () => {
// // TODO implement search
// },
// ),
PopupMenuButton<bool>( PopupMenuButton<bool>(
icon: Icon(currentSortDirection ? Icons.text_rotate_up : Icons.text_rotation_down), icon: Icon(currentSortDirection ? Icons.text_rotate_up : Icons.text_rotation_down),
itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>( itemBuilder: (context) => [true, false]
value: e, .map((e) => PopupMenuItem<bool>(
enabled: e != currentSortDirection, value: e,
child: Row( enabled: e != currentSortDirection,
children: [ child: Row(
Icon(e ? Icons.text_rotate_up : Icons.text_rotation_down, color: Theme.of(context).colorScheme.onSurface), children: [
const SizedBox(width: 15), Icon(
Text(e ? 'Aufsteigend' : 'Absteigend') e ? Icons.text_rotate_up : Icons.text_rotation_down,
], color: Theme.of(context).colorScheme.onSurface,
) ),
)).toList(), const SizedBox(width: 15),
Text(e ? 'Aufsteigend' : 'Absteigend'),
],
),
))
.toList(),
onSelected: (e) { onSelected: (e) {
setState(() { setState(() {
currentSortDirection = e; currentSortDirection = e;
@@ -149,17 +135,19 @@ class _FilesState extends State<Files> {
), ),
PopupMenuButton<SortOption>( PopupMenuButton<SortOption>(
icon: const Icon(Icons.sort), icon: const Icon(Icons.sort),
itemBuilder: (context) => SortOptions.options.keys.map((key) => PopupMenuItem<SortOption>( itemBuilder: (context) => SortOptions.options.keys
value: key, .map((key) => PopupMenuItem<SortOption>(
enabled: key != currentSort, value: key,
child: Row( enabled: key != currentSort,
children: [ child: Row(
Icon(SortOptions.getOption(key).icon, color: Theme.of(context).colorScheme.onSurface), children: [
const SizedBox(width: 15), Icon(SortOptions.getOption(key).icon, color: Theme.of(context).colorScheme.onSurface),
Text(SortOptions.getOption(key).displayName), const SizedBox(width: 15),
], Text(SortOptions.getOption(key).displayName),
) ],
)).toList(), ),
))
.toList(),
onSelected: (e) { onSelected: (e) {
setState(() { setState(() {
currentSort = e; currentSort = e;
@@ -172,81 +160,91 @@ class _FilesState extends State<Files> {
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'uploadFile', heroTag: 'uploadFile',
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
onPressed: () { onPressed: () => _showAddDialog(context, bloc),
showDialog(context: context, builder: (context) => SimpleDialog(
children: [
ListTile(
leading: const Icon(Icons.create_new_folder_outlined),
title: const Text('Ordner erstellen'),
onTap: () {
Navigator.of(context).pop();
showDialog(context: context, builder: (context) {
var inputController = TextEditingController();
return AlertDialog(
title: const Text('Neuer Ordner'),
content: TextField(
controller: inputController,
decoration: const InputDecoration(
labelText: 'Name',
),
),
actions: [
TextButton(onPressed: () {
Navigator.of(context).pop();
}, child: const Text('Abbrechen')),
TextButton(onPressed: () {
WebdavApi.webdav.then((webdav) {
webdav.mkcol(PathUri.parse("${widget.path.join("/")}/${inputController.text}")).then((value) => _query());
});
Navigator.of(context).pop();
}, child: const Text('Ordner erstellen')),
],
);
});
},
),
ListTile(
leading: const Icon(Icons.upload_file),
title: const Text('Aus Dateien hochladen'),
onTap: () {
FilePick.documentPick().then(mediaUpload);
Navigator.of(context).pop();
},
),
Visibility(
visible: !Platform.isIOS,
child: ListTile(
leading: const Icon(Icons.add_a_photo_outlined),
title: const Text('Aus Gallerie hochladen'),
onTap: () {
FilePick.multipleGalleryPick().then((value) {
if(value != null) mediaUpload(value.map((e) => e.path).toList());
});
Navigator.of(context).pop();
},
),
),
],
));
},
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),
body: data == null ? const LoadingSpinner() : data!.files.isEmpty ? const PlaceholderView(icon: Icons.folder_off_rounded, text: 'Der Ordner ist leer') : LoaderOverlay( body: LoadableStateConsumer<FilesBloc, FilesState>(
child: RefreshIndicator( child: (state, _) {
onRefresh: () { final listing = state.listing;
_query(); if (listing == null) return const SizedBox.shrink();
return Future.delayed(const Duration(seconds: 3)); if (listing.files.isEmpty) {
return const PlaceholderView(icon: Icons.folder_off_rounded, text: 'Der Ordner ist leer');
}
final files = listing.sortBy(
sortOption: currentSort,
foldersToTop: context.watch<SettingsCubit>().val().fileSettings.sortFoldersToTop,
reversed: currentSortDirection,
);
return LoaderOverlay(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: files.length,
itemBuilder: (context, index) => FileElement(files[index], widget.path, bloc.refresh),
),
);
},
),
);
}
void _showAddDialog(BuildContext context, FilesBloc bloc) {
showDialog(
context: context,
builder: (dialogCtx) => SimpleDialog(children: [
ListTile(
leading: const Icon(Icons.create_new_folder_outlined),
title: const Text('Ordner erstellen'),
onTap: () {
Navigator.of(dialogCtx).pop();
_showCreateFolderDialog(context, bloc);
}, },
child: ListView.builder( ),
padding: EdgeInsets.zero, ListTile(
itemCount: files.length, leading: const Icon(Icons.upload_file),
itemBuilder: (context, index) { title: const Text('Aus Dateien hochladen'),
var file = files.toList()[index]; onTap: () {
return FileElement(file, widget.path, _query); FilePick.documentPick().then(mediaUpload);
Navigator.of(dialogCtx).pop();
},
),
Visibility(
visible: !Platform.isIOS,
child: ListTile(
leading: const Icon(Icons.add_a_photo_outlined),
title: const Text('Aus Gallerie hochladen'),
onTap: () {
FilePick.multipleGalleryPick().then((value) {
if (value != null) mediaUpload(value.map((e) => e.path).toList());
});
Navigator.of(dialogCtx).pop();
}, },
), ),
) ),
) ]),
);
}
void _showCreateFolderDialog(BuildContext context, FilesBloc bloc) {
final inputController = TextEditingController();
showDialog(
context: context,
builder: (dialogCtx) => AlertDialog(
title: const Text('Neuer Ordner'),
content: TextField(
controller: inputController,
decoration: const InputDecoration(labelText: 'Name'),
),
actions: [
TextButton(onPressed: () => Navigator.of(dialogCtx).pop(), child: const Text('Abbrechen')),
TextButton(
onPressed: () {
bloc.createFolder(inputController.text);
Navigator.of(dialogCtx).pop();
},
child: const Text('Ordner erstellen'),
),
],
),
); );
} }
} }
+52 -13
View File
@@ -65,6 +65,28 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
); );
} }
void _showUploadError(String message) {
setState(() {
_isUploading = false;
_overallProgressValue = 0.0;
_infoText = '';
});
showDialog(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('Upload fehlgeschlagen'),
contentPadding: const EdgeInsets.all(10),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Schließen', textAlign: TextAlign.center),
),
],
),
);
}
Future<void> uploadFiles({bool override = false}) async { Future<void> uploadFiles({bool override = false}) async {
setState(() { setState(() {
_isUploading = true; _isUploading = true;
@@ -74,16 +96,24 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
} }
}); });
var webdavClient = await WebdavApi.webdav; final webdavClient = await WebdavApi.webdav;
if (!override) { if (!override) {
var result = (await webdavClient.propfind(PathUri.parse(widget.remotePath))).responses; List<dynamic> result;
try {
result = (await webdavClient.propfind(PathUri.parse(widget.remotePath))).responses;
} catch (e) {
if (!mounted) return;
_showUploadError('Verbindung fehlgeschlagen: $e');
return;
}
var conflictingFiles = _uploadableFiles.where((file) { var conflictingFiles = _uploadableFiles.where((file) {
var fileName = file.fileName; var fileName = file.fileName;
return result.any((element) => Uri.decodeComponent(element.href!).endsWith('/$fileName')); return result.any((element) => Uri.decodeComponent(element.href!).endsWith('/$fileName'));
}).toList(); }).toList();
if(conflictingFiles.isNotEmpty) { if(conflictingFiles.isNotEmpty) {
if (!mounted) return;
bool replaceFiles = await showDialog( bool replaceFiles = await showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@@ -157,17 +187,24 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
_infoText = '${_uploadableFiles.indexOf(file) + 1}/${_uploadableFiles.length}'; _infoText = '${_uploadableFiles.indexOf(file) + 1}/${_uploadableFiles.length}';
}); });
var uploadTask = await webdavClient.putFile( final dynamic uploadTask;
File(filePath), try {
FileStat.statSync(filePath), uploadTask = await webdavClient.putFile(
PathUri.parse(fullRemotePath), File(filePath),
onProgress: (progress) { FileStat.statSync(filePath),
setState(() { PathUri.parse(fullRemotePath),
file._uploadProgress = progress; onProgress: (progress) {
_overallProgressValue = ((progress + _uploadableFiles.indexOf(file)) / _uploadableFiles.length).toDouble(); setState(() {
}); file._uploadProgress = progress;
}, _overallProgressValue = ((progress + _uploadableFiles.indexOf(file)) / _uploadableFiles.length).toDouble();
); });
},
);
} catch (e) {
if (!mounted) return;
_showUploadError('Upload fehlgeschlagen für "$fileName": $e');
return;
}
if(uploadTask.statusCode < 200 || uploadTask.statusCode > 299) { if(uploadTask.statusCode < 200 || uploadTask.statusCode > 299) {
setState(() { setState(() {
@@ -175,6 +212,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
_overallProgressValue = 0.0; _overallProgressValue = 0.0;
_infoText = ''; _infoText = '';
}); });
if (!mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
showHttpErrorCode(uploadTask.statusCode); showHttpErrorCode(uploadTask.statusCode);
} else { } else {
@@ -187,6 +225,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
_overallProgressValue = 0.0; _overallProgressValue = 0.0;
_infoText = ''; _infoText = '';
}); });
if (!mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
widget.onUploadFinished(uploadetFilePaths); widget.onUploadFinished(uploadetFilePaths);
} }
@@ -6,13 +6,13 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:loader_overlay/loader_overlay.dart'; import 'package:loader_overlay/loader_overlay.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:badges/badges.dart' as badges; 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/accountData.dart';
import '../../../../storage/base/settingsProvider.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../widget/filePick.dart'; import '../../../../widget/filePick.dart';
import '../../../../widget/focusBehaviour.dart'; import '../../../../widget/focusBehaviour.dart';
import '../../../../widget/infoDialog.dart'; import '../../../../widget/infoDialog.dart';
@@ -113,7 +113,7 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
child: Visibility( child: Visibility(
visible: _error != null, visible: _error != null,
child: Visibility( child: Visibility(
visible: Provider.of<SettingsProvider>(context, listen: false).val().devToolsEnabled, visible: context.read<SettingsCubit>().val().devToolsEnabled,
replacement: const Text('Senden fehlgeschlagen, bitte überprüfe die Internetverbindung.', textAlign: TextAlign.center, style: TextStyle(color: Colors.red)), replacement: const Text('Senden fehlgeschlagen, bitte überprüfe die Internetverbindung.', textAlign: TextAlign.center, style: TextStyle(color: Colors.red)),
child: Text('Senden fehlgeschlagen: \n $_error', textAlign: TextAlign.center, style: const TextStyle(color: Colors.red)), child: Text('Senden fehlgeschlagen: \n $_error', textAlign: TextAlign.center, style: const TextStyle(color: Colors.red)),
), ),
@@ -156,13 +156,16 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
appVersion: int.parse((await PackageInfo.fromPlatform()).buildNumber), appVersion: int.parse((await PackageInfo.fromPlatform()).buildNumber),
) )
).run().then((value) { ).run().then((value) {
if (!context.mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
InfoDialog.show(context, 'Danke für dein Feedback!'); InfoDialog.show(context, 'Danke für dein Feedback!');
context.loaderOverlay.hide(); context.loaderOverlay.hide();
}).catchError((error, trace) { }).catchError((error, trace) {
if (!mounted) return;
setState(() { setState(() {
_error = error.toString(); _error = error.toString();
}); });
if (!context.mounted) return;
context.loaderOverlay.hide(); context.loaderOverlay.hide();
}); });
}, },
+1 -1
View File
@@ -13,7 +13,7 @@ class Roomplan extends StatelessWidget {
imageProvider: Image.asset('assets/img/raumplan.jpg').image, imageProvider: Image.asset('assets/img/raumplan.jpg').image,
minScale: 0.5, minScale: 0.5,
maxScale: 2.0, maxScale: 2.0,
backgroundDecoration: BoxDecoration(color: Theme.of(context).colorScheme.background), backgroundDecoration: BoxDecoration(color: Theme.of(context).colorScheme.surface),
), ),
); );
} }
@@ -8,7 +8,7 @@ class AppSharePlatformView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var foregroundColor = Theme.of(context).colorScheme.onBackground; var foregroundColor = Theme.of(context).colorScheme.onSurface;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -23,14 +23,14 @@ class SelectShareTypeDialog extends StatelessWidget {
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: () {
Share.share( SharePlus.instance.share(ShareParams(
sharePositionOrigin: SharePositionOrigin.get(context), sharePositionOrigin: SharePositionOrigin.get(context),
subject: 'App Teilen', subject: 'App Teilen',
'Hol dir die für das Marianum maßgeschneiderte App:' text: 'Hol dir die für das Marianum maßgeschneiderte App:'
'\n\nAndroid: https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client ' '\n\nAndroid: https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client '
'\niOS: https://apps.apple.com/us/app/marianum-fulda/id6458789560 ' '\niOS: https://apps.apple.com/us/app/marianum-fulda/id6458789560 '
'\n\nViel Spaß!' '\n\nViel Spaß!',
); ));
}, },
) )
], ],
+18 -7
View File
@@ -3,12 +3,13 @@ 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:provider/provider.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../extensions/renderNotNull.dart'; import '../../extensions/renderNotNull.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 '../../state/app/modules/app_modules.dart'; import '../../state/app/modules/app_modules.dart';
import '../../storage/base/settingsProvider.dart'; import '../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../storage/base/settings.dart' as model;
import '../../widget/centeredLeading.dart'; import '../../widget/centeredLeading.dart';
import '../../widget/infoDialog.dart'; import '../../widget/infoDialog.dart';
import '../settings/defaultSettings.dart'; import '../settings/defaultSettings.dart';
@@ -27,7 +28,9 @@ class _OverhangState extends State<Overhang> {
bool editMode = false; bool editMode = false;
@override @override
Widget build(BuildContext context) => Consumer<SettingsProvider>(builder: (context, settings, child) => Scaffold( Widget build(BuildContext context) => BlocBuilder<SettingsCubit, model.Settings>(builder: (context, _) {
final settings = context.read<SettingsCubit>();
return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Mehr'), title: const Text('Mehr'),
actions: [ actions: [
@@ -42,9 +45,11 @@ class _OverhangState extends State<Overhang> {
], ],
), ),
body: editMode ? _sorting() : _overhang(), body: editMode ? _sorting() : _overhang(),
)); );
});
Widget _sorting() => Consumer<SettingsProvider>(builder: (context, settings, child) { Widget _sorting() => BlocBuilder<SettingsCubit, model.Settings>(builder: (context, _) {
final settings = context.read<SettingsCubit>();
void changeVisibility(Modules module) { void changeVisibility(Modules module) {
var hidden = settings.val(write: true).modulesSettings.hiddenModules; var hidden = settings.val(write: true).modulesSettings.hiddenModules;
hidden.contains(module) ? hidden.remove(module) : (hidden.length < 3 ? hidden.add(module) : null); hidden.contains(module) ? hidden.remove(module) : (hidden.length < 3 ? hidden.add(module) : null);
@@ -107,8 +112,14 @@ class _OverhangState extends State<Overhang> {
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () { onTap: () {
InAppReview.instance.openStoreListing(appStoreId: '6458789560').then( InAppReview.instance.openStoreListing(appStoreId: '6458789560').then(
(value) => InfoDialog.show(context, 'Vielen Dank!'), (value) {
onError: (error) => InfoDialog.show(context, error.toString()) if (!context.mounted) return;
InfoDialog.show(context, 'Vielen Dank!');
},
onError: (error) {
if (!context.mounted) return;
InfoDialog.show(context, error.toString());
},
); );
}, },
); );
+78 -85
View File
@@ -1,79 +1,85 @@
import 'dart:async';
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 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_split_view/flutter_split_view.dart'; import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart';
import '../../../api/marianumcloud/talk/createRoom/createRoom.dart'; import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../api/marianumcloud/talk/createRoom/createRoomParams.dart'; import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../model/chatList/chatListProps.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 '../../../notification/notifyUpdater.dart';
import '../../../storage/base/settingsProvider.dart'; import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../widget/confirmDialog.dart'; import '../../../widget/confirmDialog.dart';
import '../../../widget/loadingSpinner.dart';
import 'components/chatTile.dart'; import 'components/chatTile.dart';
import 'components/splitViewPlaceholder.dart'; import 'components/splitViewPlaceholder.dart';
import 'joinChat.dart'; import 'joinChat.dart';
import 'searchChat.dart'; import 'searchChat.dart';
class ChatList extends StatefulWidget { class ChatList extends StatelessWidget {
const ChatList({super.key}); const ChatList({super.key});
@override @override
State<ChatList> createState() => _ChatListState(); Widget build(BuildContext context) => BlocModule<ChatListBloc, LoadableState<ChatListState>>(
create: (_) => ChatListBloc(),
child: (context, bloc, _) => const _ChatListView(),
);
} }
class _ChatListState extends State<ChatList> { class _ChatListView extends StatefulWidget {
late SettingsProvider settings; const _ChatListView();
@override
State<_ChatListView> createState() => _ChatListViewState();
}
class _ChatListViewState extends State<_ChatListView> {
late final SettingsCubit _settings;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
settings = Provider.of<SettingsProvider>(context, listen: false); _settings = context.read<SettingsCubit>();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((_) => _maybeAskForNotificationPermission());
_query();
if(!settings.val().notificationSettings.enabled && !settings.val().notificationSettings.askUsageDismissed) {
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) {
switch (value.authorizationStatus) {
case AuthorizationStatus.authorized:
NotifyUpdater.enableAfterDisclaimer(settings).asDialog(context);
break;
case AuthorizationStatus.denied:
showDialog(context: context, builder: (context) => const AlertDialog(
content: Text('Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren.'),
));
break;
default:
break;
}
});
},
).asDialog(context);
}
});
} }
void _query({bool renew = false}) { void _maybeAskForNotificationPermission() {
Provider.of<ChatListProps>(context, listen: false).run(renew: renew); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ChatListProps? latestData; final bloc = context.read<ChatListBloc>();
return SplitView.material( return SplitView.material(
placeholder: const SplitViewPlaceholder(), placeholder: const SplitViewPlaceholder(),
breakpoint: 1000, breakpoint: 1000,
@@ -83,63 +89,50 @@ class _ChatListState extends State<ChatList> {
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
onPressed: () async { onPressed: () {
if(latestData == null) return; final rooms = bloc.state.data?.rooms;
showSearch(context: context, delegate: SearchChat(latestData!.getRoomsResponse.data.toList())); if (rooms == null) return;
showSearch(context: context, delegate: SearchChat(rooms.data.toList()));
}, },
) ),
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'createChat', heroTag: 'createChat',
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
onPressed: () async { onPressed: () {
showSearch(context: context, delegate: JoinChat()).then((username) { showSearch(context: context, delegate: JoinChat()).then((username) {
if(username == null) return; if (username == null || !context.mounted) return;
ConfirmDialog( ConfirmDialog(
title: 'Chat starten', title: 'Chat starten',
content: "Möchtest du einen Chat mit Nutzer '$username' starten?", content: "Möchtest du einen Chat mit Nutzer '$username' starten?",
confirmButton: 'Chat starten', confirmButton: 'Chat starten',
onConfirm: () { onConfirm: () {
CreateRoom(CreateRoomParams( bloc.createDirectChat(username);
roomType: 1,
invite: username,
)).run().then((value) {
_query(renew: true);
});
}, },
).asDialog(context); ).asDialog(context);
}); });
}, },
child: const Icon(Icons.add_comment_outlined), child: const Icon(Icons.add_comment_outlined),
), ),
body: Consumer<ChatListProps>( body: LoadableStateConsumer<ChatListBloc, ChatListState>(
builder: (context, data, child) { child: (state, _) {
final rooms = state.rooms;
if (rooms == null) return const SizedBox.shrink();
if(data.primaryLoading()) return const LoadingSpinner(); final talkSettings = context.watch<SettingsCubit>().val().talkSettings;
latestData = data; final sorted = rooms.sortBy(
var chats = <ChatTile>[];
for (var chatRoom in data.getRoomsResponse.sortBy(
lastActivity: true, lastActivity: true,
favoritesToTop: Provider.of<SettingsProvider>(context).val().talkSettings.sortFavoritesToTop, favoritesToTop: talkSettings.sortFavoritesToTop,
unreadToTop: Provider.of<SettingsProvider>(context).val().talkSettings.sortUnreadToTop, unreadToTop: talkSettings.sortUnreadToTop,
) );
) {
var hasDraft = settings.val().talkSettings.drafts.containsKey(chatRoom.token);
chats.add(ChatTile(data: chatRoom, query: _query, hasDraft: hasDraft));
}
return RefreshIndicator( return ListView(
color: Theme.of(context).primaryColor, padding: EdgeInsets.zero,
onRefresh: () { children: sorted.map((room) {
_query(renew: true); final hasDraft = _settings.val().talkSettings.drafts.containsKey(room.token);
return Future.delayed(const Duration(seconds: 3)); return ChatTile(data: room, hasDraft: hasDraft);
}, }).toList(),
child: ListView(
padding: EdgeInsets.zero,
children: chats
),
); );
}, },
), ),
+57 -61
View File
@@ -1,12 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../extensions/dateTime.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.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 '../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../state/app/modules/chat/bloc/chat_state.dart';
import '../../../theming/appTheme.dart'; import '../../../theming/appTheme.dart';
import '../../../model/chatList/chatProps.dart';
import '../../../widget/clickableAppBar.dart'; import '../../../widget/clickableAppBar.dart';
import '../../../widget/loadingSpinner.dart'; import '../../../widget/loadingSpinner.dart';
import '../../../widget/userAvatar.dart'; import '../../../widget/userAvatar.dart';
@@ -27,66 +27,63 @@ class ChatView extends StatefulWidget {
} }
class _ChatViewState extends State<ChatView> { class _ChatViewState extends State<ChatView> {
final ScrollController _listController = ScrollController(); final ScrollController _listController = ScrollController();
@override void _refresh() {
void initState() { context.read<ChatBloc>().setToken(widget.room.token);
super.initState();
}
void _query({bool renew = false}) {
Provider.of<ChatProps>(context, listen: false).setQueryToken(widget.room.token);
} }
@override @override
Widget build(BuildContext context) => Consumer<ChatProps>( Widget build(BuildContext context) => BlocBuilder<ChatBloc, dynamic>(
builder: (context, data, child) { builder: (context, _) {
var messages = List<Widget>.empty(growable: true); final state = context.watch<ChatBloc>().state.data ?? const ChatState();
final response = state.chatResponse;
final isLoading = response == null;
if(!data.primaryLoading()) { final messages = <Widget>[];
if (response != null) {
var lastDate = DateTime.now(); var lastDate = DateTime.now();
data.getChatResponse.sortByTimestamp().forEach((element) { for (final element in response.sortByTimestamp()) {
var elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000); final elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000);
if(element.systemMessage.contains('reaction')) return; if (element.systemMessage.contains('reaction')) continue;
if(element.systemMessage.contains('poll_voted')) return; if (element.systemMessage.contains('poll_voted')) continue;
var commonRead = int.parse(data.getChatResponse.headers?['x-chat-last-common-read'] ?? '0'); final commonRead = int.parse(response.headers?['x-chat-last-common-read'] ?? '0');
if(!elementDate.isSameDay(lastDate)) { if (!elementDate.isSameDay(lastDate)) {
lastDate = elementDate; lastDate = elementDate;
messages.add(ChatBubble( messages.add(ChatBubble(
context: context, context: context,
isSender: false, isSender: false,
bubbleData: GetChatResponseObject.getDateDummy(element.timestamp), bubbleData: GetChatResponseObject.getDateDummy(element.timestamp),
chatData: widget.room, chatData: widget.room,
refetch: _query, refetch: ({bool renew = false}) => _refresh(),
)); ));
} }
messages.add(
ChatBubble(
context: context,
isSender: element.actorId == widget.selfId && element.messageType == GetRoomResponseObjectMessageType.comment,
bubbleData: element,
chatData: widget.room,
refetch: _query,
isRead: element.id <= commonRead,
selfId: widget.selfId,
)
);
});
if(data.getChatResponse.data.length >= 200) { messages.add(ChatBubble(
context: context,
isSender: element.actorId == widget.selfId &&
element.messageType == GetRoomResponseObjectMessageType.comment,
bubbleData: element,
chatData: widget.room,
refetch: ({bool renew = false}) => _refresh(),
isRead: element.id <= commonRead,
selfId: widget.selfId,
));
}
if (response.data.length >= 200) {
messages.insert(0, ChatBubble( messages.insert(0, ChatBubble(
context: context, context: context,
isSender: false, isSender: false,
bubbleData: GetChatResponseObject.getTextDummy( bubbleData: GetChatResponseObject.getTextDummy(
'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. ' 'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. '
'Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de' 'Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de',
), ),
chatData: widget.room, chatData: widget.room,
refetch: _query, refetch: ({bool renew = false}) => _refresh(),
)); ));
} }
} }
@@ -94,9 +91,7 @@ class _ChatViewState extends State<ChatView> {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xffefeae2), backgroundColor: const Color(0xffefeae2),
appBar: ClickableAppBar( appBar: ClickableAppBar(
onTap: () { onTap: () => TalkNavigator.pushSplitView(context, ChatInfo(widget.room)),
TalkNavigator.pushSplitView(context, ChatInfo(widget.room));
},
appBar: AppBar( appBar: AppBar(
title: Row( title: Row(
children: [ children: [
@@ -104,7 +99,7 @@ class _ChatViewState extends State<ChatView> {
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Text(widget.room.displayName, overflow: TextOverflow.ellipsis, maxLines: 1), child: Text(widget.room.displayName, overflow: TextOverflow.ellipsis, maxLines: 1),
) ),
], ],
), ),
), ),
@@ -117,26 +112,27 @@ class _ChatViewState extends State<ChatView> {
opacity: 1, opacity: 1,
repeat: ImageRepeat.repeat, repeat: ImageRepeat.repeat,
invertColors: AppTheme.isDarkMode(context), invertColors: AppTheme.isDarkMode(context),
) ),
), ),
child: data.primaryLoading() ? const LoadingSpinner() : Column( child: isLoading
children: [ ? const LoadingSpinner()
Expanded( : Column(
child: ListView( children: [
reverse: true, Expanded(
controller: _listController, child: ListView(
children: messages.reversed.toList(), reverse: true,
controller: _listController,
children: messages.reversed.toList(),
),
),
Container(
color: Theme.of(context).colorScheme.surface,
child: TalkNavigator.isSecondaryVisible(context)
? ChatTextfield(widget.room.token, selfId: widget.selfId)
: SafeArea(child: ChatTextfield(widget.room.token, selfId: widget.selfId)),
),
],
), ),
),
Container(
color: Theme.of(context).colorScheme.surface,
child: TalkNavigator.isSecondaryVisible(context)
? ChatTextfield(widget.room.token, selfId: widget.selfId)
: SafeArea(child: ChatTextfield(widget.room.token, selfId: widget.selfId)
),
)
],
),
), ),
); );
}, },
@@ -16,8 +16,8 @@ class AnswerReference extends StatelessWidget {
return DecoratedBox( return DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: referenceMessage.actorId == selfId color: referenceMessage.actorId == selfId
? style.getSelfStyle(false).color!.withGreen(200).withOpacity(0.2) ? style.getSelfStyle(false).color!.withGreen(200).withValues(alpha: 0.2)
: style.getRemoteStyle(false).color!.withWhite(200).withOpacity(0.2), : style.getRemoteStyle(false).color!.withWhite(200).withValues(alpha: 0.2),
borderRadius: const BorderRadius.all(Radius.circular(5)), borderRadius: const BorderRadius.all(Radius.circular(5)),
border: Border(left: BorderSide( border: Border(left: BorderSide(
color: referenceMessage.actorId == selfId color: referenceMessage.actorId == selfId
@@ -8,7 +8,7 @@ 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 '../../../../api/marianumcloud/talk/getPoll/getPollState.dart';
import '../../../../extensions/text.dart'; import '../../../../extensions/text.dart';
import 'package:provider/provider.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/deleteMessage/deleteMessage.dart';
@@ -17,7 +17,7 @@ import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessage
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 '../../../../model/chatList/chatProps.dart'; import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../widget/debug/debugTile.dart'; import '../../../../widget/debug/debugTile.dart';
import '../../../../widget/loadingSpinner.dart'; import '../../../../widget/loadingSpinner.dart';
import '../../files/fileElement.dart'; import '../../files/fileElement.dart';
@@ -189,9 +189,9 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
child: ListTile( child: ListTile(
leading: const Icon(Icons.reply_outlined), leading: const Icon(Icons.reply_outlined),
title: const Text('Antworten'), title: const Text('Antworten'),
onTap: () => { onTap: () {
Provider.of<ChatProps>(context, listen: false).setReferenceMessageId(widget.bubbleData.id, context, widget.chatData.token), context.read<ChatBloc>().setReferenceMessageId(widget.bubbleData.id);
Navigator.of(context).pop(), Navigator.of(context).pop();
}, },
), ),
), ),
@@ -236,7 +236,8 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
title: const Text('Nachricht löschen'), title: const Text('Nachricht löschen'),
onTap: () { onTap: () {
DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) { DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) {
Provider.of<ChatProps>(context, listen: false).run(); if (!context.mounted) return;
context.read<ChatBloc>().refresh();
Navigator.of(context).pop(); Navigator.of(context).pop();
}); });
}, },
@@ -294,7 +295,7 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
_position = const Offset(0, 0); _position = const Offset(0, 0);
}); });
if(widget.bubbleData.isReplyable && isAction) { if(widget.bubbleData.isReplyable && isAction) {
Provider.of<ChatProps>(context, listen: false).setReferenceMessageId(widget.bubbleData.id, context, widget.chatData.token); context.read<ChatBloc>().setReferenceMessageId(widget.bubbleData.id);
} }
}, },
onLongPress: showOptionsDialog, onLongPress: showOptionsDialog,
@@ -341,6 +342,7 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
TextButton(onPressed: () { TextButton(onPressed: () {
downloadCore?.then((value) { downloadCore?.then((value) {
if(!value.isCancelled) value.cancel(); if(!value.isCancelled) value.cancel();
if (!context.mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
}); });
setState(() { setState(() {
@@ -5,14 +5,16 @@ import '../../../../theming/appTheme.dart';
extension ColorExtensions on Color { extension ColorExtensions on Color {
Color invert() { Color invert() {
final r = 255 - red; final invertedR = 1.0 - r;
final g = 255 - green; final invertedG = 1.0 - g;
final b = 255 - blue; final invertedB = 1.0 - b;
return Color.from(alpha: a, red: invertedR, green: invertedG, blue: invertedB);
return Color.fromARGB((opacity * 255).round(), r, g, b);
} }
Color withWhite(int whiteValue) => Color.fromARGB(alpha, whiteValue, whiteValue, whiteValue); Color withWhite(int whiteValue) {
final value = whiteValue / 255.0;
return Color.from(alpha: a, red: value, green: value, blue: value);
}
} }
class ChatBubbleStyles { class ChatBubbleStyles {
+174 -166
View File
@@ -1,17 +1,17 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.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:provider/provider.dart';
import '../../../../api/marianumcloud/files-sharing/fileSharingApi.dart'; import '../../../../api/marianumcloud/files-sharing/fileSharingApi.dart';
import '../../../../api/marianumcloud/files-sharing/fileSharingApiParams.dart'; import '../../../../api/marianumcloud/files-sharing/fileSharingApiParams.dart';
import '../../../../api/marianumcloud/talk/sendMessage/sendMessage.dart'; import '../../../../api/marianumcloud/talk/sendMessage/sendMessage.dart';
import '../../../../api/marianumcloud/talk/sendMessage/sendMessageParams.dart'; import '../../../../api/marianumcloud/talk/sendMessage/sendMessageParams.dart';
import '../../../../api/marianumcloud/webdav/webdavApi.dart'; import '../../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../../model/chatList/chatProps.dart'; import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../storage/base/settingsProvider.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../widget/filePick.dart'; import '../../../../widget/filePick.dart';
import '../../../../widget/focusBehaviour.dart'; import '../../../../widget/focusBehaviour.dart';
import '../../files/filesUploadDialog.dart'; import '../../files/filesUploadDialog.dart';
@@ -20,6 +20,7 @@ import 'answerReference.dart';
class ChatTextfield extends StatefulWidget { class ChatTextfield extends StatefulWidget {
final String sendToToken; final String sendToToken;
final String? selfId; final String? selfId;
const ChatTextfield(this.sendToToken, {this.selfId, super.key}); const ChatTextfield(this.sendToToken, {this.selfId, super.key});
@override @override
@@ -27,207 +28,214 @@ class ChatTextfield extends StatefulWidget {
} }
class _ChatTextfieldState extends State<ChatTextfield> { class _ChatTextfieldState extends State<ChatTextfield> {
late SettingsProvider settings; late SettingsCubit settings;
final TextEditingController _textBoxController = TextEditingController(); final TextEditingController _textBoxController = TextEditingController();
bool isLoading = false; bool isLoading = false;
void _query() {
Provider.of<ChatProps>(context, listen: false).run();
}
void share(String shareFolder, List<String> filePaths) { void share(String shareFolder, List<String> filePaths) {
for (var element in filePaths) { for (final element in filePaths) {
var fileName = element.split(Platform.pathSeparator).last; final fileName = element.split(Platform.pathSeparator).last;
FileSharingApi().share(FileSharingApiParams( FileSharingApi().share(FileSharingApiParams(
shareType: 10, shareType: 10,
shareWith: widget.sendToToken, shareWith: widget.sendToToken,
path: '$shareFolder/$fileName', path: '$shareFolder/$fileName',
)).then((value) => _query()); )).then((_) {
if (mounted) context.read<ChatBloc>().refresh();
});
} }
} }
Future<void> mediaUpload(List<String>? paths) async { Future<void> mediaUpload(List<String>? paths) async {
if (paths == null) return; if (paths == null) return;
var shareFolder = 'MarianumMobile'; const shareFolder = 'MarianumMobile';
WebdavApi.webdav.then((webdav) { WebdavApi.webdav.then((webdav) => webdav.mkcol(PathUri.parse('/$shareFolder')));
webdav.mkcol(PathUri.parse('/$shareFolder'));
});
if (!mounted) return;
pushScreen( pushScreen(
context, context,
withNavBar: false, withNavBar: false,
screen: FilesUploadDialog( screen: FilesUploadDialog(
filePaths: paths, filePaths: paths,
remotePath: shareFolder, remotePath: shareFolder,
onUploadFinished: (uploadedFilePaths) { onUploadFinished: (uploaded) => share(shareFolder, uploaded),
share(shareFolder, uploadedFilePaths);
},
uniqueNames: true, uniqueNames: true,
), ),
); );
} }
void setDraft(String text) { void _setDraft(String text) {
if(text.isNotEmpty) { final talkSettings = settings.val(write: true).talkSettings;
settings.val(write: true).talkSettings.drafts[widget.sendToToken] = text; if (text.isNotEmpty) {
talkSettings.drafts[widget.sendToToken] = text;
} else { } else {
settings.val(write: true).talkSettings.drafts.removeWhere((key, value) => key == widget.sendToToken); talkSettings.drafts.removeWhere((key, _) => key == widget.sendToToken);
}
}
void _setDraftReply(int? messageId) {
final talkSettings = settings.val(write: true).talkSettings;
if (messageId != null) {
talkSettings.draftReplies[widget.sendToToken] = messageId;
} else {
talkSettings.draftReplies.removeWhere((key, _) => key == widget.sendToToken);
} }
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
settings = Provider.of<SettingsProvider>(context, listen: false); settings = context.read<SettingsCubit>();
Provider.of<ChatProps>(context, listen: false).unsafeInternalSetReferenceMessageId = final draftReply = settings.val().talkSettings.draftReplies[widget.sendToToken];
settings.val().talkSettings.draftReplies[widget.sendToToken]; if (draftReply != null) {
context.read<ChatBloc>().setReferenceMessageId(draftReply);
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_textBoxController.text = settings.val().talkSettings.drafts[widget.sendToToken] ?? ''; _textBoxController.text = settings.val().talkSettings.drafts[widget.sendToToken] ?? '';
final chatBloc = context.watch<ChatBloc>();
final chatState = chatBloc.state.data;
return Stack( Widget replyBanner = const SizedBox.shrink();
children: <Widget>[ if (chatState != null && chatState.referenceMessageId != null && chatState.chatResponse != null) {
Align( try {
alignment: Alignment.bottomLeft, final referenceMessage = chatState.chatResponse!.sortByTimestamp().firstWhere(
child: Container( (e) => e.id == chatState.referenceMessageId,
padding: const EdgeInsets.only(left: 10, bottom: 3, top: 3, right: 10), );
width: double.infinity, replyBanner = Row(
child: Column( children: [
children: [ Expanded(
Consumer<ChatProps>( child: AnswerReference(
builder: (context, data, child) { context: context,
if(data.getReferenceMessageId != null) { referenceMessage: referenceMessage,
var referenceMessage = data.getChatResponse.sortByTimestamp().where((element) => element.id == data.getReferenceMessageId).last; selfId: widget.selfId,
return Row( ),
children: [
Expanded(
child: AnswerReference(
context: context,
referenceMessage: referenceMessage,
selfId: widget.selfId,
),
),
IconButton(
onPressed: () => data.setReferenceMessageId(null, context, widget.sendToToken),
icon: const Icon(Icons.close_outlined),
padding: const EdgeInsets.only(left: 0),
),
],
);
} else {
return const SizedBox.shrink();
}
},
),
Row(
children: <Widget>[
GestureDetector(
onTap: (){
showDialog(context: context, builder: (context) => SimpleDialog(
children: [
ListTile(
leading: const Icon(Icons.file_open),
title: const Text('Aus Dateien auswählen'),
onTap: () {
FilePick.documentPick().then(mediaUpload);
Navigator.of(context).pop();
},
),
Visibility(
visible: !Platform.isIOS,
child: ListTile(
leading: const Icon(Icons.image),
title: const Text('Aus Gallerie auswählen'),
onTap: () {
FilePick.multipleGalleryPick().then((value) {
if(value != null) mediaUpload(value.map((e) => e.path).toList());
});
Navigator.of(context).pop();
},
),
),
],
));
},
child: Material(
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
child: Container(
height: 30,
width: 30,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
child: const Icon(Icons.attach_file_outlined, color: Colors.white, size: 20, ),
),
)
),
const SizedBox(width: 15),
Expanded(
child: TextField(
autocorrect: true,
textCapitalization: TextCapitalization.sentences,
controller: _textBoxController,
maxLines: 7,
minLines: 1,
decoration: const InputDecoration(
hintText: 'Nachricht schreiben...',
border: InputBorder.none,
),
onChanged: (String text) {
if(text.trim().toLowerCase() == 'marbot marbot marbot') {
var newText = 'Roboter sind cool und so, aber Marbots sind besser!';
_textBoxController.text = newText;
text = newText;
}
setDraft(text);
},
onTapOutside: (PointerDownEvent event) => FocusBehaviour.textFieldTapOutside(context),
),
),
const SizedBox(width: 15),
FloatingActionButton(
mini: true,
onPressed: () {
if(_textBoxController.text.isEmpty) return;
if(isLoading) return;
setState(() {
isLoading = true;
});
SendMessage(widget.sendToToken, SendMessageParams(
_textBoxController.text,
replyTo: Provider.of<ChatProps>(context, listen: false).getReferenceMessageId.toString()
)).run().then((value) {
_query();
setState(() {
isLoading = false;
});
_textBoxController.text = '';
setDraft('');
Provider.of<ChatProps>(context, listen: false).setReferenceMessageId(null, context, widget.sendToToken);
});
},
backgroundColor: Theme.of(context).primaryColor,
elevation: 5,
child: isLoading
? Container(padding: const EdgeInsets.all(10), child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: const Icon(Icons.send, color: Colors.white, size: 18),
),
],
),
],
), ),
IconButton(
onPressed: () {
chatBloc.setReferenceMessageId(null);
_setDraftReply(null);
},
icon: const Icon(Icons.close_outlined),
padding: const EdgeInsets.only(left: 0),
),
],
);
} catch (_) {/* reference no longer in current chat data */}
}
return Stack(children: <Widget>[
Align(
alignment: Alignment.bottomLeft,
child: Container(
padding: const EdgeInsets.only(left: 10, bottom: 3, top: 3, right: 10),
width: double.infinity,
child: Column(
children: [
replyBanner,
Row(children: <Widget>[
GestureDetector(
onTap: () {
showDialog(context: context, builder: (dialogCtx) => SimpleDialog(children: [
ListTile(
leading: const Icon(Icons.file_open),
title: const Text('Aus Dateien auswählen'),
onTap: () {
FilePick.documentPick().then(mediaUpload);
Navigator.of(dialogCtx).pop();
},
),
Visibility(
visible: !Platform.isIOS,
child: ListTile(
leading: const Icon(Icons.image),
title: const Text('Aus Gallerie auswählen'),
onTap: () {
FilePick.multipleGalleryPick().then((value) {
if (value != null) mediaUpload(value.map((e) => e.path).toList());
});
Navigator.of(dialogCtx).pop();
},
),
),
]));
},
child: Material(
elevation: 5,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
child: Container(
height: 30,
width: 30,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
child: const Icon(Icons.attach_file_outlined, color: Colors.white, size: 20),
),
),
),
const SizedBox(width: 15),
Expanded(
child: TextField(
autocorrect: true,
textCapitalization: TextCapitalization.sentences,
controller: _textBoxController,
maxLines: 7,
minLines: 1,
decoration: const InputDecoration(
hintText: 'Nachricht schreiben...',
border: InputBorder.none,
),
onChanged: (text) {
if (text.trim().toLowerCase() == 'marbot marbot marbot') {
const newText = 'Roboter sind cool und so, aber Marbots sind besser!';
_textBoxController.text = newText;
text = newText;
}
_setDraft(text);
},
onTapOutside: (_) => FocusBehaviour.textFieldTapOutside(context),
),
),
const SizedBox(width: 15),
FloatingActionButton(
mini: true,
onPressed: () {
if (_textBoxController.text.isEmpty || isLoading) return;
setState(() => isLoading = true);
SendMessage(
widget.sendToToken,
SendMessageParams(
_textBoxController.text,
replyTo: chatBloc.state.data?.referenceMessageId?.toString(),
),
).run().then((_) {
if (!mounted) return;
chatBloc.refresh();
setState(() => isLoading = false);
_textBoxController.text = '';
_setDraft('');
chatBloc.setReferenceMessageId(null);
_setDraftReply(null);
});
},
backgroundColor: Theme.of(context).primaryColor,
elevation: 5,
child: isLoading
? Container(
padding: const EdgeInsets.all(10),
child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
)
: const Icon(Icons.send, color: Colors.white, size: 18),
),
]),
],
), ),
), ),
], ),
); ]);
} }
} }
+150 -139
View File
@@ -1,8 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart'; import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
import '../../../../api/marianumcloud/talk/leaveRoom/leaveRoom.dart'; import '../../../../api/marianumcloud/talk/leaveRoom/leaveRoom.dart';
@@ -10,7 +9,9 @@ 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/chatList/chatProps.dart'; import '../../../../model/accountData.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
import '../../../../widget/confirmDialog.dart'; import '../../../../widget/confirmDialog.dart';
import '../../../../widget/debug/debugTile.dart'; import '../../../../widget/debug/debugTile.dart';
import '../../../../widget/userAvatar.dart'; import '../../../../widget/userAvatar.dart';
@@ -19,167 +20,177 @@ import '../talkNavigator.dart';
class ChatTile extends StatefulWidget { class ChatTile extends StatefulWidget {
final GetRoomResponseObject data; final GetRoomResponseObject data;
final void Function({bool renew}) query;
final bool disableContextActions; final bool disableContextActions;
final bool hasDraft; final bool hasDraft;
const ChatTile({super.key, required this.data, required this.query, this.disableContextActions = false, this.hasDraft = false}); const ChatTile({super.key, required this.data, this.disableContextActions = false, this.hasDraft = false});
@override @override
State<ChatTile> createState() => _ChatTileState(); State<ChatTile> createState() => _ChatTileState();
} }
class _ChatTileState extends State<ChatTile> { class _ChatTileState extends State<ChatTile> {
late String selfUsername; String? selfUsername;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
SharedPreferences.getInstance().then((value) => { AccountData().waitForPopulation().then((_) {
selfUsername = value.getString('username')! if (!mounted) return;
setState(() => selfUsername = AccountData().isPopulated() ? AccountData().getUsername() : null);
}); });
} }
void _refreshList() => context.read<ChatListBloc>().refresh();
void setCurrentAsRead() { void setCurrentAsRead() {
SetReadMarker( SetReadMarker(
widget.data.token, widget.data.token,
true, true,
setReadMarkerParams: SetReadMarkerParams( setReadMarkerParams: SetReadMarkerParams(lastReadMessage: widget.data.lastMessage.id),
lastReadMessage: widget.data.lastMessage.id ).run().then((_) {
) if (!mounted) return;
).run().then((value) => widget.query(renew: true)); _refreshList();
});
} }
@override @override
Widget build(BuildContext context) => Consumer<ChatProps>(builder: (context, chatData, child) { Widget build(BuildContext context) {
var isGroup = widget.data.type != GetRoomResponseObjectConversationType.oneToOne; final chatBloc = context.watch<ChatBloc>();
var circleAvatar = UserAvatar(id: isGroup ? widget.data.token : widget.data.name, isGroup: isGroup); final isGroup = widget.data.type != GetRoomResponseObjectConversationType.oneToOne;
final circleAvatar = UserAvatar(id: isGroup ? widget.data.token : widget.data.name, isGroup: isGroup);
return ListTile( return ListTile(
style: ListTileStyle.list, style: ListTileStyle.list,
tileColor: chatData.currentToken() == widget.data.token && TalkNavigator.isSecondaryVisible(context) tileColor: chatBloc.state.data?.currentToken == widget.data.token && TalkNavigator.isSecondaryVisible(context)
? Theme.of(context).primaryColor.withAlpha(100) ? Theme.of(context).primaryColor.withAlpha(100)
: null, : null,
leading: Stack( leading: Stack(
children: [
circleAvatar,
Visibility(
visible: widget.data.isFavorite,
child: Positioned(
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withAlpha(200),
borderRadius: BorderRadius.circular(90.0),
),
child: const Icon(Icons.star, color: Colors.amberAccent, size: 15),
),
),
)
],
),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: Text(widget.data.displayName, overflow: TextOverflow.ellipsis)),
if (widget.hasDraft) ...[
const SizedBox(width: 5),
const Icon(Icons.edit_outlined, size: 15),
],
],
),
subtitle: Text(
'${Jiffy.parseFromMillisecondsSinceEpoch(widget.data.lastMessage.timestamp * 1000).fromNow()}: '
'${RichObjectStringProcessor.parseToString(widget.data.lastMessage.message.replaceAll("\n", " "), widget.data.lastMessage.messageParameters)}',
overflow: TextOverflow.ellipsis,
),
trailing: widget.data.unreadMessages <= 0
? null
: Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
constraints: const BoxConstraints(minWidth: 20, minHeight: 20),
child: Text(
'${widget.data.unreadMessages}',
style: const TextStyle(color: Colors.white, fontSize: 15),
textAlign: TextAlign.center,
),
),
onTap: () {
if (selfUsername == null) return;
setCurrentAsRead();
final view = ChatView(room: widget.data, selfId: selfUsername!, avatar: circleAvatar);
TalkNavigator.pushSplitView(context, view, overrideToSingleSubScreen: true);
context.read<ChatBloc>().setToken(widget.data.token);
},
onLongPress: () {
if (widget.disableContextActions) return;
showDialog(context: context, builder: (dialogCtx) => SimpleDialog(
children: [ children: [
circleAvatar,
Visibility( Visibility(
visible: widget.data.isFavorite, visible: widget.data.unreadMessages > 0,
child: Positioned( replacement: ListTile(
right: 0, leading: const Icon(Icons.mark_chat_unread_outlined),
bottom: 0, title: const Text('Als ungelesen markieren'),
child: Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withAlpha(200),
borderRadius: BorderRadius.circular(90.0),
),
child: const Icon(Icons.star, color: Colors.amberAccent, size: 15),
),
),
)
],
),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(widget.data.displayName, overflow: TextOverflow.ellipsis),
),
if(widget.hasDraft) ...[
const SizedBox(width: 5),
const Icon(Icons.edit_outlined, size: 15),
],
],
),
subtitle: Text("${Jiffy.parseFromMillisecondsSinceEpoch(widget.data.lastMessage.timestamp * 1000).fromNow()}: ${RichObjectStringProcessor.parseToString(widget.data.lastMessage.message.replaceAll("\n", " "), widget.data.lastMessage.messageParameters)}", overflow: TextOverflow.ellipsis),
trailing: widget.data.unreadMessages <= 0
? null
: Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
constraints: const BoxConstraints(
minWidth: 20,
minHeight: 20,
),
child: Text(
'${widget.data.unreadMessages}',
style: const TextStyle(
color: Colors.white,
fontSize: 15,
),
textAlign: TextAlign.center,
),
),
onTap: () async {
setCurrentAsRead();
var view = ChatView(room: widget.data, selfId: selfUsername, avatar: circleAvatar);
TalkNavigator.pushSplitView(context, view, overrideToSingleSubScreen: true);
Provider.of<ChatProps>(context, listen: false).setQueryToken(widget.data.token);
},
onLongPress: () {
if(widget.disableContextActions) return;
showDialog(context: context, builder: (context) => SimpleDialog(
children: [
Visibility(
visible: widget.data.unreadMessages > 0,
replacement: ListTile(
leading: const Icon(Icons.mark_chat_unread_outlined),
title: const Text('Als ungelesen markieren'),
onTap: () {
SetReadMarker(widget.data.token, false).run().then((value) => widget.query(renew: true));
Navigator.of(context).pop();
},
),
child: ListTile(
leading: const Icon(Icons.mark_chat_read_outlined),
title: const Text('Als gelesen markieren'),
onTap: () {
setCurrentAsRead();
Navigator.of(context).pop();
},
),
),
Visibility(
visible: widget.data.isFavorite,
replacement: ListTile(
leading: const Icon(Icons.star_outline),
title: const Text('Zu Favoriten hinzufügen'),
onTap: () {
SetFavorite(widget.data.token, true).run().then((value) => widget.query(renew: true));
Navigator.of(context).pop();
},
),
child: ListTile(
leading: const Icon(Icons.stars_outlined),
title: const Text('Von Favoriten entfernen'),
onTap: () {
SetFavorite(widget.data.token, false).run().then((value) => widget.query(renew: true));
Navigator.of(context).pop();
},
),
),
ListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('Konversation verlassen'),
onTap: () { onTap: () {
ConfirmDialog( SetReadMarker(widget.data.token, false).run().then((_) {
title: 'Chat verlassen', if (mounted) _refreshList();
content: 'Du benötigst ggf. eine Einladung um erneut beizutreten.', });
confirmButton: 'Löschen', Navigator.of(dialogCtx).pop();
onConfirm: () {
LeaveRoom(widget.data.token).run().then((value) => widget.query(renew: true));
Navigator.of(context).pop();
},
).asDialog(context);
}, },
), ),
DebugTile(context).jsonData(widget.data.toJson()), child: ListTile(
], leading: const Icon(Icons.mark_chat_read_outlined),
)); title: const Text('Als gelesen markieren'),
}, onTap: () {
); setCurrentAsRead();
}); Navigator.of(dialogCtx).pop();
},
),
),
Visibility(
visible: widget.data.isFavorite,
replacement: ListTile(
leading: const Icon(Icons.star_outline),
title: const Text('Zu Favoriten hinzufügen'),
onTap: () {
SetFavorite(widget.data.token, true).run().then((_) {
if (mounted) _refreshList();
});
Navigator.of(dialogCtx).pop();
},
),
child: ListTile(
leading: const Icon(Icons.stars_outlined),
title: const Text('Von Favoriten entfernen'),
onTap: () {
SetFavorite(widget.data.token, false).run().then((_) {
if (mounted) _refreshList();
});
Navigator.of(dialogCtx).pop();
},
),
),
ListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('Konversation verlassen'),
onTap: () {
ConfirmDialog(
title: 'Chat verlassen',
content: 'Du benötigst ggf. eine Einladung um erneut beizutreten.',
confirmButton: 'Löschen',
onConfirm: () {
LeaveRoom(widget.data.token).run().then((_) {
if (mounted) _refreshList();
});
Navigator.of(dialogCtx).pop();
},
).asDialog(dialogCtx);
},
),
DebugTile(dialogCtx).jsonData(widget.data.toJson()),
],
));
},
);
}
} }
@@ -26,7 +26,7 @@ class _PollOptionsListState extends State<PollOptionsList> {
? (widget.pollData.votes['option-$optionId'] as num?) ?? 0 ? (widget.pollData.votes['option-$optionId'] as num?) ?? 0
: 0; : 0;
var numVoters = widget.pollData.numVoters ?? 0; var numVoters = widget.pollData.numVoters ?? 0;
double portion = numVoters == 0 ? 0 : (votes / numVoters); final portion = numVoters == 0 ? 0.0 : (votes / numVoters);
return ListTile( return ListTile(
// enabled: false, // enabled: false,
+1 -1
View File
@@ -25,7 +25,7 @@ class SearchChat extends SearchDelegate {
itemCount: items.length, itemCount: items.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
var item = items.elementAt(index); var item = items.elementAt(index);
return ChatTile(data: item, disableContextActions: true, query: ({bool renew = true}) {}); return ChatTile(data: item, disableContextActions: true);
}, },
); );
} }
@@ -1,92 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import 'CrossPainter.dart';
class AppointmentComponent extends StatefulWidget {
final CalendarAppointmentDetails details;
final bool crossedOut;
const AppointmentComponent({super.key, required this.details, this.crossedOut = false});
@override
State<AppointmentComponent> createState() => _AppointmentComponentState();
}
class _AppointmentComponentState extends State<AppointmentComponent> {
@override
Widget build(BuildContext context) {
final Appointment meeting = widget.details.appointments.first;
final appointmentHeight = widget.details.bounds.height;
return Stack(
children: [
Column(
children: [
Container(
padding: const EdgeInsets.all(3),
height: appointmentHeight,
alignment: Alignment.topLeft,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(5)),
color: meeting.color.withAlpha(meeting.endTime.isBefore(DateTime.now()) ? 100 : 255),
),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
child: Text(
meeting.subject,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w500,
),
maxLines: 1,
softWrap: false,
),
),
FittedBox(
fit: BoxFit.fitWidth,
child: Text(
(meeting.location == null || meeting.location!.isEmpty ? ' ' : meeting.location!),
maxLines: 3,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
)
],
),
),
),
],
),
Visibility(
visible: widget.crossedOut,
child: Positioned.fill(
child: Container(
decoration: BoxDecoration(
border: Border.all(
width: 2,
color: Colors.red.withAlpha(200),
),
borderRadius: const BorderRadius.all(Radius.circular(5)),
),
child: CustomPaint(
painter: CrossPainter(),
),
)
),
),
],
);
}
}
@@ -1,226 +0,0 @@
import 'dart:async';
import 'package:bottom_sheet/bottom_sheet.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:provider/provider.dart';
import 'package:rrule/rrule.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
import '../../../api/mhsl/customTimetableEvent/remove/removeCustomTimetableEvent.dart';
import '../../../api/mhsl/customTimetableEvent/remove/removeCustomTimetableEventParams.dart';
import '../../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
import '../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart';
import '../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
import '../../../model/timetable/timetableProps.dart';
import '../../../widget/centeredLeading.dart';
import '../../../widget/confirmDialog.dart';
import '../../../widget/debug/debugTile.dart';
import '../../../widget/unimplementedDialog.dart';
import '../more/roomplan/roomplan.dart';
import 'arbitraryAppointment.dart';
import 'customTimetableEventEditDialog.dart';
class AppointmentDetails {
static String _getEventPrefix(String? code) {
if(code == 'cancelled') return 'Entfällt: ';
if(code == 'irregular') return 'Änderung: ';
return code ?? '';
}
static void show(BuildContext context, TimetableProps webuntisData, Appointment appointment) {
(appointment.id! as ArbitraryAppointment).handlers(
(webuntis) => _webuntis(context, webuntisData, appointment, webuntis),
(customData) => _custom(context, webuntisData, customData)
);
}
static void _bottomSheet(
BuildContext context,
Widget Function(BuildContext context) header,
SliverChildListDelegate Function(BuildContext context) body
) {
showStickyFlexibleBottomSheet(
minHeight: 0,
initHeight: 0.4,
maxHeight: 0.7,
anchors: [0, 0.4, 0.7],
isSafeArea: true,
maxHeaderHeight: 100,
context: context,
headerBuilder: (context, bottomSheetOffset) => header(context),
bodyBuilder: (context, bottomSheetOffset) => body(context)
);
}
static void _webuntis(BuildContext context, TimetableProps webuntisData, Appointment appointment, GetTimetableResponseObject timetableData) {
GetSubjectsResponseObject subject;
GetRoomsResponseObject room;
try {
subject = webuntisData.getSubjectsResponse.result.firstWhere((subject) => subject.id == timetableData.su[0].id);
} catch(e) {
subject = GetSubjectsResponseObject(0, '?', 'Unbekannt', '?', true);
}
try {
room = webuntisData.getRoomsResponse.result.firstWhere((room) => room.id == timetableData.ro[0].id);
} catch(e) {
room = GetRoomsResponseObject(0, '?', 'Unbekannt', true, '?');
}
_bottomSheet(
context,
(context) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('${_getEventPrefix(timetableData.code)}${subject.alternateName}', textAlign: TextAlign.center, style: const TextStyle(fontSize: 25), overflow: TextOverflow.ellipsis),
Text(subject.longName),
Text("${Jiffy.parseFromDateTime(appointment.startTime).format(pattern: "HH:mm")} - ${Jiffy.parseFromDateTime(appointment.endTime).format(pattern: "HH:mm")}", style: const TextStyle(fontSize: 15)),
],
),
),
(context) => SliverChildListDelegate(
[
const Divider(),
ListTile(
leading: const Icon(Icons.notifications_active),
title: Text("Status: ${timetableData.code != null ? "Geändert" : "Regulär"}"),
),
ListTile(
leading: const Icon(Icons.room),
title: Text('Raum: ${room.name} (${room.longName})'),
trailing: IconButton(
icon: const Icon(Icons.house_outlined),
onPressed: () {
pushScreen(context, withNavBar: false, screen: const Roomplan());
},
),
),
ListTile(
leading: const Icon(Icons.person),
title: timetableData.te.isNotEmpty
? Text("Lehrkraft: ${timetableData.te[0].name} ${timetableData.te[0].longname.isNotEmpty ? "(${timetableData.te[0].longname})" : ""}")
: const Text('?'),
trailing: Visibility(
visible: !kReleaseMode,
child: IconButton(
icon: const Icon(Icons.textsms_outlined),
onPressed: () {
UnimplementedDialog.show(context);
},
),
),
),
ListTile(
leading: const Icon(Icons.abc),
title: Text('Typ: ${timetableData.activityType}'),
),
ListTile(
leading: const Icon(Icons.people),
title: Text("Klasse(n): ${timetableData.kl.map((e) => e.name).join(", ")}"),
),
DebugTile(context).jsonData(timetableData.toJson()),
],
)
);
}
static Completer deleteCustomEvent(BuildContext context, CustomTimetableEvent appointment) {
var future = Completer();
ConfirmDialog(
title: 'Termin löschen',
content: "Der ${appointment.rrule.isEmpty ? "Termin" : "Serientermin"} wird unwiederruflich gelöscht.",
confirmButton: 'Löschen',
onConfirm: () {
RemoveCustomTimetableEvent(
RemoveCustomTimetableEventParams(
appointment.id
)
).run().then((value) {
Provider.of<TimetableProps>(context, listen: false).run(renew: true);
future.complete();
});
},
).asDialog(context);
return future;
}
static void _custom(BuildContext context, TimetableProps webuntisData, CustomTimetableEvent appointment) {
_bottomSheet(
context,
(context) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(appointment.title, style: const TextStyle(fontSize: 25, overflow: TextOverflow.ellipsis)),
Text("${Jiffy.parseFromDateTime(appointment.startDate).format(pattern: "HH:mm")} - ${Jiffy.parseFromDateTime(appointment.endDate).format(pattern: "HH:mm")}", style: const TextStyle(fontSize: 15)),
],
),
),
(context) => SliverChildListDelegate(
[
const Divider(),
Center(
child: Wrap(
children: [
TextButton.icon(
onPressed: () {
Navigator.of(context).pop();
showDialog(
context: context,
builder: (context) => CustomTimetableEventEditDialog(existingEvent: appointment),
);
},
label: const Text('Bearbeiten'),
icon: const Icon(Icons.edit_outlined),
),
TextButton.icon(
onPressed: () {
deleteCustomEvent(context, appointment).future.then((value) => Navigator.of(context).pop());
},
label: const Text('Löschen'),
icon: const Icon(Icons.delete_outline),
),
],
),
),
const Divider(),
ListTile(
leading: const Icon(Icons.info_outline),
title: Text(appointment.description.isEmpty ? 'Keine Beschreibung' : appointment.description),
),
ListTile(
leading: const CenteredLeading(Icon(Icons.repeat_outlined)),
title: Text("Serie: ${appointment.rrule.isNotEmpty ? "Wiederholend" : "Einmailg"}"),
subtitle: FutureBuilder(
future: RruleL10nEn.create(),
builder: (context, snapshot) {
if(appointment.rrule.isEmpty) return const Text('Keine weiteren vorkomnisse');
if(snapshot.data == null) return const Text('...');
var rrule = RecurrenceRule.fromString(appointment.rrule);
if(!rrule.canFullyConvertToText) return const Text('Keine genauere Angabe möglich.');
return Text(rrule.toText(l10n: snapshot.data!));
},
)
),
DebugTile(context).child(
ListTile(
leading: const CenteredLeading(Icon(Icons.rule)),
title: const Text('RRule'),
subtitle: Text(appointment.rrule.isEmpty ? 'Keine' : appointment.rrule),
)
),
DebugTile(context).jsonData(appointment.toJson()),
]
)
);
}
}
@@ -1,19 +0,0 @@
import '../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
import '../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
class ArbitraryAppointment {
GetTimetableResponseObject? webuntis;
CustomTimetableEvent? custom;
ArbitraryAppointment({this.webuntis, this.custom});
bool hasWebuntis() => webuntis != null;
bool hasCustom() => custom != null;
void handlers(void Function(GetTimetableResponseObject webuntisData) webuntis, void Function(CustomTimetableEvent customData) custom) {
if(hasWebuntis()) webuntis(this.webuntis!);
if(hasCustom()) custom(this.custom!);
}
}
@@ -1,233 +0,0 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import '../../../extensions/dateTime.dart';
import 'package:provider/provider.dart';
import 'package:rrule_generator/rrule_generator.dart';
import 'package:time_range_picker/time_range_picker.dart';
import '../../../api/mhsl/customTimetableEvent/add/addCustomTimetableEvent.dart';
import '../../../api/mhsl/customTimetableEvent/add/addCustomTimetableEventParams.dart';
import '../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
import '../../../api/mhsl/customTimetableEvent/update/updateCustomTimetableEvent.dart';
import '../../../api/mhsl/customTimetableEvent/update/updateCustomTimetableEventParams.dart';
import '../../../model/accountData.dart';
import '../../../model/timetable/timetableProps.dart';
import '../../../widget/focusBehaviour.dart';
import '../../../widget/infoDialog.dart';
import 'customTimetableColors.dart';
class CustomTimetableEventEditDialog extends StatefulWidget {
final CustomTimetableEvent? existingEvent;
const CustomTimetableEventEditDialog({this.existingEvent, super.key});
@override
State<CustomTimetableEventEditDialog> createState() => _AddCustomTimetableEventDialogState();
}
class _AddCustomTimetableEventDialogState extends State<CustomTimetableEventEditDialog> {
late DateTime _date = widget.existingEvent?.startDate ?? DateTime.now();
late TimeOfDay _startTime = widget.existingEvent?.startDate.toTimeOfDay() ?? const TimeOfDay(hour: 08, minute: 00);
late TimeOfDay _endTime = widget.existingEvent?.endDate.toTimeOfDay() ?? const TimeOfDay(hour: 09, minute: 30);
late final TextEditingController _eventName = TextEditingController(text: widget.existingEvent?.title);
late final TextEditingController _eventDescription = TextEditingController(text: widget.existingEvent?.description);
late String _recurringRule = widget.existingEvent?.rrule ?? '';
late CustomTimetableColors _customTimetableColor = CustomTimetableColors.values.firstWhere(
(element) => element.name == widget.existingEvent?.color,
orElse: () => TimetableColors.defaultColor
);
late bool isEditingExisting = widget.existingEvent != null;
bool validate() {
if(_eventName.text.isEmpty) return false;
return true;
}
void fetchTimetable() {
Provider.of<TimetableProps>(context, listen: false).run(renew: true);
}
@override
Widget build(BuildContext context) => AlertDialog(
insetPadding: const EdgeInsets.all(20),
contentPadding: const EdgeInsets.all(10),
title: const Text('Termin hinzufügen'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
title: TextField(
controller: _eventName,
autofocus: true,
decoration: const InputDecoration(
labelText: 'Terminname',
border: OutlineInputBorder()
),
onTapOutside: (PointerDownEvent event) => FocusBehaviour.textFieldTapOutside(context),
),
),
ListTile(
title: TextField(
controller: _eventDescription,
maxLines: 2,
minLines: 2,
decoration: const InputDecoration(
labelText: 'Beschreibung',
border: OutlineInputBorder()
),
onTapOutside: (PointerDownEvent event) => FocusBehaviour.textFieldTapOutside(context),
),
),
const Divider(),
ListTile(
leading: const Icon(Icons.date_range_outlined),
title: Text(Jiffy.parseFromDateTime(_date).yMMMd),
subtitle: const Text('Datum'),
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate: _date,
firstDate: DateTime.now().subtract(const Duration(days: 30)),
lastDate: DateTime.now().add(const Duration(days: 30)),
);
if (pickedDate != null && pickedDate != _date) {
setState(() {
_date = pickedDate;
});
}
},
),
ListTile(
leading: const Icon(Icons.access_time_outlined),
title: Text('${_startTime.format(context).toString()} - ${_endTime.format(context).toString()}'),
subtitle: const Text('Zeitraum'),
onTap: () async {
TimeRange timeRange = await showTimeRangePicker(
context: context,
start: _startTime,
end: _endTime,
disabledTime: TimeRange(startTime: const TimeOfDay(hour: 16, minute: 30), endTime: const TimeOfDay(hour: 08, minute: 00)),
disabledColor: Colors.grey,
paintingStyle: PaintingStyle.fill,
interval: const Duration(minutes: 5),
fromText: 'Beginnend',
toText: 'Endend',
strokeColor: Theme.of(context).colorScheme.secondary,
minDuration: const Duration(minutes: 15),
selectedColor: Theme.of(context).primaryColor,
ticks: 24,
);
setState(() {
_startTime = timeRange.startTime;
_endTime = timeRange.endTime;
});
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.color_lens_outlined),
title: const Text('Farbgebung'),
trailing: DropdownButton<CustomTimetableColors>(
value: _customTimetableColor,
icon: const Icon(Icons.arrow_drop_down),
items: CustomTimetableColors.values.map((e) => DropdownMenuItem<CustomTimetableColors>(
value: e,
enabled: e != _customTimetableColor,
child: Row(
children: [
Icon(Icons.circle, color: TimetableColors.getDisplayOptions(e).color),
const SizedBox(width: 10),
Text(TimetableColors.getDisplayOptions(e).displayName),
],
),
)).toList(),
onChanged: (e) {
setState(() {
_customTimetableColor = e!;
});
},
),
),
const Divider(),
RRuleGenerator(
config: RRuleGeneratorConfig(
headerEnabled: true,
weekdayBackgroundColor: Theme.of(context).colorScheme.secondary,
weekdaySelectedBackgroundColor: Theme.of(context).primaryColor,
weekdayColor: Colors.black,
),
initialRRule: _recurringRule,
textDelegate: const GermanRRuleTextDelegate(),
onChange: (String newValue) {
log('Rule: $newValue');
setState(() {
_recurringRule = newValue;
});
},
)
],
),
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Abbrechen'),
),
TextButton(
onPressed: () {
if(!validate()) return;
var editedEvent = CustomTimetableEvent(
id: '',
title: _eventName.text,
description: _eventDescription.text,
startDate: _date.withTime(_startTime),
endDate: _date.withTime(_endTime),
color: _customTimetableColor.name,
rrule: _recurringRule,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
if(!isEditingExisting) {
AddCustomTimetableEvent(
AddCustomTimetableEventParams(
AccountData().getUserSecret(),
editedEvent
)
).run().then((value) {
Navigator.of(context).pop();
fetchTimetable();
})
.catchError((error, stack) {
InfoDialog.show(context, error.toString());
});
} else {
UpdateCustomTimetableEvent(
UpdateCustomTimetableEventParams(
widget.existingEvent?.id ?? '',
editedEvent
)
).run().then((value) {
Navigator.of(context).pop();
fetchTimetable();
})
.catchError((error, stack) {
InfoDialog.show(context, error.toString());
});
}
},
child: Text(isEditingExisting ? 'Speichern' : 'Erstellen'),
),
],
);
}
@@ -1,35 +1,30 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../theming/darkAppTheme.dart'; import '../../../../theming/darkAppTheme.dart';
enum CustomTimetableColors { enum CustomTimetableColors { orange, red, green, blue }
orange,
red,
green,
blue
}
class TimetableColors { class TimetableColors {
static const CustomTimetableColors defaultColor = CustomTimetableColors.orange; static const CustomTimetableColors defaultColor = CustomTimetableColors.orange;
static ColorModeDisplay getDisplayOptions(CustomTimetableColors color) { static ColorModeDisplay getDisplayOptions(CustomTimetableColors color) {
switch(color) { switch (color) {
case CustomTimetableColors.green: case CustomTimetableColors.green:
return ColorModeDisplay(color: Colors.green, displayName: 'Grün'); return ColorModeDisplay(color: Colors.green, displayName: 'Grün');
case CustomTimetableColors.blue: case CustomTimetableColors.blue:
return ColorModeDisplay(color: Colors.blue, displayName: 'Blau'); return ColorModeDisplay(color: Colors.blue, displayName: 'Blau');
case CustomTimetableColors.orange: case CustomTimetableColors.orange:
return ColorModeDisplay(color: Colors.orange.shade800, displayName: 'Orange'); return ColorModeDisplay(color: Colors.orange.shade800, displayName: 'Orange');
case CustomTimetableColors.red: case CustomTimetableColors.red:
return ColorModeDisplay(color: DarkAppTheme.marianumRed, displayName: 'Rot'); return ColorModeDisplay(color: DarkAppTheme.marianumRed, displayName: 'Rot');
} }
} }
static Color getColorFromString(String color) => getDisplayOptions(CustomTimetableColors.values.firstWhere((element) => element.name == color, orElse: () => TimetableColors.defaultColor)).color; static Color getColorFromString(String color) =>
getDisplayOptions(CustomTimetableColors.values.firstWhere(
(e) => e.name == color,
orElse: () => defaultColor,
)).color;
} }
class ColorModeDisplay { class ColorModeDisplay {
@@ -0,0 +1,190 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:jiffy/jiffy.dart';
import 'package:rrule_generator/rrule_generator.dart';
import 'package:time_range_picker/time_range_picker.dart';
import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
import '../../../../extensions/dateTime.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../../../widget/focusBehaviour.dart';
import '../../../../widget/infoDialog.dart';
import 'custom_event_colors.dart';
class CustomEventEditDialog extends StatefulWidget {
final CustomTimetableEvent? existingEvent;
const CustomEventEditDialog({this.existingEvent, super.key});
@override
State<CustomEventEditDialog> createState() => _CustomEventEditDialogState();
}
class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
late DateTime _date = widget.existingEvent?.startDate ?? DateTime.now();
late TimeOfDay _startTime = widget.existingEvent?.startDate.toTimeOfDay() ?? const TimeOfDay(hour: 8, minute: 0);
late TimeOfDay _endTime = widget.existingEvent?.endDate.toTimeOfDay() ?? const TimeOfDay(hour: 9, minute: 30);
late final TextEditingController _name = TextEditingController(text: widget.existingEvent?.title);
late final TextEditingController _description = TextEditingController(text: widget.existingEvent?.description);
late String _rrule = widget.existingEvent?.rrule ?? '';
late CustomTimetableColors _color = CustomTimetableColors.values.firstWhere(
(e) => e.name == widget.existingEvent?.color,
orElse: () => TimetableColors.defaultColor,
);
bool get _isEditing => widget.existingEvent != null;
bool _validate() => _name.text.isNotEmpty;
void _save() {
if (!_validate()) return;
final edited = CustomTimetableEvent(
id: widget.existingEvent?.id ?? '',
title: _name.text,
description: _description.text,
startDate: _date.withTime(_startTime),
endDate: _date.withTime(_endTime),
color: _color.name,
rrule: _rrule,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
final bloc = context.read<TimetableBloc>();
final future = _isEditing
? bloc.updateCustomEvent(widget.existingEvent!.id, edited)
: bloc.addCustomEvent(edited);
future.then((_) {
if (!mounted) return;
Navigator.of(context).pop();
}).catchError((Object error) {
if (!mounted) return;
InfoDialog.show(context, error.toString());
});
}
Future<void> _pickDate() async {
final picked = await showDatePicker(
context: context,
initialDate: _date,
firstDate: DateTime.now().subtract(const Duration(days: 30)),
lastDate: DateTime.now().add(const Duration(days: 30)),
);
if (picked != null && picked != _date) setState(() => _date = picked);
}
Future<void> _pickTimeRange() async {
final range = await showTimeRangePicker(
context: context,
start: _startTime,
end: _endTime,
disabledTime: TimeRange(
startTime: const TimeOfDay(hour: 16, minute: 30),
endTime: const TimeOfDay(hour: 8, minute: 0),
),
disabledColor: Colors.grey,
paintingStyle: PaintingStyle.fill,
interval: const Duration(minutes: 5),
fromText: 'Beginnend',
toText: 'Endend',
strokeColor: Theme.of(context).colorScheme.secondary,
minDuration: const Duration(minutes: 15),
selectedColor: Theme.of(context).primaryColor,
ticks: 24,
);
setState(() {
_startTime = range.startTime;
_endTime = range.endTime;
});
}
@override
Widget build(BuildContext context) => AlertDialog(
insetPadding: const EdgeInsets.all(20),
contentPadding: const EdgeInsets.all(10),
title: Text(_isEditing ? 'Termin bearbeiten' : 'Termin hinzufügen'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: TextField(
controller: _name,
autofocus: true,
decoration: const InputDecoration(labelText: 'Terminname', border: OutlineInputBorder()),
onTapOutside: (_) => FocusBehaviour.textFieldTapOutside(context),
),
),
ListTile(
title: TextField(
controller: _description,
maxLines: 2,
minLines: 2,
decoration: const InputDecoration(labelText: 'Beschreibung', border: OutlineInputBorder()),
onTapOutside: (_) => FocusBehaviour.textFieldTapOutside(context),
),
),
const Divider(),
ListTile(
leading: const Icon(Icons.date_range_outlined),
title: Text(Jiffy.parseFromDateTime(_date).yMMMd),
subtitle: const Text('Datum'),
onTap: _pickDate,
),
ListTile(
leading: const Icon(Icons.access_time_outlined),
title: Text('${_startTime.format(context)} - ${_endTime.format(context)}'),
subtitle: const Text('Zeitraum'),
onTap: _pickTimeRange,
),
const Divider(),
ListTile(
leading: const Icon(Icons.color_lens_outlined),
title: const Text('Farbgebung'),
trailing: DropdownButton<CustomTimetableColors>(
value: _color,
icon: const Icon(Icons.arrow_drop_down),
items: CustomTimetableColors.values
.map((e) => DropdownMenuItem<CustomTimetableColors>(
value: e,
enabled: e != _color,
child: Row(
children: [
Icon(Icons.circle, color: TimetableColors.getDisplayOptions(e).color),
const SizedBox(width: 10),
Text(TimetableColors.getDisplayOptions(e).displayName),
],
),
))
.toList(),
onChanged: (e) => setState(() => _color = e!),
),
),
const Divider(),
RRuleGenerator(
config: RRuleGeneratorConfig(
headerEnabled: true,
weekdayBackgroundColor: Theme.of(context).colorScheme.secondary,
weekdaySelectedBackgroundColor: Theme.of(context).primaryColor,
weekdayColor: Colors.black,
),
initialRRule: _rrule,
textDelegate: const GermanRRuleTextDelegate(),
onChange: (newValue) {
log('Rule: $newValue');
setState(() => _rrule = newValue);
},
),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Abbrechen')),
TextButton(onPressed: _save, child: Text(_isEditing ? 'Speichern' : 'Erstellen')),
],
);
}
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import '../../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_state.dart';
import '../../../../widget/centeredLeading.dart';
import '../../../../widget/placeholderView.dart';
import '../details/delete_custom_event.dart';
import 'custom_event_edit_dialog.dart';
class CustomEventsView extends StatelessWidget {
const CustomEventsView({super.key});
void _openCreateDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => const CustomEventEditDialog(),
barrierDismissible: false,
);
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Eigene Termine'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _openCreateDialog(context),
),
],
),
body: LoadableStateConsumer<TimetableBloc, TimetableState>(
child: (state, _) {
final events = state.customEvents?.events ?? const [];
if (events.isEmpty) {
return PlaceholderView(
icon: Icons.calendar_today_outlined,
text: 'Keine Einträge vorhanden',
button: TextButton(
onPressed: () => _openCreateDialog(context),
child: const Text('Termin erstellen'),
),
);
}
return ListView(
children: events.map((e) => ListTile(
title: Text(e.title),
subtitle: Text(
'${e.rrule.isNotEmpty ? "wiederholend, " : ""}'
'beginnend ${Jiffy.parseFromDateTime(e.startDate).fromNow()}',
),
leading: CenteredLeading(Icon(e.rrule.isEmpty ? Icons.event_outlined : Icons.event_repeat_outlined)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit_outlined),
onPressed: () => showDialog(
context: context,
builder: (_) => CustomEventEditDialog(existingEvent: e),
),
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => showDeleteCustomEventDialog(context, e),
),
],
),
)).toList(),
);
},
),
);
}
@@ -0,0 +1,24 @@
import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
import '../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
sealed class ArbitraryAppointment {
const ArbitraryAppointment();
T when<T>({
required T Function(GetTimetableResponseObject lesson) webuntis,
required T Function(CustomTimetableEvent event) custom,
}) => switch (this) {
WebuntisAppointment(:final lesson) => webuntis(lesson),
CustomAppointment(:final event) => custom(event),
};
}
class WebuntisAppointment extends ArbitraryAppointment {
final GetTimetableResponseObject lesson;
const WebuntisAppointment(this.lesson);
}
class CustomAppointment extends ArbitraryAppointment {
final CustomTimetableEvent event;
const CustomAppointment(this.event);
}
@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'lesson_status.dart';
class LessonColor {
static const Color cancelled = Color(0xff000000);
static const Color irregular = Color(0xff8F19B3);
static const Color teacherChanged = Color(0xFF29639B);
static const Color parseFallback = Color(0xff404040);
static Color forStatus(LessonStatus status, ColorScheme scheme) {
switch (status) {
case LessonStatus.cancelled:
return cancelled;
case LessonStatus.irregular:
return irregular;
case LessonStatus.teacherChanged:
return teacherChanged;
case LessonStatus.past:
case LessonStatus.regular:
return scheme.primary;
case LessonStatus.ongoing:
return Color.from(
alpha: scheme.primary.a,
red: 200 / 255,
green: scheme.primary.g,
blue: scheme.primary.b,
);
}
}
}
@@ -0,0 +1,21 @@
import '../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
enum LessonStatus {
cancelled,
irregular,
teacherChanged,
past,
ongoing,
regular,
}
class LessonStatusClassifier {
static LessonStatus classify(GetTimetableResponseObject lesson, DateTime startTime, DateTime endTime, DateTime now) {
if (lesson.code == 'cancelled') return LessonStatus.cancelled;
if (lesson.code == 'irregular' || (lesson.te.isNotEmpty && lesson.te.first.id == 0)) return LessonStatus.irregular;
if (lesson.te.any((t) => t.orgname != null)) return LessonStatus.teacherChanged;
if (endTime.isBefore(now)) return LessonStatus.past;
if (startTime.isBefore(now) && endTime.isAfter(now)) return LessonStatus.ongoing;
return LessonStatus.regular;
}
}

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