dart format

This commit is contained in:
2026-05-08 20:12:40 +02:00
parent 9e139b5704
commit 3b8da1d3d6
295 changed files with 6404 additions and 4161 deletions
@@ -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);
@@ -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'),
),
],
],
),
),
),
),
);
}
}
@@ -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),
),
);
}
@@ -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,
);
}
@@ -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> {}
@@ -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);
}
+66 -17
View File
@@ -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;
}
+11 -6
View File
@@ -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 }
@@ -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;
}