claude refactor
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
+2
-1
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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,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';
|
||||||
|
|
||||||
|
|||||||
+4
-11
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
@@ -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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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ß!',
|
||||||
);
|
));
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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());
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+8
-13
@@ -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
Reference in New Issue
Block a user