dart format
This commit is contained in:
@@ -3,7 +3,6 @@ import 'package:dio/dio.dart';
|
||||
import '../../infrastructure/data_loader/data_loader.dart';
|
||||
|
||||
abstract class HolidayDataLoader<TResult> extends DataLoader<TResult> {
|
||||
HolidayDataLoader() : super(Dio(BaseOptions(
|
||||
baseUrl: 'https://ferien-api.de/api/v1/',
|
||||
)));
|
||||
HolidayDataLoader()
|
||||
: super(Dio(BaseOptions(baseUrl: 'https://ferien-api.de/api/v1/')));
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import 'package:dio/dio.dart';
|
||||
import '../../infrastructure/data_loader/data_loader.dart';
|
||||
|
||||
abstract class MhslDataLoader<TResult> extends DataLoader<TResult> {
|
||||
MhslDataLoader() : super(Dio(BaseOptions(
|
||||
baseUrl: 'https://mhsl.eu/marianum/marianummobile/'
|
||||
)));
|
||||
MhslDataLoader()
|
||||
: super(
|
||||
Dio(BaseOptions(baseUrl: 'https://mhsl.eu/marianum/marianummobile/')),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,10 +14,14 @@ abstract class DataLoader<TResult> {
|
||||
Future<TResult> run() async {
|
||||
final response = await fetch();
|
||||
try {
|
||||
return assemble(DataLoaderResult(
|
||||
json: jsonDecode(response.data!),
|
||||
headers: response.headers.map.map((key, value) => MapEntry(key, value.join(';'))),
|
||||
));
|
||||
return assemble(
|
||||
DataLoaderResult(
|
||||
json: jsonDecode(response.data!),
|
||||
headers: response.headers.map.map(
|
||||
(key, value) => MapEntry(key, value.join(';')),
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
log('DataLoader assemble failed', error: e, stackTrace: stack);
|
||||
rethrow;
|
||||
@@ -34,7 +38,8 @@ class DataLoaderResult {
|
||||
|
||||
Map<String, dynamic> asMap() => json as Map<String, dynamic>;
|
||||
List<dynamic> asList() => json as List<dynamic>;
|
||||
List<Map<String, dynamic>> asListOfMaps() => asList().map((e) => e as Map<String, dynamic>).toList();
|
||||
List<Map<String, dynamic>> asListOfMaps() =>
|
||||
asList().map((e) => e as Map<String, dynamic>).toList();
|
||||
|
||||
DataLoaderResult({required this.json, required this.headers});
|
||||
}
|
||||
|
||||
@@ -21,16 +21,19 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState>
|
||||
LoadableStateBloc() : super(const LoadableStateState(connections: null)) {
|
||||
on<ConnectivityChanged>((event, emit) {
|
||||
emit(event.state);
|
||||
if(connectivityStatusKnown() && isConnected()) {
|
||||
if(reFetch == null) return;
|
||||
if (connectivityStatusKnown() && isConnected()) {
|
||||
if (reFetch == null) return;
|
||||
reFetch!();
|
||||
}
|
||||
});
|
||||
|
||||
void emitConnectivity(List<ConnectivityResult> result) => add(ConnectivityChanged(LoadableStateState(connections: result)));
|
||||
void emitConnectivity(List<ConnectivityResult> result) =>
|
||||
add(ConnectivityChanged(LoadableStateState(connections: result)));
|
||||
|
||||
Connectivity().checkConnectivity().then(emitConnectivity);
|
||||
_updateStream = Connectivity().onConnectivityChanged.listen(emitConnectivity);
|
||||
_updateStream = Connectivity().onConnectivityChanged.listen(
|
||||
emitConnectivity,
|
||||
);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@@ -38,42 +41,51 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState>
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state != AppLifecycleState.resumed) return;
|
||||
final now = DateTime.now();
|
||||
if (now.difference(_lastResumeRefetch) < const Duration(seconds: 10)) return;
|
||||
if (now.difference(_lastResumeRefetch) < const Duration(seconds: 10)) {
|
||||
return;
|
||||
}
|
||||
_lastResumeRefetch = now;
|
||||
// Re-check connectivity. The resulting [ConnectivityChanged] event takes
|
||||
// it from there: its handler updates the offline/online indicator and
|
||||
// triggers [reFetch] when the device is connected, so a stale
|
||||
// "Verbindung fehlgeschlagen" bar from a suspend-time fetch clears as
|
||||
// soon as the network is reachable again.
|
||||
unawaited(Connectivity().checkConnectivity().then(
|
||||
(result) => add(ConnectivityChanged(LoadableStateState(connections: result))),
|
||||
));
|
||||
unawaited(
|
||||
Connectivity().checkConnectivity().then(
|
||||
(result) =>
|
||||
add(ConnectivityChanged(LoadableStateState(connections: result))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool connectivityStatusKnown() => state.connections != null;
|
||||
bool isConnected() => !(state.connections?.contains(ConnectivityResult.none) ?? true);
|
||||
bool isConnected() =>
|
||||
!(state.connections?.contains(ConnectivityResult.none) ?? true);
|
||||
bool allowRetry() => reFetch != null;
|
||||
|
||||
IconData connectionIcon() => connectivityStatusKnown()
|
||||
? isConnected()
|
||||
? Icons.nearby_error
|
||||
: Icons.signal_wifi_connected_no_internet_4
|
||||
? Icons.nearby_error
|
||||
: Icons.signal_wifi_connected_no_internet_4
|
||||
: Icons.device_unknown;
|
||||
|
||||
Color connectionColor(BuildContext context) => connectivityStatusKnown() && !isConnected()
|
||||
Color connectionColor(BuildContext context) =>
|
||||
connectivityStatusKnown() && !isConnected()
|
||||
? Colors.grey.shade600
|
||||
: Theme.of(context).primaryColor;
|
||||
|
||||
Color connectionForegroundColor(BuildContext context) => connectivityStatusKnown() && !isConnected()
|
||||
Color connectionForegroundColor(BuildContext context) =>
|
||||
connectivityStatusKnown() && !isConnected()
|
||||
? Colors.white
|
||||
: ThemeData.estimateBrightnessForColor(Theme.of(context).primaryColor) == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black;
|
||||
: ThemeData.estimateBrightnessForColor(Theme.of(context).primaryColor) ==
|
||||
Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black;
|
||||
|
||||
String connectionText({int? lastUpdated}) => connectivityStatusKnown()
|
||||
? isConnected()
|
||||
? 'Verbindung fehlgeschlagen'
|
||||
: 'Offline${lastUpdated == null ? '' : ' - Stand von ${DateTime.fromMillisecondsSinceEpoch(lastUpdated).formatRelative()}'}'
|
||||
? 'Verbindung fehlgeschlagen'
|
||||
: 'Offline${lastUpdated == null ? '' : ' - Stand von ${DateTime.fromMillisecondsSinceEpoch(lastUpdated).formatRelative()}'}'
|
||||
: 'Unbekannte Fehlerursache';
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'loadable_state_state.dart';
|
||||
|
||||
sealed class LoadableStateEvent {}
|
||||
|
||||
final class ConnectivityChanged extends LoadableStateEvent {
|
||||
final LoadableStateState state;
|
||||
ConnectivityChanged(this.state);
|
||||
|
||||
+11
-10
@@ -8,14 +8,15 @@ class LoadableStateBackgroundLoading extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AnimatedSwitcher(
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) => SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, -1.0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
child: visible ? const LinearProgressIndicator() : const SizedBox.shrink(),
|
||||
);
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) =>
|
||||
SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, -1.0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
child: visible ? const LinearProgressIndicator() : const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@ import 'loadable_state_error_bar.dart';
|
||||
import 'loadable_state_error_screen.dart';
|
||||
import 'loadable_state_primary_loading.dart';
|
||||
|
||||
class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>>, TState> extends StatelessWidget {
|
||||
class LoadableStateConsumer<
|
||||
TController
|
||||
extends Bloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>>,
|
||||
TState
|
||||
>
|
||||
extends StatelessWidget {
|
||||
final Widget Function(TState state, bool loading) child;
|
||||
final void Function(TState state)? onLoad;
|
||||
final bool wrapWithScrollView;
|
||||
@@ -39,12 +44,16 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
var loadableState = context.watch<TController>().state;
|
||||
|
||||
final loadedData = loadableState.data;
|
||||
if(!loadableState.isLoading && onLoad != null && loadedData is TState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => onLoad!(loadedData));
|
||||
if (!loadableState.isLoading && onLoad != null && loadedData is TState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(timeStamp) => onLoad!(loadedData),
|
||||
);
|
||||
}
|
||||
|
||||
final typedData = loadedData is TState ? loadedData : null;
|
||||
final hasContent = typedData != null && (isReady?.call(typedData) ?? loadableState.showContent());
|
||||
final hasContent =
|
||||
typedData != null &&
|
||||
(isReady?.call(typedData) ?? loadableState.showContent());
|
||||
final hasError = loadableState.error != null;
|
||||
final isLoading = loadableState.isLoading;
|
||||
|
||||
@@ -57,23 +66,23 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
condition: loadableState.reFetch != null,
|
||||
wrapper: (child) => RefreshIndicator(
|
||||
onRefresh: () {
|
||||
if(loadableState.reFetch != null) loadableState.reFetch!();
|
||||
if (loadableState.reFetch != null) loadableState.reFetch!();
|
||||
return Future.value();
|
||||
},
|
||||
child: ConditionalWrapper(
|
||||
condition: wrapWithScrollView,
|
||||
wrapper: (child) => SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: child
|
||||
child: child,
|
||||
),
|
||||
child: child,
|
||||
)
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: hasContent
|
||||
? child(typedData as TState, isLoading)
|
||||
: const SizedBox.shrink(),
|
||||
? child(typedData as TState, isLoading)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -94,7 +103,9 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
child: Stack(
|
||||
children: [
|
||||
LoadableStatePrimaryLoading(visible: showPrimaryLoading),
|
||||
LoadableStateBackgroundLoading(visible: showBackgroundLoading),
|
||||
LoadableStateBackgroundLoading(
|
||||
visible: showBackgroundLoading,
|
||||
),
|
||||
LoadableStateErrorScreen(
|
||||
visible: showError,
|
||||
message: loadableState.error?.message,
|
||||
@@ -109,10 +120,10 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,48 +26,57 @@ class LoadableStateErrorBar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.watch<LoadableStateBloc>();
|
||||
final isOfflineWithCache = hasContent && bloc.connectivityStatusKnown() && !bloc.isConnected();
|
||||
final isOfflineWithCache =
|
||||
hasContent && bloc.connectivityStatusKnown() && !bloc.isConnected();
|
||||
final shouldShow = visible || isOfflineWithCache;
|
||||
|
||||
return AnimatedSize(
|
||||
duration: animationDuration,
|
||||
child: AnimatedSwitcher(
|
||||
duration: animationDuration,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) => SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, -1.0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
child: Visibility(
|
||||
key: Key(shouldShow.hashCode.toString()),
|
||||
visible: shouldShow,
|
||||
replacement: const SizedBox(width: double.infinity),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
var bloc = context.watch<LoadableStateBloc>();
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if(!bloc.isConnected()) return;
|
||||
final body = [
|
||||
if (message != null && message!.isNotEmpty) message!,
|
||||
if (technicalDetails != null && technicalDetails!.isNotEmpty) technicalDetails!,
|
||||
].join('\n\n');
|
||||
if (body.isEmpty) return;
|
||||
InfoDialog.show(context, body, copyable: true, title: 'Fehlerdetails');
|
||||
},
|
||||
child: Container(
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: bloc.connectionColor(context),
|
||||
),
|
||||
child: LoadableStateErrorBarText(lastUpdated: lastUpdated),
|
||||
),
|
||||
duration: animationDuration,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) =>
|
||||
SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, -1.0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
child: Visibility(
|
||||
key: Key(shouldShow.hashCode.toString()),
|
||||
visible: shouldShow,
|
||||
replacement: const SizedBox(width: double.infinity),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
var bloc = context.watch<LoadableStateBloc>();
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (!bloc.isConnected()) return;
|
||||
final body = [
|
||||
if (message != null && message!.isNotEmpty) message!,
|
||||
if (technicalDetails != null &&
|
||||
technicalDetails!.isNotEmpty)
|
||||
technicalDetails!,
|
||||
].join('\n\n');
|
||||
if (body.isEmpty) return;
|
||||
InfoDialog.show(
|
||||
context,
|
||||
body,
|
||||
copyable: true,
|
||||
title: 'Fehlerdetails',
|
||||
);
|
||||
},
|
||||
)
|
||||
)
|
||||
child: Container(
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: bloc.connectionColor(context),
|
||||
),
|
||||
child: LoadableStateErrorBarText(lastUpdated: lastUpdated),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -78,14 +87,18 @@ class LoadableStateErrorBarText extends StatefulWidget {
|
||||
const LoadableStateErrorBarText({required this.lastUpdated, super.key});
|
||||
|
||||
@override
|
||||
State<LoadableStateErrorBarText> createState() => _LoadableStateErrorBarTextState();
|
||||
State<LoadableStateErrorBarText> createState() =>
|
||||
_LoadableStateErrorBarTextState();
|
||||
}
|
||||
|
||||
class _LoadableStateErrorBarTextState extends State<LoadableStateErrorBarText> {
|
||||
late Timer _rebuildTimer;
|
||||
@override
|
||||
void initState() {
|
||||
_rebuildTimer = Timer.periodic(const Duration(seconds: 10), (timer) => setState(() {}));
|
||||
_rebuildTimer = Timer.periodic(
|
||||
const Duration(seconds: 10),
|
||||
(timer) => setState(() {}),
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,52 +20,66 @@ class LoadableStateErrorScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.watch<LoadableStateBloc>();
|
||||
final isOffline = bloc.connectivityStatusKnown() && !bloc.isConnected();
|
||||
final headline = isOffline ? bloc.connectionText() : (message ?? bloc.connectionText());
|
||||
final headline = isOffline
|
||||
? bloc.connectionText()
|
||||
: (message ?? bloc.connectionText());
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: !visible ? null : Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(bloc.connectionIcon(), size: 40),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
headline,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
textAlign: TextAlign.center,
|
||||
child: !visible
|
||||
? null
|
||||
: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(bloc.connectionIcon(), size: 40),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
headline,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (!isOffline &&
|
||||
message != null &&
|
||||
message != headline) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
if (bloc.allowRetry()) ...[
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () => bloc.reFetch!(),
|
||||
child: const Text('Erneut versuchen'),
|
||||
),
|
||||
],
|
||||
if (technicalDetails != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
TextButton(
|
||||
onPressed: () => InfoDialog.show(
|
||||
context,
|
||||
technicalDetails!,
|
||||
copyable: true,
|
||||
title: 'Fehlerdetails',
|
||||
),
|
||||
child: const Text('Details anzeigen'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isOffline && message != null && message != headline) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message!,
|
||||
style: TextStyle(color: Theme.of(context).hintColor, fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
if (bloc.allowRetry()) ...[
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () => bloc.reFetch!(),
|
||||
child: const Text('Erneut versuchen'),
|
||||
),
|
||||
],
|
||||
if (technicalDetails != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
TextButton(
|
||||
onPressed: () => InfoDialog.show(context, technicalDetails!, copyable: true, title: 'Fehlerdetails'),
|
||||
child: const Text('Details anzeigen'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -9,9 +9,9 @@ class LoadableStatePrimaryLoading extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AnimatedOpacity(
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: const Center(child: AppProgressIndicator.large()),
|
||||
);
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: const Center(child: AppProgressIndicator.large()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class BlocModule<TBloc extends StateStreamableSource<TState>, TState> extends StatelessWidget {
|
||||
class BlocModule<TBloc extends StateStreamableSource<TState>, TState>
|
||||
extends StatelessWidget {
|
||||
final TBloc Function(BuildContext context) create;
|
||||
final Widget Function(BuildContext context, TBloc bloc, TState state) child;
|
||||
final bool autoRebuild;
|
||||
final void Function(BuildContext context, TBloc bloc)? onInitialisation;
|
||||
const BlocModule({required this.create, required this.child, this.autoRebuild = false, this.onInitialisation, super.key});
|
||||
const BlocModule({
|
||||
required this.create,
|
||||
required this.child,
|
||||
this.autoRebuild = false,
|
||||
this.onInitialisation,
|
||||
super.key,
|
||||
});
|
||||
|
||||
Widget rebuildChild(BuildContext context) => child(context, context.watch<TBloc>(), context.watch<TBloc>().state);
|
||||
Widget staticChild(BuildContext context) => child(context, context.read<TBloc>(), context.read<TBloc>().state);
|
||||
Widget rebuildChild(BuildContext context) =>
|
||||
child(context, context.watch<TBloc>(), context.watch<TBloc>().state);
|
||||
Widget staticChild(BuildContext context) =>
|
||||
child(context, context.read<TBloc>(), context.read<TBloc>().state);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocProvider<TBloc>(
|
||||
@@ -19,9 +28,8 @@ class BlocModule<TBloc extends StateStreamableSource<TState>, TState> extends St
|
||||
return bloc;
|
||||
},
|
||||
child: Builder(
|
||||
builder: (context) => autoRebuild
|
||||
? rebuildChild(context)
|
||||
: staticChild(context)
|
||||
)
|
||||
builder: (context) =>
|
||||
autoRebuild ? rebuildChild(context) : staticChild(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+89
-66
@@ -13,60 +13,79 @@ abstract class LoadableHydratedBloc<
|
||||
TEvent extends LoadableHydratedBlocEvent<TState>,
|
||||
TState,
|
||||
TRepository extends Repository<TState>
|
||||
> extends HydratedBloc<
|
||||
LoadableHydratedBlocEvent<TState>,
|
||||
LoadableState<TState>
|
||||
> {
|
||||
>
|
||||
extends
|
||||
HydratedBloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>> {
|
||||
late TRepository _repository;
|
||||
LoadableHydratedBloc() : super(const LoadableState(
|
||||
error: null,
|
||||
data: null,
|
||||
isLoading: true,
|
||||
lastFetch: null,
|
||||
reFetch: null,
|
||||
)) {
|
||||
|
||||
LoadableHydratedBloc()
|
||||
: super(
|
||||
const LoadableState(
|
||||
error: null,
|
||||
data: null,
|
||||
isLoading: true,
|
||||
lastFetch: null,
|
||||
reFetch: null,
|
||||
),
|
||||
) {
|
||||
on<Emit<TState>>((event, emit) {
|
||||
emit(LoadableState(
|
||||
isLoading: state.isLoading,
|
||||
data: event.state(innerState ?? fromNothing()),
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: retry,
|
||||
error: state.error,
|
||||
));
|
||||
emit(
|
||||
LoadableState(
|
||||
isLoading: state.isLoading,
|
||||
data: event.state(innerState ?? fromNothing()),
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: retry,
|
||||
error: state.error,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
on<DataGathered<TState>>((event, emit) => emit(LoadableState(
|
||||
isLoading: false,
|
||||
data: event.state(innerState ?? fromNothing()),
|
||||
lastFetch: DateTime.now().millisecondsSinceEpoch,
|
||||
reFetch: retry,
|
||||
error: null,
|
||||
)));
|
||||
on<DataGathered<TState>>(
|
||||
(event, emit) => emit(
|
||||
LoadableState(
|
||||
isLoading: false,
|
||||
data: event.state(innerState ?? fromNothing()),
|
||||
lastFetch: DateTime.now().millisecondsSinceEpoch,
|
||||
reFetch: retry,
|
||||
error: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
on<RefetchStarted<TState>>((event, emit) => emit(LoadableState(
|
||||
isLoading: true,
|
||||
data: innerState,
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: null,
|
||||
error: null,
|
||||
)));
|
||||
on<RefetchStarted<TState>>(
|
||||
(event, emit) => emit(
|
||||
LoadableState(
|
||||
isLoading: true,
|
||||
data: innerState,
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: null,
|
||||
error: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
on<Error<TState>>((event, emit) => emit(LoadableState(
|
||||
isLoading: false,
|
||||
data: innerState,
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: retry,
|
||||
error: event.error
|
||||
)));
|
||||
on<Error<TState>>(
|
||||
(event, emit) => emit(
|
||||
LoadableState(
|
||||
isLoading: false,
|
||||
data: innerState,
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: retry,
|
||||
error: event.error,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
on<Reset<TState>>((event, emit) => emit(const LoadableState(
|
||||
isLoading: false,
|
||||
data: null,
|
||||
lastFetch: null,
|
||||
reFetch: null,
|
||||
error: null,
|
||||
)));
|
||||
on<Reset<TState>>(
|
||||
(event, emit) => emit(
|
||||
const LoadableState(
|
||||
isLoading: false,
|
||||
data: null,
|
||||
lastFetch: null,
|
||||
reFetch: null,
|
||||
error: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
_repository = repository();
|
||||
fetch();
|
||||
@@ -92,23 +111,27 @@ abstract class LoadableHydratedBloc<
|
||||
|
||||
void fetch() {
|
||||
log('Fetching data for ${TState.toString()}');
|
||||
gatherData().catchError(
|
||||
(e) {
|
||||
log('Error while fetching ${TState.toString()}: ${e.toString()}');
|
||||
// The bloc may have been closed before this async error landed (e.g.
|
||||
// when its scoping widget tree was disposed mid-fetch). Adding to a
|
||||
// closed bloc throws "Cannot add new events after calling close",
|
||||
// so swallow that case quietly.
|
||||
if (isClosed) return;
|
||||
add(Error(LoadingError(
|
||||
message: errorToUserMessage(e),
|
||||
technicalDetails: errorToTechnicalDetails(e),
|
||||
allowRetry: errorAllowsRetry(e),
|
||||
)));
|
||||
},
|
||||
).then((value) {
|
||||
log('Fetch for ${TState.toString()} completed!');
|
||||
});
|
||||
gatherData()
|
||||
.catchError((e) {
|
||||
log('Error while fetching ${TState.toString()}: ${e.toString()}');
|
||||
// The bloc may have been closed before this async error landed (e.g.
|
||||
// when its scoping widget tree was disposed mid-fetch). Adding to a
|
||||
// closed bloc throws "Cannot add new events after calling close",
|
||||
// so swallow that case quietly.
|
||||
if (isClosed) return;
|
||||
add(
|
||||
Error(
|
||||
LoadingError(
|
||||
message: errorToUserMessage(e),
|
||||
technicalDetails: errorToTechnicalDetails(e),
|
||||
allowRetry: errorAllowsRetry(e),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
.then((value) {
|
||||
log('Fetch for ${TState.toString()} completed!');
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -129,13 +152,13 @@ abstract class LoadableHydratedBloc<
|
||||
try {
|
||||
final stateData = state.data;
|
||||
data = stateData is TState ? toStorage(stateData) : null;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
log('Failed to save state ${TState.toString()}: ${e.toString()}');
|
||||
}
|
||||
|
||||
return LoadableSaveContext.wrap(
|
||||
data,
|
||||
state.lastFetch ?? DateTime.now().millisecondsSinceEpoch
|
||||
state.lastFetch ?? DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -1,17 +1,22 @@
|
||||
import '../../loadable_state/loading_error.dart';
|
||||
|
||||
class LoadableHydratedBlocEvent<TState> {}
|
||||
|
||||
class Emit<TState> extends LoadableHydratedBlocEvent<TState> {
|
||||
final TState Function(TState state) state;
|
||||
Emit(this.state);
|
||||
}
|
||||
|
||||
class DataGathered<TState> extends LoadableHydratedBlocEvent<TState> {
|
||||
final TState Function(TState state) state;
|
||||
DataGathered(this.state);
|
||||
}
|
||||
|
||||
class Error<TState> extends LoadableHydratedBlocEvent<TState> {
|
||||
final LoadingError error;
|
||||
Error(this.error);
|
||||
}
|
||||
|
||||
class RefetchStarted<TState> extends LoadableHydratedBlocEvent<TState> {}
|
||||
|
||||
class Reset<TState> extends LoadableHydratedBlocEvent<TState> {}
|
||||
|
||||
+14
-7
@@ -6,18 +6,25 @@ part 'loadable_save_context.g.dart';
|
||||
@freezed
|
||||
abstract class LoadableSaveContext with _$LoadableSaveContext {
|
||||
const LoadableSaveContext._();
|
||||
const factory LoadableSaveContext({
|
||||
required int timestamp,
|
||||
}) = _LoadableSaveContext;
|
||||
const factory LoadableSaveContext({required int timestamp}) =
|
||||
_LoadableSaveContext;
|
||||
|
||||
factory LoadableSaveContext.fromJson(Map<String, dynamic> json) => _$LoadableSaveContextFromJson(json);
|
||||
factory LoadableSaveContext.fromJson(Map<String, dynamic> json) =>
|
||||
_$LoadableSaveContextFromJson(json);
|
||||
|
||||
static String dataKey = 'data';
|
||||
static String metaKey = 'meta';
|
||||
|
||||
static Map<String, dynamic> wrap(Map<String, dynamic>? data, int lastFetch) =>
|
||||
{dataKey: data, metaKey: LoadableSaveContext(timestamp: lastFetch).toJson()};
|
||||
{
|
||||
dataKey: data,
|
||||
metaKey: LoadableSaveContext(timestamp: lastFetch).toJson(),
|
||||
};
|
||||
|
||||
static ({Map<String, dynamic> data, LoadableSaveContext meta}) unwrap(Map<String, dynamic> data) =>
|
||||
(data: data[dataKey] as Map<String, dynamic>, meta: LoadableSaveContext.fromJson(data[metaKey] as Map<String, dynamic>));
|
||||
static ({Map<String, dynamic> data, LoadableSaveContext meta}) unwrap(
|
||||
Map<String, dynamic> data,
|
||||
) => (
|
||||
data: data[dataKey] as Map<String, dynamic>,
|
||||
meta: LoadableSaveContext.fromJson(data[metaKey] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@ import 'account_event.dart';
|
||||
import 'account_state.dart';
|
||||
|
||||
class AccountBloc extends Bloc<AccountEvent, AccountState> {
|
||||
AccountBloc({AccountStatus initialStatus = AccountStatus.undefined}) : super(AccountState(status: initialStatus)) {
|
||||
on<AccountStatusChanged>((event, emit) => emit(state.copyWith(status: event.status)));
|
||||
AccountBloc({AccountStatus initialStatus = AccountStatus.undefined})
|
||||
: super(AccountState(status: initialStatus)) {
|
||||
on<AccountStatusChanged>(
|
||||
(event, emit) => emit(state.copyWith(status: event.status)),
|
||||
);
|
||||
}
|
||||
|
||||
void setStatus(AccountStatus status) => add(AccountStatusChanged(status));
|
||||
|
||||
@@ -4,5 +4,6 @@ class AccountState {
|
||||
final AccountStatus status;
|
||||
const AccountState({this.status = AccountStatus.undefined});
|
||||
|
||||
AccountState copyWith({AccountStatus? status}) => AccountState(status: status ?? this.status);
|
||||
AccountState copyWith({AccountStatus? status}) =>
|
||||
AccountState(status: status ?? this.status);
|
||||
}
|
||||
|
||||
@@ -27,9 +27,18 @@ class AppModule {
|
||||
BreakerArea breakerArea;
|
||||
Widget Function() create;
|
||||
|
||||
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, {bool showFiltered = false}) {
|
||||
static Map<Modules, AppModule> modules(
|
||||
BuildContext context, {
|
||||
bool showFiltered = false,
|
||||
}) {
|
||||
final settings = context.read<SettingsCubit>();
|
||||
var available = {
|
||||
Modules.timetable: AppModule(
|
||||
@@ -45,8 +54,12 @@ class AppModule {
|
||||
icon: () => BlocBuilder<ChatListBloc, LoadableState<ChatListState>>(
|
||||
builder: (context, state) {
|
||||
final rooms = state.data?.rooms;
|
||||
if (rooms == null || rooms.data.isEmpty) return const Icon(Icons.chat);
|
||||
final messages = rooms.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(
|
||||
showBadge: messages > 0,
|
||||
position: badges.BadgePosition.topEnd(top: -3, end: -3),
|
||||
@@ -56,7 +69,14 @@ class AppModule {
|
||||
badgeColor: Theme.of(context).primaryColor,
|
||||
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: const Icon(Icons.chat),
|
||||
);
|
||||
},
|
||||
@@ -108,9 +128,19 @@ class AppModule {
|
||||
),
|
||||
};
|
||||
|
||||
if (!showFiltered) available.removeWhere((key, value) => settings.val().modulesSettings.hiddenModules.contains(key));
|
||||
if (!showFiltered) {
|
||||
available.removeWhere(
|
||||
(key, value) =>
|
||||
settings.val().modulesSettings.hiddenModules.contains(key),
|
||||
);
|
||||
}
|
||||
|
||||
return { for (var element in settings.val().modulesSettings.moduleOrder.where((element) => available.containsKey(element))) element : available[element]! };
|
||||
return {
|
||||
for (var element in settings.val().modulesSettings.moduleOrder.where(
|
||||
(element) => available.containsKey(element),
|
||||
))
|
||||
element: available[element]!,
|
||||
};
|
||||
}
|
||||
|
||||
static const int minBottomBarSlots = 3;
|
||||
@@ -150,26 +180,45 @@ class AppModule {
|
||||
return all.skip(slots).toList();
|
||||
}
|
||||
|
||||
Widget toListTile(BuildContext context, {Key? key, bool isReorder = false, Function()? onVisibleChange, bool isVisible = true}) => ListTile(
|
||||
Widget toListTile(
|
||||
BuildContext context, {
|
||||
Key? key,
|
||||
bool isReorder = false,
|
||||
Function()? onVisibleChange,
|
||||
bool isVisible = true,
|
||||
}) => ListTile(
|
||||
key: key,
|
||||
leading: CenteredLeading(icon()),
|
||||
title: Text(name),
|
||||
onTap: isReorder ? null : () => AppRoutes.openModule(context, this),
|
||||
trailing: isReorder
|
||||
? Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(onPressed: onVisibleChange, icon: Icon(isVisible ? Icons.visibility_outlined : Icons.visibility_off_outlined)),
|
||||
Icon(Icons.drag_handle_outlined)
|
||||
])
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: onVisibleChange,
|
||||
icon: Icon(
|
||||
isVisible
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
),
|
||||
),
|
||||
Icon(Icons.drag_handle_outlined),
|
||||
],
|
||||
)
|
||||
: const Icon(Icons.arrow_right),
|
||||
);
|
||||
|
||||
PersistentTabConfig toBottomTab(BuildContext context, {Widget Function(IconData icon)? iconBuilder}) => PersistentTabConfig(
|
||||
PersistentTabConfig toBottomTab(
|
||||
BuildContext context, {
|
||||
Widget Function(IconData icon)? iconBuilder,
|
||||
}) => PersistentTabConfig(
|
||||
screen: Breaker(breaker: breakerArea, child: create()),
|
||||
item: ItemConfig(
|
||||
activeForegroundColor: Theme.of(context).primaryColor,
|
||||
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
|
||||
icon: icon(),
|
||||
title: name
|
||||
activeForegroundColor: Theme.of(context).primaryColor,
|
||||
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
|
||||
icon: icon(),
|
||||
title: name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import '../repository/breaker_repository.dart';
|
||||
import 'breaker_event.dart';
|
||||
import 'breaker_state.dart';
|
||||
|
||||
class BreakerBloc extends LoadableHydratedBloc<BreakerEvent, BreakerState, BreakerRepository> {
|
||||
class BreakerBloc
|
||||
extends
|
||||
LoadableHydratedBloc<BreakerEvent, BreakerState, BreakerRepository> {
|
||||
PackageInfo? _packageInfo;
|
||||
|
||||
@override
|
||||
@@ -18,7 +20,8 @@ class BreakerBloc extends LoadableHydratedBloc<BreakerEvent, BreakerState, Break
|
||||
BreakerState fromNothing() => const BreakerState();
|
||||
|
||||
@override
|
||||
BreakerState fromStorage(Map<String, dynamic> json) => BreakerState.fromJson(json);
|
||||
BreakerState fromStorage(Map<String, dynamic> json) =>
|
||||
BreakerState.fromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toStorage(BreakerState state) => state.toJson();
|
||||
|
||||
@@ -7,9 +7,8 @@ part 'breaker_state.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class BreakerState with _$BreakerState {
|
||||
const factory BreakerState({
|
||||
GetBreakersResponse? response,
|
||||
}) = _BreakerState;
|
||||
const factory BreakerState({GetBreakersResponse? response}) = _BreakerState;
|
||||
|
||||
factory BreakerState.fromJson(Map<String, Object?> json) => _$BreakerStateFromJson(json);
|
||||
factory BreakerState.fromJson(Map<String, Object?> json) =>
|
||||
_$BreakerStateFromJson(json);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import '../../../../../api/mhsl/breaker/get_breakers/get_breakers_response.dart'
|
||||
class BreakerDataProvider {
|
||||
Future<GetBreakersResponse> getBreakers() {
|
||||
final completer = Completer<GetBreakersResponse>();
|
||||
GetBreakersCache(onUpdate: (data) {
|
||||
if (!completer.isCompleted) completer.complete(data);
|
||||
});
|
||||
GetBreakersCache(
|
||||
onUpdate: (data) {
|
||||
if (!completer.isCompleted) completer.complete(data);
|
||||
},
|
||||
);
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import '../data_provider/breaker_data_provider.dart';
|
||||
class BreakerRepository extends Repository<BreakerState> {
|
||||
final BreakerDataProvider _provider;
|
||||
|
||||
BreakerRepository([BreakerDataProvider? provider]) : _provider = provider ?? BreakerDataProvider();
|
||||
BreakerRepository([BreakerDataProvider? provider])
|
||||
: _provider = provider ?? BreakerDataProvider();
|
||||
|
||||
BreakerDataProvider get data => _provider;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import '../repository/chat_repository.dart';
|
||||
import 'chat_event.dart';
|
||||
import 'chat_state.dart';
|
||||
|
||||
class ChatBloc extends LoadableHydratedBloc<ChatEvent, ChatState, ChatRepository> {
|
||||
class ChatBloc
|
||||
extends LoadableHydratedBloc<ChatEvent, ChatState, ChatRepository> {
|
||||
DateTime _lastTokenSet = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
@override
|
||||
@@ -86,11 +87,15 @@ class ChatBloc extends LoadableHydratedBloc<ChatEvent, ChatState, ChatRepository
|
||||
if (!stillCurrent()) return;
|
||||
|
||||
if (capturedError != null) {
|
||||
add(Error(LoadingError(
|
||||
message: errorToUserMessage(capturedError),
|
||||
technicalDetails: errorToTechnicalDetails(capturedError),
|
||||
allowRetry: errorAllowsRetry(capturedError),
|
||||
)));
|
||||
add(
|
||||
Error(
|
||||
LoadingError(
|
||||
message: errorToUserMessage(capturedError),
|
||||
technicalDetails: errorToTechnicalDetails(capturedError),
|
||||
allowRetry: errorAllowsRetry(capturedError),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ abstract class ChatState with _$ChatState {
|
||||
int? referenceMessageId,
|
||||
}) = _ChatState;
|
||||
|
||||
factory ChatState.fromJson(Map<String, Object?> json) => _$ChatStateFromJson(json);
|
||||
factory ChatState.fromJson(Map<String, Object?> json) =>
|
||||
_$ChatStateFromJson(json);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import '../data_provider/chat_data_provider.dart';
|
||||
class ChatRepository extends Repository<ChatState> {
|
||||
final ChatDataProvider _provider;
|
||||
|
||||
ChatRepository([ChatDataProvider? provider]) : _provider = provider ?? ChatDataProvider();
|
||||
ChatRepository([ChatDataProvider? provider])
|
||||
: _provider = provider ?? ChatDataProvider();
|
||||
|
||||
ChatDataProvider get data => _provider;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ import '../repository/chat_list_repository.dart';
|
||||
import 'chat_list_event.dart';
|
||||
import 'chat_list_state.dart';
|
||||
|
||||
class ChatListBloc extends LoadableHydratedBloc<ChatListEvent, ChatListState, ChatListRepository> {
|
||||
class ChatListBloc
|
||||
extends
|
||||
LoadableHydratedBloc<ChatListEvent, ChatListState, ChatListRepository> {
|
||||
bool _forceRenew = false;
|
||||
|
||||
@override
|
||||
@@ -27,7 +29,8 @@ class ChatListBloc extends LoadableHydratedBloc<ChatListEvent, ChatListState, Ch
|
||||
ChatListState fromNothing() => const ChatListState();
|
||||
|
||||
@override
|
||||
ChatListState fromStorage(Map<String, dynamic> json) => ChatListState.fromJson(json);
|
||||
ChatListState fromStorage(Map<String, dynamic> json) =>
|
||||
ChatListState.fromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toStorage(ChatListState state) => state.toJson();
|
||||
@@ -62,11 +65,15 @@ class ChatListBloc extends LoadableHydratedBloc<ChatListEvent, ChatListState, Ch
|
||||
capturedError = e;
|
||||
}
|
||||
if (capturedError != null) {
|
||||
add(Error(LoadingError(
|
||||
message: errorToUserMessage(capturedError),
|
||||
technicalDetails: errorToTechnicalDetails(capturedError),
|
||||
allowRetry: errorAllowsRetry(capturedError),
|
||||
)));
|
||||
add(
|
||||
Error(
|
||||
LoadingError(
|
||||
message: errorToUserMessage(capturedError),
|
||||
technicalDetails: errorToTechnicalDetails(capturedError),
|
||||
allowRetry: errorAllowsRetry(capturedError),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +84,10 @@ class ChatListBloc extends LoadableHydratedBloc<ChatListEvent, ChatListState, Ch
|
||||
|
||||
void _updateAppBadge(GetRoomResponse rooms) {
|
||||
try {
|
||||
final unread = rooms.data.fold<int>(0, (a, room) => a + room.unreadMessages);
|
||||
final unread = rooms.data.fold<int>(
|
||||
0,
|
||||
(a, room) => a + room.unreadMessages,
|
||||
);
|
||||
FlutterAppBadge.count(unread);
|
||||
} on Object catch (e) {
|
||||
log('Failed to update app badge: $e');
|
||||
|
||||
@@ -7,9 +7,8 @@ part 'chat_list_state.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ChatListState with _$ChatListState {
|
||||
const factory ChatListState({
|
||||
GetRoomResponse? rooms,
|
||||
}) = _ChatListState;
|
||||
const factory ChatListState({GetRoomResponse? rooms}) = _ChatListState;
|
||||
|
||||
factory ChatListState.fromJson(Map<String, Object?> json) => _$ChatListStateFromJson(json);
|
||||
factory ChatListState.fromJson(Map<String, Object?> json) =>
|
||||
_$ChatListStateFromJson(json);
|
||||
}
|
||||
|
||||
@@ -8,16 +8,12 @@ class ChatListDataProvider {
|
||||
Future<GetRoomResponse> getRooms({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) =>
|
||||
resolveFromCache<GetRoomResponse>(
|
||||
(onUpdate, onError) => GetRoomCache(
|
||||
renew: renew,
|
||||
onUpdate: onUpdate,
|
||||
onError: onError,
|
||||
),
|
||||
onError: onError,
|
||||
operationName: 'getRooms',
|
||||
);
|
||||
}) => resolveFromCache<GetRoomResponse>(
|
||||
(onUpdate, onError) =>
|
||||
GetRoomCache(renew: renew, onUpdate: onUpdate, onError: onError),
|
||||
onError: onError,
|
||||
operationName: 'getRooms',
|
||||
);
|
||||
|
||||
Future<void> createDirectRoom(String invite) =>
|
||||
CreateRoom(CreateRoomParams(roomType: 1, invite: invite)).run();
|
||||
|
||||
@@ -5,7 +5,8 @@ import '../data_provider/chat_list_data_provider.dart';
|
||||
class ChatListRepository extends Repository<ChatListState> {
|
||||
final ChatListDataProvider _provider;
|
||||
|
||||
ChatListRepository([ChatListDataProvider? provider]) : _provider = provider ?? ChatListDataProvider();
|
||||
ChatListRepository([ChatListDataProvider? provider])
|
||||
: _provider = provider ?? ChatListDataProvider();
|
||||
|
||||
ChatListDataProvider get data => _provider;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import '../repository/files_repository.dart';
|
||||
import 'files_event.dart';
|
||||
import 'files_state.dart';
|
||||
|
||||
class FilesBloc extends LoadableHydratedBloc<FilesEvent, FilesState, FilesRepository> {
|
||||
class FilesBloc
|
||||
extends LoadableHydratedBloc<FilesEvent, FilesState, FilesRepository> {
|
||||
final List<String> initialPath;
|
||||
|
||||
FilesBloc({this.initialPath = const []});
|
||||
@@ -19,7 +20,8 @@ class FilesBloc extends LoadableHydratedBloc<FilesEvent, FilesState, FilesReposi
|
||||
FilesState fromNothing() => FilesState(currentPath: initialPath);
|
||||
|
||||
@override
|
||||
FilesState fromStorage(Map<String, dynamic> json) => FilesState.fromJson(json);
|
||||
FilesState fromStorage(Map<String, dynamic> json) =>
|
||||
FilesState.fromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toStorage(FilesState state) => null;
|
||||
@@ -60,7 +62,9 @@ class FilesBloc extends LoadableHydratedBloc<FilesEvent, FilesState, FilesReposi
|
||||
// Cached payload arrives before the network call settles. Surface it
|
||||
// immediately via Emit so the listing is visible while isLoading
|
||||
// stays true and the top loading bar keeps spinning.
|
||||
cached.files.removeWhere((file) => file.name.isEmpty || file.name == path.lastOrNull);
|
||||
cached.files.removeWhere(
|
||||
(file) => file.name.isEmpty || file.name == path.lastOrNull,
|
||||
);
|
||||
add(Emit((s) => s.copyWith(listing: cached)));
|
||||
},
|
||||
onError: (e) => capturedError = e,
|
||||
@@ -70,15 +74,21 @@ class FilesBloc extends LoadableHydratedBloc<FilesEvent, FilesState, FilesReposi
|
||||
}
|
||||
|
||||
if (listing != null) {
|
||||
listing.files.removeWhere((file) => file.name.isEmpty || file.name == path.lastOrNull);
|
||||
listing.files.removeWhere(
|
||||
(file) => file.name.isEmpty || file.name == path.lastOrNull,
|
||||
);
|
||||
add(DataGathered((s) => s.copyWith(listing: listing)));
|
||||
}
|
||||
if (capturedError != null) {
|
||||
add(Error(LoadingError(
|
||||
message: errorToUserMessage(capturedError),
|
||||
technicalDetails: errorToTechnicalDetails(capturedError),
|
||||
allowRetry: errorAllowsRetry(capturedError),
|
||||
)));
|
||||
add(
|
||||
Error(
|
||||
LoadingError(
|
||||
message: errorToUserMessage(capturedError),
|
||||
technicalDetails: errorToTechnicalDetails(capturedError),
|
||||
allowRetry: errorAllowsRetry(capturedError),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ abstract class FilesState with _$FilesState {
|
||||
ListFilesResponse? listing,
|
||||
}) = _FilesState;
|
||||
|
||||
factory FilesState.fromJson(Map<String, Object?> json) => _$FilesStateFromJson(json);
|
||||
factory FilesState.fromJson(Map<String, Object?> json) =>
|
||||
_$FilesStateFromJson(json);
|
||||
}
|
||||
|
||||
@@ -15,17 +15,16 @@ class FilesDataProvider {
|
||||
String path, {
|
||||
void Function(ListFilesResponse)? onCacheData,
|
||||
void Function(Object)? onError,
|
||||
}) =>
|
||||
resolveFromCache<ListFilesResponse>(
|
||||
(onUpdate, onError) => ListFilesCache(
|
||||
path: path,
|
||||
onUpdate: onUpdate,
|
||||
onCacheData: onCacheData,
|
||||
onError: onError,
|
||||
),
|
||||
onError: onError,
|
||||
operationName: 'listFiles',
|
||||
);
|
||||
}) => resolveFromCache<ListFilesResponse>(
|
||||
(onUpdate, onError) => ListFilesCache(
|
||||
path: path,
|
||||
onUpdate: onUpdate,
|
||||
onCacheData: onCacheData,
|
||||
onError: onError,
|
||||
),
|
||||
onError: onError,
|
||||
operationName: 'listFiles',
|
||||
);
|
||||
|
||||
Future<void> createFolder(String fullPath) async {
|
||||
final webdav = await WebdavApi.webdav;
|
||||
|
||||
@@ -5,7 +5,8 @@ import '../data_provider/files_data_provider.dart';
|
||||
class FilesRepository extends Repository<FilesState> {
|
||||
final FilesDataProvider _provider;
|
||||
|
||||
FilesRepository([FilesDataProvider? provider]) : _provider = provider ?? FilesDataProvider();
|
||||
FilesRepository([FilesDataProvider? provider])
|
||||
: _provider = provider ?? FilesDataProvider();
|
||||
|
||||
FilesDataProvider get data => _provider;
|
||||
}
|
||||
|
||||
@@ -3,17 +3,23 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'grade_averages_event.dart';
|
||||
import 'grade_averages_state.dart';
|
||||
|
||||
class GradeAveragesBloc extends HydratedBloc<GradeAveragesEvent, GradeAveragesState> {
|
||||
GradeAveragesBloc() : super(const GradeAveragesState(gradingSystem: GradeAveragesGradingSystem.middleSchool, grades: [])) {
|
||||
|
||||
class GradeAveragesBloc
|
||||
extends HydratedBloc<GradeAveragesEvent, GradeAveragesState> {
|
||||
GradeAveragesBloc()
|
||||
: super(
|
||||
const GradeAveragesState(
|
||||
gradingSystem: GradeAveragesGradingSystem.middleSchool,
|
||||
grades: [],
|
||||
),
|
||||
) {
|
||||
on<GradingSystemChanged>((event, emit) {
|
||||
add(ResetAll());
|
||||
emit(
|
||||
state.copyWith(
|
||||
gradingSystem: event.isMiddleSchool
|
||||
? GradeAveragesGradingSystem.middleSchool
|
||||
: GradeAveragesGradingSystem.highSchool
|
||||
)
|
||||
? GradeAveragesGradingSystem.middleSchool
|
||||
: GradeAveragesGradingSystem.highSchool,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -22,7 +28,12 @@ class GradeAveragesBloc extends HydratedBloc<GradeAveragesEvent, GradeAveragesSt
|
||||
});
|
||||
|
||||
on<ResetGrade>((event, emit) {
|
||||
emit(state.copyWith(grades: [...state.grades]..removeWhere((grade) => grade == event.grade)));
|
||||
emit(
|
||||
state.copyWith(
|
||||
grades: [...state.grades]
|
||||
..removeWhere((grade) => grade == event.grade),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
on<IncrementGrade>((event, emit) {
|
||||
@@ -30,20 +41,26 @@ class GradeAveragesBloc extends HydratedBloc<GradeAveragesEvent, GradeAveragesSt
|
||||
});
|
||||
|
||||
on<DecrementGrade>((event, emit) {
|
||||
emit(state.copyWith(grades: List.from(state.grades)..remove(event.grade)));
|
||||
emit(
|
||||
state.copyWith(grades: List.from(state.grades)..remove(event.grade)),
|
||||
);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
double average() => state.grades.isEmpty ? 0 : state.grades.reduce((a, b) => a + b) / state.grades.length;
|
||||
bool isMiddleSchool() => state.gradingSystem == GradeAveragesGradingSystem.middleSchool;
|
||||
double average() => state.grades.isEmpty
|
||||
? 0
|
||||
: state.grades.reduce((a, b) => a + b) / state.grades.length;
|
||||
bool isMiddleSchool() =>
|
||||
state.gradingSystem == GradeAveragesGradingSystem.middleSchool;
|
||||
bool canDecrementOrDelete(int grade) => state.grades.contains(grade);
|
||||
int countOfGrade(int grade) => state.grades.where((g) => g == grade).length;
|
||||
int gradesInGradingSystem() => state.gradingSystem == GradeAveragesGradingSystem.middleSchool ? 6 : 16;
|
||||
int gradesInGradingSystem() =>
|
||||
state.gradingSystem == GradeAveragesGradingSystem.middleSchool ? 6 : 16;
|
||||
int getGradeFromIndex(int index) => isMiddleSchool() ? index + 1 : 15 - index;
|
||||
|
||||
@override
|
||||
GradeAveragesState? fromJson(Map<String, dynamic> json) => GradeAveragesState.fromJson(json);
|
||||
GradeAveragesState? fromJson(Map<String, dynamic> json) =>
|
||||
GradeAveragesState.fromJson(json);
|
||||
@override
|
||||
Map<String, dynamic>? toJson(GradeAveragesState state) => state.toJson();
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
|
||||
sealed class GradeAveragesEvent {}
|
||||
|
||||
final class GradingSystemChanged extends GradeAveragesEvent {
|
||||
final bool isMiddleSchool;
|
||||
GradingSystemChanged(this.isMiddleSchool);
|
||||
}
|
||||
|
||||
final class ResetAll extends GradeAveragesEvent {}
|
||||
|
||||
final class ResetGrade extends GradeAveragesEvent {
|
||||
final int grade;
|
||||
ResetGrade(this.grade);
|
||||
}
|
||||
|
||||
final class IncrementGrade extends GradeAveragesEvent {
|
||||
final int grade;
|
||||
IncrementGrade(this.grade);
|
||||
}
|
||||
|
||||
final class DecrementGrade extends GradeAveragesEvent {
|
||||
final int grade;
|
||||
DecrementGrade(this.grade);
|
||||
|
||||
@@ -10,10 +10,8 @@ abstract class GradeAveragesState with _$GradeAveragesState {
|
||||
required List<int> grades,
|
||||
}) = _GradeAveragesState;
|
||||
|
||||
factory GradeAveragesState.fromJson(Map<String, dynamic> json) => _$GradeAveragesStateFromJson(json);
|
||||
factory GradeAveragesState.fromJson(Map<String, dynamic> json) =>
|
||||
_$GradeAveragesStateFromJson(json);
|
||||
}
|
||||
|
||||
enum GradeAveragesGradingSystem {
|
||||
highSchool,
|
||||
middleSchool,
|
||||
}
|
||||
enum GradeAveragesGradingSystem { highSchool, middleSchool }
|
||||
|
||||
@@ -4,32 +4,51 @@ import '../repository/holidays_repository.dart';
|
||||
import 'holidays_event.dart';
|
||||
import 'holidays_state.dart';
|
||||
|
||||
class HolidaysBloc extends LoadableHydratedBloc<HolidaysEvent, HolidaysState, HolidaysRepository> {
|
||||
class HolidaysBloc
|
||||
extends
|
||||
LoadableHydratedBloc<HolidaysEvent, HolidaysState, HolidaysRepository> {
|
||||
HolidaysBloc() {
|
||||
on<SetPastHolidaysVisible>((event, emit) {
|
||||
add(Emit((state) => state.copyWith(showPastHolidays: event.shouldBeVisible)));
|
||||
add(
|
||||
Emit(
|
||||
(state) => state.copyWith(showPastHolidays: event.shouldBeVisible),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
on<DisclaimerDismissed>((event, emit) => add(
|
||||
Emit((state) => state.copyWith(showDisclaimer: false))
|
||||
));
|
||||
on<DisclaimerDismissed>(
|
||||
(event, emit) =>
|
||||
add(Emit((state) => state.copyWith(showDisclaimer: false))),
|
||||
);
|
||||
}
|
||||
|
||||
bool showPastHolidays() => innerState?.showPastHolidays ?? false;
|
||||
bool showDisclaimerOnEntry() => innerState?.showDisclaimer ?? false;
|
||||
List<Holiday>? getHolidays() => innerState?.holidays
|
||||
.where((element) => showPastHolidays() || DateTime.parse(element.end).isAfter(DateTime.now()))
|
||||
.toList() ?? [];
|
||||
List<Holiday>? getHolidays() =>
|
||||
innerState?.holidays
|
||||
.where(
|
||||
(element) =>
|
||||
showPastHolidays() ||
|
||||
DateTime.parse(element.end).isAfter(DateTime.now()),
|
||||
)
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
@override
|
||||
HolidaysState fromNothing() => const HolidaysState(showPastHolidays: false, holidays: [], showDisclaimer: true);
|
||||
HolidaysState fromNothing() => const HolidaysState(
|
||||
showPastHolidays: false,
|
||||
holidays: [],
|
||||
showDisclaimer: true,
|
||||
);
|
||||
@override
|
||||
HolidaysState fromStorage(Map<String, dynamic> json) => HolidaysState.fromJson(json);
|
||||
HolidaysState fromStorage(Map<String, dynamic> json) =>
|
||||
HolidaysState.fromJson(json);
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
var holidays = await repo.getHolidays();
|
||||
add(DataGathered((state) => state.copyWith(holidays: holidays)));
|
||||
}
|
||||
|
||||
@override
|
||||
HolidaysRepository repository() => HolidaysRepository();
|
||||
@override
|
||||
|
||||
@@ -2,8 +2,10 @@ import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_
|
||||
import 'holidays_state.dart';
|
||||
|
||||
sealed class HolidaysEvent extends LoadableHydratedBlocEvent<HolidaysState> {}
|
||||
|
||||
class SetPastHolidaysVisible extends HolidaysEvent {
|
||||
final bool shouldBeVisible;
|
||||
SetPastHolidaysVisible(this.shouldBeVisible);
|
||||
}
|
||||
|
||||
class DisclaimerDismissed extends HolidaysEvent {}
|
||||
|
||||
@@ -12,7 +12,8 @@ abstract class HolidaysState with _$HolidaysState {
|
||||
required List<Holiday> holidays,
|
||||
}) = _HolidaysState;
|
||||
|
||||
factory HolidaysState.fromJson(Map<String, Object?> json) => _$HolidaysStateFromJson(json);
|
||||
factory HolidaysState.fromJson(Map<String, Object?> json) =>
|
||||
_$HolidaysStateFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -26,5 +27,6 @@ abstract class Holiday with _$Holiday {
|
||||
required String slug,
|
||||
}) = _Holiday;
|
||||
|
||||
factory Holiday.fromJson(Map<String, Object?> json) => _$HolidayFromJson(json);
|
||||
factory Holiday.fromJson(Map<String, Object?> json) =>
|
||||
_$HolidayFromJson(json);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import '../bloc/holidays_state.dart';
|
||||
|
||||
class HolidaysGetHolidays extends HolidayDataLoader<List<Holiday>> {
|
||||
@override
|
||||
List<Holiday> assemble(DataLoaderResult data) => data.asListOfMaps().map(Holiday.fromJson).toList();
|
||||
List<Holiday> assemble(DataLoaderResult data) =>
|
||||
data.asListOfMaps().map(Holiday.fromJson).toList();
|
||||
|
||||
@override
|
||||
Future<Response<String>> fetch() => dio.get('/holidays/HE');
|
||||
|
||||
@@ -4,28 +4,41 @@ import '../repository/marianum_dates_repository.dart';
|
||||
import 'marianum_dates_event.dart';
|
||||
import 'marianum_dates_state.dart';
|
||||
|
||||
class MarianumDatesBloc extends LoadableHydratedBloc<MarianumDatesEvent, MarianumDatesState, MarianumDatesRepository> {
|
||||
class MarianumDatesBloc
|
||||
extends
|
||||
LoadableHydratedBloc<
|
||||
MarianumDatesEvent,
|
||||
MarianumDatesState,
|
||||
MarianumDatesRepository
|
||||
> {
|
||||
MarianumDatesBloc() {
|
||||
on<SetPastEventsVisible>((event, emit) {
|
||||
add(Emit((state) => state.copyWith(showPastEvents: event.shouldBeVisible)));
|
||||
add(
|
||||
Emit((state) => state.copyWith(showPastEvents: event.shouldBeVisible)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
bool showPastEvents() => innerState?.showPastEvents ?? false;
|
||||
|
||||
List<MarianumDate>? getEvents() => innerState?.events
|
||||
.where((e) => showPastEvents() || e.end.isAfter(DateTime.now()))
|
||||
.toList() ?? [];
|
||||
List<MarianumDate>? getEvents() =>
|
||||
innerState?.events
|
||||
.where((e) => showPastEvents() || e.end.isAfter(DateTime.now()))
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
@override
|
||||
MarianumDatesState fromNothing() => const MarianumDatesState(showPastEvents: false, events: []);
|
||||
MarianumDatesState fromNothing() =>
|
||||
const MarianumDatesState(showPastEvents: false, events: []);
|
||||
@override
|
||||
MarianumDatesState fromStorage(Map<String, dynamic> json) => MarianumDatesState.fromJson(json);
|
||||
MarianumDatesState fromStorage(Map<String, dynamic> json) =>
|
||||
MarianumDatesState.fromJson(json);
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
final events = await repo.getEvents();
|
||||
add(DataGathered((state) => state.copyWith(events: events)));
|
||||
}
|
||||
|
||||
@override
|
||||
MarianumDatesRepository repository() => MarianumDatesRepository();
|
||||
@override
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
|
||||
import 'marianum_dates_state.dart';
|
||||
|
||||
sealed class MarianumDatesEvent extends LoadableHydratedBlocEvent<MarianumDatesState> {}
|
||||
sealed class MarianumDatesEvent
|
||||
extends LoadableHydratedBlocEvent<MarianumDatesState> {}
|
||||
|
||||
class SetPastEventsVisible extends MarianumDatesEvent {
|
||||
final bool shouldBeVisible;
|
||||
|
||||
@@ -11,7 +11,8 @@ abstract class MarianumDatesState with _$MarianumDatesState {
|
||||
required List<MarianumDate> events,
|
||||
}) = _MarianumDatesState;
|
||||
|
||||
factory MarianumDatesState.fromJson(Map<String, Object?> json) => _$MarianumDatesStateFromJson(json);
|
||||
factory MarianumDatesState.fromJson(Map<String, Object?> json) =>
|
||||
_$MarianumDatesStateFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -25,5 +26,6 @@ abstract class MarianumDate with _$MarianumDate {
|
||||
required bool isAllDay,
|
||||
}) = _MarianumDate;
|
||||
|
||||
factory MarianumDate.fromJson(Map<String, Object?> json) => _$MarianumDateFromJson(json);
|
||||
factory MarianumDate.fromJson(Map<String, Object?> json) =>
|
||||
_$MarianumDateFromJson(json);
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@ import 'package:enough_icalendar/enough_icalendar.dart';
|
||||
import '../bloc/marianum_dates_state.dart';
|
||||
|
||||
class MarianumDatesGetEvents {
|
||||
static const String url = 'https://public-cal.marianumlan.de/cal_public/ad4c5da8-7466-9c72-89cb-8b8d9a5cf26c';
|
||||
static const String url =
|
||||
'https://public-cal.marianumlan.de/cal_public/ad4c5da8-7466-9c72-89cb-8b8d9a5cf26c';
|
||||
|
||||
final Dio _dio = Dio(BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
));
|
||||
final Dio _dio = Dio(
|
||||
BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
),
|
||||
);
|
||||
|
||||
Future<List<MarianumDate>> run() async {
|
||||
final response = await _dio.get<String>(url);
|
||||
@@ -20,7 +23,11 @@ class MarianumDatesGetEvents {
|
||||
final calendar = root is VCalendar ? root : null;
|
||||
final source = calendar?.children ?? root.children;
|
||||
|
||||
final events = source.whereType<VEvent>().map(_toMarianumDate).whereType<MarianumDate>().toList();
|
||||
final events = source
|
||||
.whereType<VEvent>()
|
||||
.map(_toMarianumDate)
|
||||
.whereType<MarianumDate>()
|
||||
.toList();
|
||||
events.sort((a, b) => a.start.compareTo(b.start));
|
||||
return events;
|
||||
}
|
||||
@@ -41,8 +48,11 @@ class MarianumDatesGetEvents {
|
||||
}
|
||||
|
||||
static bool _isAllDay(DateTime start, DateTime end) {
|
||||
final startMidnight = start.hour == 0 && start.minute == 0 && start.second == 0;
|
||||
final startMidnight =
|
||||
start.hour == 0 && start.minute == 0 && start.second == 0;
|
||||
final endMidnight = end.hour == 0 && end.minute == 0 && end.second == 0;
|
||||
return startMidnight && endMidnight && end.difference(start).inHours % 24 == 0;
|
||||
return startMidnight &&
|
||||
endMidnight &&
|
||||
end.difference(start).inHours % 24 == 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@ import '../repository/marianum_message_repository.dart';
|
||||
import 'marianum_message_event.dart';
|
||||
import 'marianum_message_state.dart';
|
||||
|
||||
class MarianumMessageBloc extends LoadableHydratedBloc<MarianumMessageEvent, MarianumMessageState, MarianumMessageRepository> {
|
||||
class MarianumMessageBloc
|
||||
extends
|
||||
LoadableHydratedBloc<
|
||||
MarianumMessageEvent,
|
||||
MarianumMessageState,
|
||||
MarianumMessageRepository
|
||||
> {
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
var messages = await repo.getMessages();
|
||||
@@ -15,10 +21,13 @@ class MarianumMessageBloc extends LoadableHydratedBloc<MarianumMessageEvent, Mar
|
||||
MarianumMessageRepository repository() => MarianumMessageRepository();
|
||||
|
||||
@override
|
||||
MarianumMessageState fromNothing() => const MarianumMessageState(messageList: MarianumMessageList(base: '', messages: []));
|
||||
MarianumMessageState fromNothing() => const MarianumMessageState(
|
||||
messageList: MarianumMessageList(base: '', messages: []),
|
||||
);
|
||||
|
||||
@override
|
||||
MarianumMessageState fromStorage(Map<String, dynamic> json) => MarianumMessageState.fromJson(json);
|
||||
MarianumMessageState fromStorage(Map<String, dynamic> json) =>
|
||||
MarianumMessageState.fromJson(json);
|
||||
@override
|
||||
Map<String, dynamic>? toStorage(MarianumMessageState state) => state.toJson();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
|
||||
import 'marianum_message_state.dart';
|
||||
|
||||
sealed class MarianumMessageEvent extends LoadableHydratedBlocEvent<MarianumMessageState> {}
|
||||
sealed class MarianumMessageEvent
|
||||
extends LoadableHydratedBlocEvent<MarianumMessageState> {}
|
||||
|
||||
class MessageEvent extends MarianumMessageEvent {}
|
||||
|
||||
@@ -3,14 +3,14 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'marianum_message_state.freezed.dart';
|
||||
part 'marianum_message_state.g.dart';
|
||||
|
||||
|
||||
@freezed
|
||||
abstract class MarianumMessageState with _$MarianumMessageState {
|
||||
const factory MarianumMessageState({
|
||||
required MarianumMessageList messageList,
|
||||
}) = _MarianumMessageState;
|
||||
|
||||
factory MarianumMessageState.fromJson(Map<String, dynamic> json) => _$MarianumMessageStateFromJson(json);
|
||||
factory MarianumMessageState.fromJson(Map<String, dynamic> json) =>
|
||||
_$MarianumMessageStateFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -20,7 +20,8 @@ abstract class MarianumMessageList with _$MarianumMessageList {
|
||||
required List<MarianumMessage> messages,
|
||||
}) = _MarianumMessageList;
|
||||
|
||||
factory MarianumMessageList.fromJson(Map<String, dynamic> json) => _$MarianumMessageListFromJson(json);
|
||||
factory MarianumMessageList.fromJson(Map<String, dynamic> json) =>
|
||||
_$MarianumMessageListFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -31,11 +32,8 @@ abstract class MarianumMessage with _$MarianumMessage {
|
||||
required String url,
|
||||
}) = _MarianumMessage;
|
||||
|
||||
factory MarianumMessage.fromJson(Map<String, dynamic> json) => _$MarianumMessageFromJson(json);
|
||||
factory MarianumMessage.fromJson(Map<String, dynamic> json) =>
|
||||
_$MarianumMessageFromJson(json);
|
||||
}
|
||||
|
||||
|
||||
enum GradeAveragesGradingSystem {
|
||||
highSchool,
|
||||
middleSchool,
|
||||
}
|
||||
enum GradeAveragesGradingSystem { highSchool, middleSchool }
|
||||
|
||||
+2
-1
@@ -8,5 +8,6 @@ class MarianumMessageGetMessages extends MhslDataLoader<MarianumMessageList> {
|
||||
@override
|
||||
Future<Response<String>> fetch() async => dio.get('/message/messages.json');
|
||||
@override
|
||||
MarianumMessageList assemble(DataLoaderResult data) => MarianumMessageList.fromJson(data.asMap());
|
||||
MarianumMessageList assemble(DataLoaderResult data) =>
|
||||
MarianumMessageList.fromJson(data.asMap());
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ import '../bloc/marianum_message_state.dart';
|
||||
import '../data_provider/marianum_message_get_messages.dart';
|
||||
|
||||
class MarianumMessageRepository extends Repository<MarianumMessageState> {
|
||||
Future<MarianumMessageList> getMessages() => MarianumMessageGetMessages().run();
|
||||
Future<MarianumMessageList> getMessages() =>
|
||||
MarianumMessageGetMessages().run();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@ class SettingsCubit extends HydratedCubit<Settings> {
|
||||
_emitFreshInstance();
|
||||
});
|
||||
}
|
||||
Debouncer.debounce(_debounceTag, const Duration(milliseconds: 500), _emitFreshInstance);
|
||||
Debouncer.debounce(
|
||||
_debounceTag,
|
||||
const Duration(milliseconds: 500),
|
||||
_emitFreshInstance,
|
||||
);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@@ -50,7 +54,11 @@ class SettingsCubit extends HydratedCubit<Settings> {
|
||||
return _appendNewModules(Settings.fromJson(json));
|
||||
} catch (_) {
|
||||
try {
|
||||
return _appendNewModules(Settings.fromJson(_mergeSettings(json, DefaultSettings.get().toJson())));
|
||||
return _appendNewModules(
|
||||
Settings.fromJson(
|
||||
_mergeSettings(json, DefaultSettings.get().toJson()),
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
return DefaultSettings.get();
|
||||
}
|
||||
@@ -63,7 +71,9 @@ class SettingsCubit extends HydratedCubit<Settings> {
|
||||
Settings _appendNewModules(Settings s) {
|
||||
final order = s.modulesSettings.moduleOrder;
|
||||
final hidden = s.modulesSettings.hiddenModules;
|
||||
final missing = Modules.values.where((m) => !order.contains(m) && !hidden.contains(m));
|
||||
final missing = Modules.values.where(
|
||||
(m) => !order.contains(m) && !hidden.contains(m),
|
||||
);
|
||||
if (missing.isEmpty) return s;
|
||||
s.modulesSettings.moduleOrder = [...order, ...missing];
|
||||
return s;
|
||||
@@ -72,12 +82,19 @@ class SettingsCubit extends HydratedCubit<Settings> {
|
||||
@override
|
||||
Map<String, dynamic>? toJson(Settings state) => state.toJson();
|
||||
|
||||
Map<String, dynamic> _mergeSettings(Map<String, dynamic> oldMap, Map<String, dynamic> newMap) {
|
||||
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] as Map<String, dynamic>);
|
||||
if (value is Map<String, dynamic> &&
|
||||
merged[key] is Map<String, dynamic>) {
|
||||
merged[key] = _mergeSettings(
|
||||
value,
|
||||
merged[key] as Map<String, dynamic>,
|
||||
);
|
||||
} else {
|
||||
merged[key] = value;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,13 @@ import '../repository/timetable_repository.dart';
|
||||
import 'timetable_event.dart';
|
||||
import 'timetable_state.dart';
|
||||
|
||||
class TimetableBloc extends LoadableHydratedBloc<TimetableEvent, TimetableState, TimetableRepository> {
|
||||
class TimetableBloc
|
||||
extends
|
||||
LoadableHydratedBloc<
|
||||
TimetableEvent,
|
||||
TimetableState,
|
||||
TimetableRepository
|
||||
> {
|
||||
static const Duration _weekSpan = Duration(days: 7);
|
||||
static final DateFormat _weekKeyFormat = DateFormat('yyyyMMdd');
|
||||
|
||||
@@ -37,7 +43,8 @@ class TimetableBloc extends LoadableHydratedBloc<TimetableEvent, TimetableState,
|
||||
}
|
||||
|
||||
@override
|
||||
TimetableState fromStorage(Map<String, dynamic> json) => TimetableState.fromJson(json);
|
||||
TimetableState fromStorage(Map<String, dynamic> json) =>
|
||||
TimetableState.fromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toStorage(TimetableState state) => state.toJson();
|
||||
@@ -54,7 +61,12 @@ class TimetableBloc extends LoadableHydratedBloc<TimetableEvent, TimetableState,
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
_loadCurrentWeek(initial.startDate, initial.endDate, onError: recordError, renew: renew),
|
||||
_loadCurrentWeek(
|
||||
initial.startDate,
|
||||
initial.endDate,
|
||||
onError: recordError,
|
||||
renew: renew,
|
||||
),
|
||||
_loadStaticReferenceData(onError: recordError, renew: renew),
|
||||
_loadCustomEvents(onError: recordError, renew: renew),
|
||||
]);
|
||||
@@ -104,7 +116,12 @@ class TimetableBloc extends LoadableHydratedBloc<TimetableEvent, TimetableState,
|
||||
final requestStart = DateTime.now();
|
||||
_lastWeekRequestStart = requestStart;
|
||||
try {
|
||||
final week = await repo.data.getWeek(startDate, endDate, onError: onError, renew: renew);
|
||||
final week = await repo.data.getWeek(
|
||||
startDate,
|
||||
endDate,
|
||||
onError: onError,
|
||||
renew: renew,
|
||||
);
|
||||
if (_lastWeekRequestStart.isAfter(requestStart)) return;
|
||||
_writeWeekToCache(startDate, week);
|
||||
} catch (e) {
|
||||
@@ -123,19 +140,27 @@ class TimetableBloc extends LoadableHydratedBloc<TimetableEvent, TimetableState,
|
||||
repo.data.getSchoolHolidays(onError: onError, renew: renew),
|
||||
).wait;
|
||||
|
||||
add(Emit((s) => s.copyWith(
|
||||
add(
|
||||
Emit(
|
||||
(s) => s.copyWith(
|
||||
rooms: rooms,
|
||||
subjects: subjects,
|
||||
schoolHolidays: schoolHolidays,
|
||||
dataVersion: s.dataVersion + 1,
|
||||
)));
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
}
|
||||
|
||||
try {
|
||||
final timegrid = await repo.data.getTimegrid(renew: renew);
|
||||
add(Emit((s) => s.copyWith(timegrid: timegrid, dataVersion: s.dataVersion + 1)));
|
||||
add(
|
||||
Emit(
|
||||
(s) => s.copyWith(timegrid: timegrid, dataVersion: s.dataVersion + 1),
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
// Timegrid load failure falls back to a hardcoded schedule in the UI layer.
|
||||
}
|
||||
@@ -146,8 +171,16 @@ class TimetableBloc extends LoadableHydratedBloc<TimetableEvent, TimetableState,
|
||||
bool renew = false,
|
||||
}) async {
|
||||
try {
|
||||
final events = await repo.data.getCustomEvents(renew: renew, onError: onError);
|
||||
add(Emit((s) => s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1)));
|
||||
final events = await repo.data.getCustomEvents(
|
||||
renew: renew,
|
||||
onError: onError,
|
||||
);
|
||||
add(
|
||||
Emit(
|
||||
(s) =>
|
||||
s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
}
|
||||
@@ -155,7 +188,11 @@ class TimetableBloc extends LoadableHydratedBloc<TimetableEvent, TimetableState,
|
||||
|
||||
Future<void> _refreshCustomEvents() async {
|
||||
final events = await repo.data.getCustomEvents(renew: true);
|
||||
add(DataGathered((s) => s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1)));
|
||||
add(
|
||||
DataGathered(
|
||||
(s) => s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _prefetchAdjacentWeeks(DateTime start, DateTime end) {
|
||||
@@ -164,16 +201,21 @@ class TimetableBloc extends LoadableHydratedBloc<TimetableEvent, TimetableState,
|
||||
}
|
||||
|
||||
void _prefetchWeek(DateTime start, DateTime end) {
|
||||
repo.data.getWeek(start, end).then((week) => _writeWeekToCache(start, week)).catchError((_) {});
|
||||
repo.data
|
||||
.getWeek(start, end)
|
||||
.then((week) => _writeWeekToCache(start, week))
|
||||
.catchError((_) {});
|
||||
}
|
||||
|
||||
void _writeWeekToCache(DateTime weekStart, GetTimetableResponse week) {
|
||||
final key = _weekKeyFormat.format(weekStart);
|
||||
add(Emit((s) {
|
||||
final updated = Map<String, GetTimetableResponse>.of(s.weekCache);
|
||||
updated[key] = week;
|
||||
return s.copyWith(weekCache: updated, dataVersion: s.dataVersion + 1);
|
||||
}));
|
||||
add(
|
||||
Emit((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) {
|
||||
@@ -182,7 +224,9 @@ class TimetableBloc extends LoadableHydratedBloc<TimetableEvent, TimetableState,
|
||||
}
|
||||
|
||||
static DateTime _endOfWeek(DateTime reference) {
|
||||
final friday = reference.add(Duration(days: DateTime.daysPerWeek - reference.weekday - 2));
|
||||
final friday = reference.add(
|
||||
Duration(days: DateTime.daysPerWeek - reference.weekday - 2),
|
||||
);
|
||||
return DateTime(friday.year, friday.month, friday.day);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ abstract class TimetableState with _$TimetableState {
|
||||
const TimetableState._();
|
||||
|
||||
const factory TimetableState({
|
||||
@Default(<String, GetTimetableResponse>{}) Map<String, GetTimetableResponse> weekCache,
|
||||
@Default(<String, GetTimetableResponse>{})
|
||||
Map<String, GetTimetableResponse> weekCache,
|
||||
GetRoomsResponse? rooms,
|
||||
GetSubjectsResponse? subjects,
|
||||
GetHolidaysResponse? schoolHolidays,
|
||||
@@ -26,10 +27,15 @@ abstract class TimetableState with _$TimetableState {
|
||||
@Default(0) int dataVersion,
|
||||
}) = _TimetableState;
|
||||
|
||||
factory TimetableState.fromJson(Map<String, Object?> json) => _$TimetableStateFromJson(json);
|
||||
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;
|
||||
bool get hasReferenceData =>
|
||||
rooms != null &&
|
||||
subjects != null &&
|
||||
schoolHolidays != null &&
|
||||
customEvents != null;
|
||||
}
|
||||
|
||||
@@ -31,90 +31,78 @@ class TimetableDataProvider {
|
||||
DateTime endDate, {
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) =>
|
||||
resolveFromCache<GetTimetableResponse>(
|
||||
(onUpdate, onError) => GetTimetableCache(
|
||||
startdate: int.parse(_dateFormat.format(startDate)),
|
||||
enddate: int.parse(_dateFormat.format(endDate)),
|
||||
renew: renew,
|
||||
onUpdate: onUpdate,
|
||||
onError: onError,
|
||||
),
|
||||
onError: onError,
|
||||
operationName: 'getWeek',
|
||||
);
|
||||
}) => resolveFromCache<GetTimetableResponse>(
|
||||
(onUpdate, onError) => GetTimetableCache(
|
||||
startdate: int.parse(_dateFormat.format(startDate)),
|
||||
enddate: int.parse(_dateFormat.format(endDate)),
|
||||
renew: renew,
|
||||
onUpdate: onUpdate,
|
||||
onError: onError,
|
||||
),
|
||||
onError: onError,
|
||||
operationName: 'getWeek',
|
||||
);
|
||||
|
||||
Future<GetRoomsResponse> getRooms({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) =>
|
||||
resolveFromCache<GetRoomsResponse>(
|
||||
(onUpdate, onError) => GetRoomsCache(
|
||||
renew: renew,
|
||||
onUpdate: onUpdate,
|
||||
onError: onError,
|
||||
),
|
||||
onError: onError,
|
||||
operationName: 'getRooms',
|
||||
);
|
||||
}) => resolveFromCache<GetRoomsResponse>(
|
||||
(onUpdate, onError) =>
|
||||
GetRoomsCache(renew: renew, onUpdate: onUpdate, onError: onError),
|
||||
onError: onError,
|
||||
operationName: 'getRooms',
|
||||
);
|
||||
|
||||
Future<GetSubjectsResponse> getSubjects({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) =>
|
||||
resolveFromCache<GetSubjectsResponse>(
|
||||
(onUpdate, onError) => GetSubjectsCache(
|
||||
renew: renew,
|
||||
onUpdate: onUpdate,
|
||||
onError: onError,
|
||||
),
|
||||
onError: onError,
|
||||
operationName: 'getSubjects',
|
||||
);
|
||||
}) => resolveFromCache<GetSubjectsResponse>(
|
||||
(onUpdate, onError) =>
|
||||
GetSubjectsCache(renew: renew, onUpdate: onUpdate, onError: onError),
|
||||
onError: onError,
|
||||
operationName: 'getSubjects',
|
||||
);
|
||||
|
||||
Future<GetHolidaysResponse> getSchoolHolidays({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) =>
|
||||
resolveFromCache<GetHolidaysResponse>(
|
||||
(onUpdate, onError) => GetHolidaysCache(
|
||||
renew: renew,
|
||||
onUpdate: onUpdate,
|
||||
onError: onError,
|
||||
),
|
||||
onError: onError,
|
||||
operationName: 'getSchoolHolidays',
|
||||
);
|
||||
}) => resolveFromCache<GetHolidaysResponse>(
|
||||
(onUpdate, onError) =>
|
||||
GetHolidaysCache(renew: renew, onUpdate: onUpdate, onError: onError),
|
||||
onError: onError,
|
||||
operationName: 'getSchoolHolidays',
|
||||
);
|
||||
|
||||
Future<GetTimegridUnitsResponse> getTimegrid({bool renew = false}) =>
|
||||
resolveFromCache<GetTimegridUnitsResponse>(
|
||||
(onUpdate, _) => GetTimegridUnitsCache(
|
||||
renew: renew,
|
||||
onUpdate: onUpdate,
|
||||
),
|
||||
(onUpdate, _) =>
|
||||
GetTimegridUnitsCache(renew: renew, onUpdate: onUpdate),
|
||||
operationName: 'getTimegrid',
|
||||
);
|
||||
|
||||
Future<GetCustomTimetableEventResponse> getCustomEvents({
|
||||
bool renew = false,
|
||||
void Function(Object)? onError,
|
||||
}) =>
|
||||
resolveFromCache<GetCustomTimetableEventResponse>(
|
||||
(onUpdate, onError) => GetCustomTimetableEventCache(
|
||||
GetCustomTimetableEventParams(AccountData().getUserSecret()),
|
||||
renew: renew,
|
||||
onUpdate: onUpdate,
|
||||
onError: onError,
|
||||
),
|
||||
onError: onError,
|
||||
operationName: 'getCustomEvents',
|
||||
);
|
||||
}) => resolveFromCache<GetCustomTimetableEventResponse>(
|
||||
(onUpdate, onError) => GetCustomTimetableEventCache(
|
||||
GetCustomTimetableEventParams(AccountData().getUserSecret()),
|
||||
renew: renew,
|
||||
onUpdate: onUpdate,
|
||||
onError: onError,
|
||||
),
|
||||
onError: onError,
|
||||
operationName: 'getCustomEvents',
|
||||
);
|
||||
|
||||
Future<void> addCustomEvent(CustomTimetableEvent event) =>
|
||||
AddCustomTimetableEvent(AddCustomTimetableEventParams(AccountData().getUserSecret(), event)).run();
|
||||
AddCustomTimetableEvent(
|
||||
AddCustomTimetableEventParams(AccountData().getUserSecret(), event),
|
||||
).run();
|
||||
|
||||
Future<void> updateCustomEvent(String id, CustomTimetableEvent event) =>
|
||||
UpdateCustomTimetableEvent(UpdateCustomTimetableEventParams(id, event)).run();
|
||||
UpdateCustomTimetableEvent(
|
||||
UpdateCustomTimetableEventParams(id, event),
|
||||
).run();
|
||||
|
||||
Future<void> removeCustomEvent(String id) =>
|
||||
RemoveCustomTimetableEvent(RemoveCustomTimetableEventParams(id)).run();
|
||||
|
||||
@@ -5,7 +5,8 @@ import '../data_provider/timetable_data_provider.dart';
|
||||
class TimetableRepository extends Repository<TimetableState> {
|
||||
final TimetableDataProvider _provider;
|
||||
|
||||
TimetableRepository([TimetableDataProvider? provider]) : _provider = provider ?? TimetableDataProvider();
|
||||
TimetableRepository([TimetableDataProvider? provider])
|
||||
: _provider = provider ?? TimetableDataProvider();
|
||||
|
||||
TimetableDataProvider get data => _provider;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user