diff --git a/analysis_options.yaml b/analysis_options.yaml index ebc4a78..a40338c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,10 @@ # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +analyzer: + errors: + invalid_annotation_target: ignore + linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..dcc8576 --- /dev/null +++ b/build.yaml @@ -0,0 +1,7 @@ +targets: + $default: + builders: + json_serializable: + options: + explicit_to_json: false + generic_argument_factories: true \ No newline at end of file diff --git a/lib/api/mhsl/message/getMessages/getMessages.dart b/lib/api/mhsl/message/getMessages/getMessages.dart deleted file mode 100644 index 32a5734..0000000 --- a/lib/api/mhsl/message/getMessages/getMessages.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:convert'; - -import 'package:http/http.dart' as http; - -import '../../mhslApi.dart'; -import 'getMessagesResponse.dart'; - -class GetMessages extends MhslApi { - GetMessages() : super('message/messages.json'); - - - @override - GetMessagesResponse assemble(String raw) => GetMessagesResponse.fromJson(jsonDecode(raw)); - - @override - Future request(Uri uri) => http.get(uri); -} diff --git a/lib/api/mhsl/message/getMessages/getMessagesCache.dart b/lib/api/mhsl/message/getMessages/getMessagesCache.dart deleted file mode 100644 index fcb9afe..0000000 --- a/lib/api/mhsl/message/getMessages/getMessagesCache.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:convert'; - -import '../../../requestCache.dart'; -import 'getMessages.dart'; -import 'getMessagesResponse.dart'; - -class GetMessagesCache extends RequestCache { - GetMessagesCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) { - start('message'); - } - - @override - GetMessagesResponse onLocalData(String json) => GetMessagesResponse.fromJson(jsonDecode(json)); - - @override - Future onLoad() => GetMessages().run(); -} diff --git a/lib/api/mhsl/message/getMessages/getMessagesResponse.dart b/lib/api/mhsl/message/getMessages/getMessagesResponse.dart deleted file mode 100644 index 107c492..0000000 --- a/lib/api/mhsl/message/getMessages/getMessagesResponse.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -import '../../../apiResponse.dart'; - -part 'getMessagesResponse.g.dart'; - -@JsonSerializable(explicitToJson: true) -class GetMessagesResponse extends ApiResponse { - String base; - Set messages; - - GetMessagesResponse(this.base, this.messages); - - factory GetMessagesResponse.fromJson(Map json) => _$GetMessagesResponseFromJson(json); - Map toJson() => _$GetMessagesResponseToJson(this); -} - -@JsonSerializable(explicitToJson: true) -class GetMessagesResponseObject { - String name; - String date; - String url; - - GetMessagesResponseObject(this.name, this.date, this.url); - - factory GetMessagesResponseObject.fromJson(Map json) => _$GetMessagesResponseObjectFromJson(json); - Map toJson() => _$GetMessagesResponseObjectToJson(this); -} diff --git a/lib/api/mhsl/message/getMessages/getMessagesResponse.g.dart b/lib/api/mhsl/message/getMessages/getMessagesResponse.g.dart deleted file mode 100644 index 393b020..0000000 --- a/lib/api/mhsl/message/getMessages/getMessagesResponse.g.dart +++ /dev/null @@ -1,49 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'getMessagesResponse.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -GetMessagesResponse _$GetMessagesResponseFromJson(Map json) => - GetMessagesResponse( - json['base'] as String, - (json['messages'] as List) - .map((e) => - GetMessagesResponseObject.fromJson(e as Map)) - .toSet(), - )..headers = (json['headers'] as Map?)?.map( - (k, e) => MapEntry(k, e as String), - ); - -Map _$GetMessagesResponseToJson(GetMessagesResponse instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('headers', instance.headers); - val['base'] = instance.base; - val['messages'] = instance.messages.map((e) => e.toJson()).toList(); - return val; -} - -GetMessagesResponseObject _$GetMessagesResponseObjectFromJson( - Map json) => - GetMessagesResponseObject( - json['name'] as String, - json['date'] as String, - json['url'] as String, - ); - -Map _$GetMessagesResponseObjectToJson( - GetMessagesResponseObject instance) => - { - 'name': instance.name, - 'date': instance.date, - 'url': instance.url, - }; diff --git a/lib/app.dart b/lib/app.dart index a250380..72de706 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,6 +5,7 @@ import 'dart:developer'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; +import 'state/app/modules/app_modules.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:provider/provider.dart'; import 'package:badges/badges.dart' as badges; @@ -21,10 +22,7 @@ import 'notification/notificationController.dart'; import 'notification/notificationTasks.dart'; import 'notification/notifyUpdater.dart'; import 'storage/base/settingsProvider.dart'; -import 'view/pages/files/files.dart'; import 'view/pages/overhang.dart'; -import 'view/pages/talk/chatList.dart'; -import 'view/pages/timetable/timetable.dart'; class App extends StatefulWidget { const App({super.key}); @@ -99,53 +97,34 @@ class _AppState extends State with WidgetsBindingObserver { controller: Main.bottomNavigator, navBarOverlap: const NavBarOverlap.none(), backgroundColor: Theme.of(context).colorScheme.primary, + handleAndroidBackButtonPress: false, screenTransitionAnimation: const ScreenTransitionAnimation(curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200)), tabs: [ - PersistentTabConfig( - screen: const Breaker(breaker: BreakerArea.timetable, child: Timetable()), - item: ItemConfig( - activeForegroundColor: Theme.of(context).primaryColor, - inactiveForegroundColor: Theme.of(context).colorScheme.secondary, - icon: const Icon(Icons.calendar_month), - title: 'Vertretung' - ), - ), - PersistentTabConfig( - screen: const Breaker(breaker: BreakerArea.talk, child: ChatList()), - item: ItemConfig( - activeForegroundColor: Theme.of(context).primaryColor, - inactiveForegroundColor: Theme.of(context).colorScheme.secondary, - icon: Consumer( - builder: (context, value, child) { - if(value.primaryLoading()) return const Icon(Icons.chat); - var messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b); - return badges.Badge( - showBadge: messages > 0, - position: badges.BadgePosition.topEnd(top: -3, end: -3), - stackFit: StackFit.loose, - badgeStyle: badges.BadgeStyle( - padding: const EdgeInsets.all(3), - badgeColor: Theme.of(context).primaryColor, - elevation: 1, - ), - badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)), - child: const Icon(Icons.chat), - ); - }, - ), - title: 'Talk', - ), - ), - PersistentTabConfig( - screen: const Breaker(breaker: BreakerArea.files, child: Files([])), - item: ItemConfig( - activeForegroundColor: Theme.of(context).primaryColor, - inactiveForegroundColor: Theme.of(context).colorScheme.secondary, - icon: const Icon(Icons.folder), - title: 'Dateien' + AppModule.getModule(Modules.timetable).toBottomTab(context), + AppModule.getModule(Modules.talk).toBottomTab( + context, + itemBuilder: (icon) => Consumer( + builder: (context, value, child) { + if(value.primaryLoading()) return Icon(icon); + var messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b); + return badges.Badge( + showBadge: messages > 0, + position: badges.BadgePosition.topEnd(top: -3, end: -3), + stackFit: StackFit.loose, + badgeStyle: badges.BadgeStyle( + padding: const EdgeInsets.all(3), + badgeColor: Theme.of(context).primaryColor, + elevation: 1, + ), + badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)), + child: Icon(icon), + ); + }, ), ), + AppModule.getModule(Modules.files).toBottomTab(context), + PersistentTabConfig( screen: const Breaker(breaker: BreakerArea.more, child: Overhang()), item: ItemConfig( diff --git a/lib/main.dart b/lib/main.dart index 0e1aa43..151f10e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,8 +7,10 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:jiffy/jiffy.dart'; import 'package:loader_overlay/loader_overlay.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:provider/provider.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -24,7 +26,6 @@ import 'model/chatList/chatListProps.dart'; import 'model/chatList/chatProps.dart'; import 'model/files/filesProps.dart'; import 'model/holidays/holidaysProps.dart'; -import 'model/message/messageProps.dart'; import 'model/timetable/timetableProps.dart'; import 'storage/base/settingsProvider.dart'; import 'theming/darkAppTheme.dart'; @@ -35,21 +36,26 @@ import 'widget/placeholderView.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - try { - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - log("Firebase token: ${await FirebaseMessaging.instance.getToken() ?? "Error: no Firebase token!"}"); - } catch (e) { - log('Error initializing Firebase app!'); - } + var initialisationTasks = [ + Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform) + .then((value) async => log("Firebase token: ${await FirebaseMessaging.instance.getToken() ?? "Error: no Firebase token!"}")) + .onError((error, stackTrace) => log('Error initializing Firebase: $error')), - var data = await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem'); - SecurityContext.defaultContext.setTrustedCertificatesBytes(data.buffer.asUint8List()); + PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem') + .then((certificate) => SecurityContext.defaultContext.setTrustedCertificatesBytes(certificate.buffer.asUint8List())), + + Future(() async { + await HydratedStorage.build(storageDirectory: await getTemporaryDirectory()).then((storage) => HydratedBloc.storage = storage); + }) + ]; + + await Future.wait(initialisationTasks); if(kReleaseMode) { ErrorWidget.builder = (error) => PlaceholderView( - icon: Icons.phonelink_erase_rounded, - text: error.toStringShort(), - ); + icon: Icons.phonelink_erase_rounded, + text: error.toStringShort(), + ); } runApp( @@ -65,7 +71,6 @@ Future main() async { ChangeNotifierProvider(create: (context) => ChatProps()), ChangeNotifierProvider(create: (context) => FilesProps()), - ChangeNotifierProvider(create: (context) => MessageProps()), ChangeNotifierProvider(create: (context) => HolidaysProps()), ], child: const Main(), diff --git a/lib/model/message/messageProps.dart b/lib/model/message/messageProps.dart deleted file mode 100644 index c2e2a37..0000000 --- a/lib/model/message/messageProps.dart +++ /dev/null @@ -1,25 +0,0 @@ - -import '../../api/apiResponse.dart'; -import '../../api/mhsl/message/getMessages/getMessagesCache.dart'; -import '../../api/mhsl/message/getMessages/getMessagesResponse.dart'; -import '../dataHolder.dart'; - -class MessageProps extends DataHolder { - GetMessagesResponse? _getMessagesResponse; - GetMessagesResponse get getMessagesResponse => _getMessagesResponse!; - - @override - List properties() => [_getMessagesResponse]; - - @override - void run({renew}) { - GetMessagesCache( - renew: renew, - onUpdate: (GetMessagesResponse data) => { - _getMessagesResponse = data, - notifyListeners(), - } - ); - } - -} diff --git a/lib/state/app/infrastructure/dataLoader/data_loader.dart b/lib/state/app/infrastructure/dataLoader/data_loader.dart new file mode 100644 index 0000000..cb4a3fe --- /dev/null +++ b/lib/state/app/infrastructure/dataLoader/data_loader.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:dio/dio.dart'; + +abstract class DataLoader { + final Dio dio; + DataLoader(this.dio) { + dio.options.connectTimeout = const Duration(seconds: 10).inMilliseconds; + dio.options.sendTimeout = const Duration(seconds: 30).inMilliseconds; + dio.options.receiveTimeout = const Duration(seconds: 30).inMilliseconds; + } + + Future run() async { + var fetcher = fetch(); + await Future.wait([ + fetcher, + Future.delayed(const Duration(milliseconds: 500)) // TODO tune or remove + ]); + + var response = await fetcher; + try { + return assemble(DataLoaderResult( + json: jsonDecode(response.data!), + headers: response.headers.map.map((key, value) => MapEntry(key, value.join(';'))), + )); + } catch(trace, e) { + log(trace.toString()); + throw(e); + } + } + + Future> fetch(); + TResult assemble(DataLoaderResult data); +} + +class DataLoaderResult { + final Map json; + final Map headers; + + DataLoaderResult({required this.json, required this.headers}); +} diff --git a/lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart b/lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart new file mode 100644 index 0000000..0825587 --- /dev/null +++ b/lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart @@ -0,0 +1,9 @@ +import 'package:dio/dio.dart'; + +import 'data_loader.dart'; + +abstract class MhslDataLoader extends DataLoader { + MhslDataLoader() : super(Dio(BaseOptions( + baseUrl: 'https://mhsl.eu/marianum/marianummobile/' + ))); +} diff --git a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart new file mode 100644 index 0000000..b246417 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart @@ -0,0 +1,55 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; + +import 'loadable_state_event.dart'; +import 'loadable_state_state.dart'; + +class LoadableStateBloc extends Bloc { + late StreamSubscription> _updateStream; + void Function()? reFetch; + + LoadableStateBloc() : super(const LoadableStateState(connections: null)) { + on((event, emit) { + emit(event.state); + if(connectivityStatusKnown() && isConnected()) { + if(reFetch == null) return; + reFetch!(); + } + }); + + emitConnectivity(List result) => add(ConnectivityChanged(LoadableStateState(connections: result))); + + Connectivity().checkConnectivity().then(emitConnectivity); + _updateStream = Connectivity().onConnectivityChanged.listen(emitConnectivity); + } + + bool connectivityStatusKnown() => state.connections != null; + 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.device_unknown; + + Color connectionColor(BuildContext context) => connectivityStatusKnown() && !isConnected() + ? Colors.grey.shade600 + : Theme.of(context).primaryColor; + + String connectionText({int? lastUpdated}) => connectivityStatusKnown() + ? isConnected() + ? 'Verbindung fehlgeschlagen' + : 'Offline${lastUpdated == null ? '' : ' - Stand von ${Jiffy.parseFromMillisecondsSinceEpoch(lastUpdated).fromNow()}'}' + : 'Unbekannte Fehlerursache'; + + @override + Future close() { + _updateStream.cancel(); + return super.close(); + } +} diff --git a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_event.dart b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_event.dart new file mode 100644 index 0000000..80f3791 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_event.dart @@ -0,0 +1,7 @@ +import 'loadable_state_state.dart'; + +sealed class LoadableStateEvent {} +final class ConnectivityChanged extends LoadableStateEvent { + final LoadableStateState state; + ConnectivityChanged(this.state); +} diff --git a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_state.dart b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_state.dart new file mode 100644 index 0000000..712c9fd --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_state.dart @@ -0,0 +1,11 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'loadable_state_state.freezed.dart'; + +@freezed +class LoadableStateState with _$LoadableStateState { + const factory LoadableStateState({ + required List? connections, + }) = _LoadableStateState; +} diff --git a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_state.freezed.dart b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_state.freezed.dart new file mode 100644 index 0000000..66011f9 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_state.freezed.dart @@ -0,0 +1,147 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'loadable_state_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$LoadableStateState { + List? get connections => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $LoadableStateStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoadableStateStateCopyWith<$Res> { + factory $LoadableStateStateCopyWith( + LoadableStateState value, $Res Function(LoadableStateState) then) = + _$LoadableStateStateCopyWithImpl<$Res, LoadableStateState>; + @useResult + $Res call({List? connections}); +} + +/// @nodoc +class _$LoadableStateStateCopyWithImpl<$Res, $Val extends LoadableStateState> + implements $LoadableStateStateCopyWith<$Res> { + _$LoadableStateStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? connections = freezed, + }) { + return _then(_value.copyWith( + connections: freezed == connections + ? _value.connections + : connections // ignore: cast_nullable_to_non_nullable + as List?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LoadableStateStateImplCopyWith<$Res> + implements $LoadableStateStateCopyWith<$Res> { + factory _$$LoadableStateStateImplCopyWith(_$LoadableStateStateImpl value, + $Res Function(_$LoadableStateStateImpl) then) = + __$$LoadableStateStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List? connections}); +} + +/// @nodoc +class __$$LoadableStateStateImplCopyWithImpl<$Res> + extends _$LoadableStateStateCopyWithImpl<$Res, _$LoadableStateStateImpl> + implements _$$LoadableStateStateImplCopyWith<$Res> { + __$$LoadableStateStateImplCopyWithImpl(_$LoadableStateStateImpl _value, + $Res Function(_$LoadableStateStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? connections = freezed, + }) { + return _then(_$LoadableStateStateImpl( + connections: freezed == connections + ? _value._connections + : connections // ignore: cast_nullable_to_non_nullable + as List?, + )); + } +} + +/// @nodoc + +class _$LoadableStateStateImpl implements _LoadableStateState { + const _$LoadableStateStateImpl( + {required final List? connections}) + : _connections = connections; + + final List? _connections; + @override + List? get connections { + final value = _connections; + if (value == null) return null; + if (_connections is EqualUnmodifiableListView) return _connections; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'LoadableStateState(connections: $connections)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoadableStateStateImpl && + const DeepCollectionEquality() + .equals(other._connections, _connections)); + } + + @override + int get hashCode => Object.hash( + runtimeType, const DeepCollectionEquality().hash(_connections)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LoadableStateStateImplCopyWith<_$LoadableStateStateImpl> get copyWith => + __$$LoadableStateStateImplCopyWithImpl<_$LoadableStateStateImpl>( + this, _$identity); +} + +abstract class _LoadableStateState implements LoadableStateState { + const factory _LoadableStateState( + {required final List? connections}) = + _$LoadableStateStateImpl; + + @override + List? get connections; + @override + @JsonKey(ignore: true) + _$$LoadableStateStateImplCopyWith<_$LoadableStateStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/state/app/infrastructure/loadableState/loadable_state.dart b/lib/state/app/infrastructure/loadableState/loadable_state.dart new file mode 100644 index 0000000..ac1626a --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/loadable_state.dart @@ -0,0 +1,27 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'loading_error.dart'; + +part 'loadable_state.freezed.dart'; + +@freezed +class LoadableState with _$LoadableState { + const LoadableState._(); + + const factory LoadableState({ + @Default(true) bool isLoading, + @Default(null) TState? data, + @Default(null) int? lastFetch, + @Default(null) void Function()? reFetch, + @Default(null) LoadingError? error, + }) = _LoadableState; + + bool _hasError() => error != null; + bool _hasData() => data != null; + + bool showPrimaryLoading() => isLoading && !_hasData(); + bool showBackgroundLoading() => isLoading && _hasData(); + bool showErrorBar() => _hasError() && _hasData(); + bool showError() => _hasError() && !_hasData(); + bool showContent() => _hasData(); +} diff --git a/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart b/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart new file mode 100644 index 0000000..e66b4a8 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart @@ -0,0 +1,246 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'loadable_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$LoadableState { + bool get isLoading => throw _privateConstructorUsedError; + TState? get data => throw _privateConstructorUsedError; + int? get lastFetch => throw _privateConstructorUsedError; + void Function()? get reFetch => throw _privateConstructorUsedError; + LoadingError? get error => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $LoadableStateCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoadableStateCopyWith { + factory $LoadableStateCopyWith(LoadableState value, + $Res Function(LoadableState) then) = + _$LoadableStateCopyWithImpl>; + @useResult + $Res call( + {bool isLoading, + TState? data, + int? lastFetch, + void Function()? reFetch, + LoadingError? error}); + + $LoadingErrorCopyWith<$Res>? get error; +} + +/// @nodoc +class _$LoadableStateCopyWithImpl> + implements $LoadableStateCopyWith { + _$LoadableStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isLoading = null, + Object? data = freezed, + Object? lastFetch = freezed, + Object? reFetch = freezed, + Object? error = freezed, + }) { + return _then(_value.copyWith( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as TState?, + lastFetch: freezed == lastFetch + ? _value.lastFetch + : lastFetch // ignore: cast_nullable_to_non_nullable + as int?, + reFetch: freezed == reFetch + ? _value.reFetch + : reFetch // ignore: cast_nullable_to_non_nullable + as void Function()?, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as LoadingError?, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $LoadingErrorCopyWith<$Res>? get error { + if (_value.error == null) { + return null; + } + + return $LoadingErrorCopyWith<$Res>(_value.error!, (value) { + return _then(_value.copyWith(error: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$LoadableStateImplCopyWith + implements $LoadableStateCopyWith { + factory _$$LoadableStateImplCopyWith(_$LoadableStateImpl value, + $Res Function(_$LoadableStateImpl) then) = + __$$LoadableStateImplCopyWithImpl; + @override + @useResult + $Res call( + {bool isLoading, + TState? data, + int? lastFetch, + void Function()? reFetch, + LoadingError? error}); + + @override + $LoadingErrorCopyWith<$Res>? get error; +} + +/// @nodoc +class __$$LoadableStateImplCopyWithImpl + extends _$LoadableStateCopyWithImpl> + implements _$$LoadableStateImplCopyWith { + __$$LoadableStateImplCopyWithImpl(_$LoadableStateImpl _value, + $Res Function(_$LoadableStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isLoading = null, + Object? data = freezed, + Object? lastFetch = freezed, + Object? reFetch = freezed, + Object? error = freezed, + }) { + return _then(_$LoadableStateImpl( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as TState?, + lastFetch: freezed == lastFetch + ? _value.lastFetch + : lastFetch // ignore: cast_nullable_to_non_nullable + as int?, + reFetch: freezed == reFetch + ? _value.reFetch + : reFetch // ignore: cast_nullable_to_non_nullable + as void Function()?, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as LoadingError?, + )); + } +} + +/// @nodoc + +class _$LoadableStateImpl extends _LoadableState { + const _$LoadableStateImpl( + {this.isLoading = true, + this.data = null, + this.lastFetch = null, + this.reFetch = null, + this.error = null}) + : super._(); + + @override + @JsonKey() + final bool isLoading; + @override + @JsonKey() + final TState? data; + @override + @JsonKey() + final int? lastFetch; + @override + @JsonKey() + final void Function()? reFetch; + @override + @JsonKey() + final LoadingError? error; + + @override + String toString() { + return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoadableStateImpl && + (identical(other.isLoading, isLoading) || + other.isLoading == isLoading) && + const DeepCollectionEquality().equals(other.data, data) && + (identical(other.lastFetch, lastFetch) || + other.lastFetch == lastFetch) && + (identical(other.reFetch, reFetch) || other.reFetch == reFetch) && + (identical(other.error, error) || other.error == error)); + } + + @override + int get hashCode => Object.hash(runtimeType, isLoading, + const DeepCollectionEquality().hash(data), lastFetch, reFetch, error); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LoadableStateImplCopyWith> + get copyWith => __$$LoadableStateImplCopyWithImpl>(this, _$identity); +} + +abstract class _LoadableState extends LoadableState { + const factory _LoadableState( + {final bool isLoading, + final TState? data, + final int? lastFetch, + final void Function()? reFetch, + final LoadingError? error}) = _$LoadableStateImpl; + const _LoadableState._() : super._(); + + @override + bool get isLoading; + @override + TState? get data; + @override + int? get lastFetch; + @override + void Function()? get reFetch; + @override + LoadingError? get error; + @override + @JsonKey(ignore: true) + _$$LoadableStateImplCopyWith> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/state/app/infrastructure/loadableState/loading_error.dart b/lib/state/app/infrastructure/loadableState/loading_error.dart new file mode 100644 index 0000000..b68ac7d --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/loading_error.dart @@ -0,0 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'loading_error.freezed.dart'; + +@freezed +class LoadingError with _$LoadingError { + const factory LoadingError({ + required String message, + @Default(false) bool allowRetry, + }) = _LoadingError; +} diff --git a/lib/state/app/infrastructure/loadableState/loading_error.freezed.dart b/lib/state/app/infrastructure/loadableState/loading_error.freezed.dart new file mode 100644 index 0000000..6515a46 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/loading_error.freezed.dart @@ -0,0 +1,152 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'loading_error.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$LoadingError { + String get message => throw _privateConstructorUsedError; + bool get allowRetry => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $LoadingErrorCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoadingErrorCopyWith<$Res> { + factory $LoadingErrorCopyWith( + LoadingError value, $Res Function(LoadingError) then) = + _$LoadingErrorCopyWithImpl<$Res, LoadingError>; + @useResult + $Res call({String message, bool allowRetry}); +} + +/// @nodoc +class _$LoadingErrorCopyWithImpl<$Res, $Val extends LoadingError> + implements $LoadingErrorCopyWith<$Res> { + _$LoadingErrorCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + Object? allowRetry = null, + }) { + return _then(_value.copyWith( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + allowRetry: null == allowRetry + ? _value.allowRetry + : allowRetry // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LoadingErrorImplCopyWith<$Res> + implements $LoadingErrorCopyWith<$Res> { + factory _$$LoadingErrorImplCopyWith( + _$LoadingErrorImpl value, $Res Function(_$LoadingErrorImpl) then) = + __$$LoadingErrorImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String message, bool allowRetry}); +} + +/// @nodoc +class __$$LoadingErrorImplCopyWithImpl<$Res> + extends _$LoadingErrorCopyWithImpl<$Res, _$LoadingErrorImpl> + implements _$$LoadingErrorImplCopyWith<$Res> { + __$$LoadingErrorImplCopyWithImpl( + _$LoadingErrorImpl _value, $Res Function(_$LoadingErrorImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + Object? allowRetry = null, + }) { + return _then(_$LoadingErrorImpl( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + allowRetry: null == allowRetry + ? _value.allowRetry + : allowRetry // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$LoadingErrorImpl implements _LoadingError { + const _$LoadingErrorImpl({required this.message, this.allowRetry = false}); + + @override + final String message; + @override + @JsonKey() + final bool allowRetry; + + @override + String toString() { + return 'LoadingError(message: $message, allowRetry: $allowRetry)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoadingErrorImpl && + (identical(other.message, message) || other.message == message) && + (identical(other.allowRetry, allowRetry) || + other.allowRetry == allowRetry)); + } + + @override + int get hashCode => Object.hash(runtimeType, message, allowRetry); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LoadingErrorImplCopyWith<_$LoadingErrorImpl> get copyWith => + __$$LoadingErrorImplCopyWithImpl<_$LoadingErrorImpl>(this, _$identity); +} + +abstract class _LoadingError implements LoadingError { + const factory _LoadingError( + {required final String message, + final bool allowRetry}) = _$LoadingErrorImpl; + + @override + String get message; + @override + bool get allowRetry; + @override + @JsonKey(ignore: true) + _$$LoadingErrorImplCopyWith<_$LoadingErrorImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_background_loading.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_background_loading.dart new file mode 100644 index 0000000..5147e45 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_background_loading.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +import 'loadable_state_consumer.dart'; + +class LoadableStateBackgroundLoading extends StatelessWidget { + final bool visible; + const LoadableStateBackgroundLoading({required this.visible, super.key}); + + @override + Widget build(BuildContext context) => AnimatedSwitcher( + duration: LoadableStateConsumer.animationDuration, + transitionBuilder: (Widget child, Animation animation) => SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: visible ? const LinearProgressIndicator() : const SizedBox.shrink(), + ); +} diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart new file mode 100644 index 0000000..7d9244e --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart @@ -0,0 +1,78 @@ +import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../widget/conditional_wrapper.dart'; +import '../../utilityWidgets/bloc_module.dart'; +import '../../utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; +import '../bloc/loadable_state_bloc.dart'; +import '../bloc/loadable_state_state.dart'; +import '../loadable_state.dart'; +import 'loadable_state_background_loading.dart'; +import 'loadable_state_error_bar.dart'; +import 'loadable_state_error_screen.dart'; +import 'loadable_state_primary_loading.dart'; + +class LoadableStateConsumer, LoadableState>, TState> extends StatelessWidget { + final Widget Function(TState state, bool loading) child; + final bool wrapWithScrollView; + const LoadableStateConsumer({required this.child, this.wrapWithScrollView = false, super.key}); + + static Duration animationDuration = const Duration(milliseconds: 200); + + @override + Widget build(BuildContext context) { + var loadableState = context.watch().state; + var childWidget = ConditionalWrapper( + condition: loadableState.reFetch != null, + wrapper: (child) => RefreshIndicator( + onRefresh: () { + if(loadableState.reFetch != null) loadableState.reFetch!(); + return Future.value(); + }, + child: ConditionalWrapper( + condition: wrapWithScrollView, + wrapper: (child) => SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: child + ), + child: child, + ) + ), + child: SizedBox( + height: MediaQuery.of(context).size.height, + child: loadableState.showContent() + ? child(loadableState.data, loadableState.isLoading) + : const SizedBox.shrink(), + ), + ); + + return BlocModule( + create: (context) => LoadableStateBloc(), + child: (context, bloc, state) { + bloc.reFetch = loadableState.reFetch; + return Column( + children: [ + LoadableStateErrorBar(visible: loadableState.showErrorBar(), message: loadableState.error?.message, lastUpdated: loadableState.lastFetch), + Expanded( + child: Stack( + children: [ + LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()), + LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()), + LoadableStateErrorScreen(visible: loadableState.showError(), message: loadableState.error?.message), + + AnimatedOpacity( + opacity: loadableState.showContent() ? 1.0 : 0.0, + duration: animationDuration, + curve: Curves.easeInOut, + child: childWidget, + ), + ], + ), + ) + ], + ); + } + ); + } +} diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart new file mode 100644 index 0000000..bc5353c --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart @@ -0,0 +1,92 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../../widget/infoDialog.dart'; +import '../bloc/loadable_state_bloc.dart'; + +class LoadableStateErrorBar extends StatelessWidget { + final bool visible; + final String? message; + final int? lastUpdated; + const LoadableStateErrorBar({required this.visible, this.message, this.lastUpdated, super.key}); + + final Duration animationDuration = const Duration(milliseconds: 200); + + @override + Widget build(BuildContext context) => AnimatedSize( + duration: animationDuration, + child: AnimatedSwitcher( + duration: animationDuration, + transitionBuilder: (Widget child, Animation animation) => SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: Visibility( + key: Key(visible.hashCode.toString()), + visible: visible, + replacement: const SizedBox(width: double.infinity), + child: Builder( + builder: (context) { + var bloc = context.watch(); + return InkWell( + onTap: () { + if(!bloc.isConnected()) return; + InfoDialog.show(context, 'Exception: ${message.toString()}'); + }, + child: Container( + height: 20, + decoration: BoxDecoration( + color: bloc.connectionColor(context), + ), + child: LoadableStateErrorBarText(lastUpdated: lastUpdated), + ), + ); + }, + ) + ) + ), + ); +} + +class LoadableStateErrorBarText extends StatefulWidget { + final int? lastUpdated; + const LoadableStateErrorBarText({required this.lastUpdated, super.key}); + + @override + State createState() => _LoadableStateErrorBarTextState(); +} + +class _LoadableStateErrorBarTextState extends State { + late Timer _rebuildTimer; + @override + void initState() { + _rebuildTimer = Timer.periodic(const Duration(seconds: 10), (timer) => setState(() {})); + super.initState(); + } + + @override + Widget build(BuildContext context) { + var bloc = context.watch(); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(bloc.connectionIcon(), size: 14), + const SizedBox(width: 10), + Text(bloc.connectionText(lastUpdated: widget.lastUpdated), style: const TextStyle(fontSize: 12)) + ], + ); + } + + @override + void dispose() { + _rebuildTimer.cancel(); + super.dispose(); + } +} + diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart new file mode 100644 index 0000000..cb68e76 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../bloc/loadable_state_bloc.dart'; +import 'loadable_state_consumer.dart'; + +class LoadableStateErrorScreen extends StatelessWidget { + final bool visible; + final String? message; + const LoadableStateErrorScreen({required this.visible, this.message, super.key}); + + + @override + Widget build(BuildContext context) { + final bloc = context.watch(); + return AnimatedOpacity( + opacity: visible ? 1.0 : 0.0, + duration: LoadableStateConsumer.animationDuration, + curve: Curves.easeInOut, + child: !visible ? null : Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(bloc.connectionIcon(), size: 40), + const SizedBox(height: 10), + Text(bloc.connectionText(), style: const TextStyle(fontSize: 20)), + + if(bloc.allowRetry()) ...[ + const SizedBox(height: 10), + TextButton(onPressed: () => bloc.reFetch!(), child: const Text('Erneut versuschen')), + const SizedBox(height: 40), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + message ?? 'Task failed successfully :)', + style: TextStyle( + color: Theme.of(context).hintColor, + fontSize: 12 + ), + maxLines: 10, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_primary_loading.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_primary_loading.dart new file mode 100644 index 0000000..053aaba --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_primary_loading.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +import 'loadable_state_consumer.dart'; + +class LoadableStatePrimaryLoading extends StatelessWidget { + final bool visible; + const LoadableStatePrimaryLoading({required this.visible, super.key}); + + @override + Widget build(BuildContext context) => AnimatedOpacity( + opacity: visible ? 1.0 : 0.0, + duration: LoadableStateConsumer.animationDuration, + curve: Curves.easeInOut, + child: const Center(child: CircularProgressIndicator()), + ); +} diff --git a/lib/state/app/infrastructure/repository/repository.dart b/lib/state/app/infrastructure/repository/repository.dart new file mode 100644 index 0000000..58a4e85 --- /dev/null +++ b/lib/state/app/infrastructure/repository/repository.dart @@ -0,0 +1 @@ +abstract class Repository {} diff --git a/lib/state/app/infrastructure/utilityWidgets/bloc_module.dart b/lib/state/app/infrastructure/utilityWidgets/bloc_module.dart new file mode 100644 index 0000000..35297b7 --- /dev/null +++ b/lib/state/app/infrastructure/utilityWidgets/bloc_module.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class BlocModule, TState> extends StatelessWidget { + final TBloc Function(BuildContext context) create; + final Widget Function(BuildContext context, TBloc bloc, TState state) child; + final bool autoRebuild; + const BlocModule({required this.create, required this.child, this.autoRebuild = false, super.key}); + + Widget rebuildChild(BuildContext context) => child(context, context.watch(), context.watch().state); + Widget staticChild(BuildContext context) => child(context, context.read(), context.read().state); + + @override + Widget build(BuildContext context) => BlocProvider( + create: create, + child: Builder( + builder: (context) => autoRebuild + ? rebuildChild(context) + : staticChild(context) + ) + ); +} diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart new file mode 100644 index 0000000..116bad2 --- /dev/null +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart @@ -0,0 +1,91 @@ +import 'dart:developer'; + +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import '../../loadableState/loading_error.dart'; +import '../../repository/repository.dart'; +import 'loadable_hydrated_bloc_event.dart'; +import '../../loadableState/loadable_state.dart'; +import 'loadable_save_context.dart'; + +abstract class LoadableHydratedBloc< + TEvent extends LoadableHydratedBlocEvent, + TState, + TRepository extends Repository +> extends HydratedBloc< + LoadableHydratedBlocEvent, + LoadableState +> { + late TRepository _repository; + LoadableHydratedBloc() : super(const LoadableState()) { + + on>((event, emit) => emit(LoadableState( + isLoading: event.loading, + data: event.state(innerState ?? fromNothing()), + lastFetch: DateTime.now().millisecondsSinceEpoch, + reFetch: retry + ))); + + on>((event, emit) => emit(LoadableState( + isLoading: true, + data: innerState, + lastFetch: state.lastFetch + ))); + + on>((event, emit) => emit(const LoadableState())); + + on>((event, emit) => emit(LoadableState( + isLoading: false, + data: innerState, + lastFetch: state.lastFetch, + reFetch: retry, + error: event.error + ))); + + _repository = repository(); + fetch(); + } + + TState? get innerState => state.data; + TRepository get repo => _repository; + + void retry() { + log('Fetch retry triggered for ${TState.toString()}'); + add(RefetchStarted()); + fetch(); + } + + void fetch() { + log('Fetching data for ${TState.toString()}'); + gatherData().catchError( + (e) { + log('Error while fetching ${TState.toString()}: ${e.toString()}'); + add(Error(LoadingError( + message: e.message, + allowRetry: true, + ))); + }, + ).then((value) { + log('Fetch for ${TState.toString()} completed!'); + }); + } + + @override + fromJson(Map json) { + var rawData = LoadableSaveContext.unwrap(json); + return LoadableState(isLoading: true, lastFetch: rawData.meta.timestamp, data: fromStorage(rawData.data)); + } + + @override + Map? toJson(LoadableState state) => LoadableSaveContext.wrap( + toStorage(state.data), + state.lastFetch ?? DateTime.now().millisecondsSinceEpoch + ); + + Future gatherData(); + TRepository repository(); + + TState fromNothing(); + TState fromStorage(Map json); + Map? toStorage(TState state); +} diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart new file mode 100644 index 0000000..06462de --- /dev/null +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart @@ -0,0 +1,14 @@ +import '../../loadableState/loading_error.dart'; + +class LoadableHydratedBlocEvent {} +class Emit extends LoadableHydratedBlocEvent { + final TState Function(TState state) state; + final bool loading; + Emit(this.state, {this.loading = false}); +} +class ClearState extends LoadableHydratedBlocEvent {} +class Error extends LoadableHydratedBlocEvent { + final LoadingError error; + Error(this.error); +} +class RefetchStarted extends LoadableHydratedBlocEvent {} diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.dart new file mode 100644 index 0000000..e4d68d5 --- /dev/null +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.dart @@ -0,0 +1,24 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'loadable_save_context.freezed.dart'; +part 'loadable_save_context.g.dart'; + +@freezed +class LoadableSaveContext with _$LoadableSaveContext { + const LoadableSaveContext._(); + const factory LoadableSaveContext({ + required int timestamp, + }) = _LoadableSaveContext; + + factory LoadableSaveContext.fromJson(Map json) => _$LoadableSaveContextFromJson(json); + + static String dataKey = 'data'; + static String metaKey = 'meta'; + + static Map wrap(Map? data, int lastFetch) => + {dataKey: data, metaKey: LoadableSaveContext(timestamp: lastFetch).toJson()}; + + static ({Map data, LoadableSaveContext meta}) unwrap(Map data) => + (data: data[dataKey] as Map, meta: LoadableSaveContext.fromJson(data[metaKey])); +} + diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.freezed.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.freezed.dart new file mode 100644 index 0000000..649526e --- /dev/null +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.freezed.dart @@ -0,0 +1,155 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'loadable_save_context.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +LoadableSaveContext _$LoadableSaveContextFromJson(Map json) { + return _LoadableSaveContext.fromJson(json); +} + +/// @nodoc +mixin _$LoadableSaveContext { + int get timestamp => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $LoadableSaveContextCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoadableSaveContextCopyWith<$Res> { + factory $LoadableSaveContextCopyWith( + LoadableSaveContext value, $Res Function(LoadableSaveContext) then) = + _$LoadableSaveContextCopyWithImpl<$Res, LoadableSaveContext>; + @useResult + $Res call({int timestamp}); +} + +/// @nodoc +class _$LoadableSaveContextCopyWithImpl<$Res, $Val extends LoadableSaveContext> + implements $LoadableSaveContextCopyWith<$Res> { + _$LoadableSaveContextCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? timestamp = null, + }) { + return _then(_value.copyWith( + timestamp: null == timestamp + ? _value.timestamp + : timestamp // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LoadableSaveContextImplCopyWith<$Res> + implements $LoadableSaveContextCopyWith<$Res> { + factory _$$LoadableSaveContextImplCopyWith(_$LoadableSaveContextImpl value, + $Res Function(_$LoadableSaveContextImpl) then) = + __$$LoadableSaveContextImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int timestamp}); +} + +/// @nodoc +class __$$LoadableSaveContextImplCopyWithImpl<$Res> + extends _$LoadableSaveContextCopyWithImpl<$Res, _$LoadableSaveContextImpl> + implements _$$LoadableSaveContextImplCopyWith<$Res> { + __$$LoadableSaveContextImplCopyWithImpl(_$LoadableSaveContextImpl _value, + $Res Function(_$LoadableSaveContextImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? timestamp = null, + }) { + return _then(_$LoadableSaveContextImpl( + timestamp: null == timestamp + ? _value.timestamp + : timestamp // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$LoadableSaveContextImpl extends _LoadableSaveContext { + const _$LoadableSaveContextImpl({required this.timestamp}) : super._(); + + factory _$LoadableSaveContextImpl.fromJson(Map json) => + _$$LoadableSaveContextImplFromJson(json); + + @override + final int timestamp; + + @override + String toString() { + return 'LoadableSaveContext(timestamp: $timestamp)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoadableSaveContextImpl && + (identical(other.timestamp, timestamp) || + other.timestamp == timestamp)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, timestamp); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LoadableSaveContextImplCopyWith<_$LoadableSaveContextImpl> get copyWith => + __$$LoadableSaveContextImplCopyWithImpl<_$LoadableSaveContextImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$LoadableSaveContextImplToJson( + this, + ); + } +} + +abstract class _LoadableSaveContext extends LoadableSaveContext { + const factory _LoadableSaveContext({required final int timestamp}) = + _$LoadableSaveContextImpl; + const _LoadableSaveContext._() : super._(); + + factory _LoadableSaveContext.fromJson(Map json) = + _$LoadableSaveContextImpl.fromJson; + + @override + int get timestamp; + @override + @JsonKey(ignore: true) + _$$LoadableSaveContextImplCopyWith<_$LoadableSaveContextImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.g.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.g.dart new file mode 100644 index 0000000..c536225 --- /dev/null +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'loadable_save_context.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$LoadableSaveContextImpl _$$LoadableSaveContextImplFromJson( + Map json) => + _$LoadableSaveContextImpl( + timestamp: json['timestamp'] as int, + ); + +Map _$$LoadableSaveContextImplToJson( + _$LoadableSaveContextImpl instance) => + { + 'timestamp': instance.timestamp, + }; diff --git a/lib/state/app/modules/app_modules.dart b/lib/state/app/modules/app_modules.dart new file mode 100644 index 0000000..83b93b5 --- /dev/null +++ b/lib/state/app/modules/app_modules.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; + +import '../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart'; +import '../../../model/breakers/Breaker.dart'; +import '../../../view/pages/files/files.dart'; +import '../../../view/pages/more/holidays/holidays.dart'; +import '../../../view/pages/more/roomplan/roomplan.dart'; +import '../../../view/pages/talk/chatList.dart'; +import '../../../view/pages/timetable/timetable.dart'; +import '../../../widget/centeredLeading.dart'; +import 'gradeAverages/view/grade_averages_view.dart'; +import 'marianumMessage/view/marianum_message_list_view.dart'; + +class AppModule { + String name; + IconData icon; + Widget Function() create; + + AppModule(this.name, this.icon, this.create); + + static Map modules() => { + Modules.timetable: AppModule('Vertretung', Icons.calendar_month, Timetable.new), + Modules.talk: AppModule('Talk', Icons.chat, ChatList.new), + Modules.files: AppModule('Files', Icons.folder, Files.new), + Modules.marianumMessage: AppModule('Marianum Message', Icons.newspaper, MarianumMessageListView.new), + Modules.roomPlan: AppModule('Raumplan', Icons.location_pin, Roomplan.new), + Modules.gradeAveragesCalculator: AppModule('Notendurschnittsrechner', Icons.calculate, GradeAveragesView.new), + Modules.holidays: AppModule('Schulferien', Icons.holiday_village, Holidays.new), + }; + + static AppModule getModule(Modules module) => modules()[module]!; + + Widget toListTile(BuildContext context) => ListTile( + leading: CenteredLeading(Icon(icon)), + title: Text(name), + onTap: () => pushScreen(context, withNavBar: false, screen: create()), + trailing: const Icon(Icons.arrow_right), + ); + + PersistentTabConfig toBottomTab(BuildContext context, {Widget Function(IconData icon)? itemBuilder}) => PersistentTabConfig( + screen: Breaker(breaker: BreakerArea.global, child: create()), + item: ItemConfig( + activeForegroundColor: Theme.of(context).primaryColor, + inactiveForegroundColor: Theme.of(context).colorScheme.secondary, + icon: itemBuilder == null ? Icon(icon) : itemBuilder(icon), + title: name + ), + ); +} + +enum Modules { + timetable, + talk, + files, + marianumMessage, + roomPlan, + gradeAveragesCalculator, + holidays, +} diff --git a/lib/state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart b/lib/state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart new file mode 100644 index 0000000..6a084ca --- /dev/null +++ b/lib/state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart @@ -0,0 +1,49 @@ +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'grade_averages_event.dart'; +import 'grade_averages_state.dart'; + +class GradeAveragesBloc extends HydratedBloc { + GradeAveragesBloc() : super(const GradeAveragesState(gradingSystem: GradeAveragesGradingSystem.middleSchool, grades: [])) { + + on((event, emit) { + add(ResetAll()); + emit( + state.copyWith( + gradingSystem: event.isMiddleSchool + ? GradeAveragesGradingSystem.middleSchool + : GradeAveragesGradingSystem.highSchool + ) + ); + }); + + on((event, emit) { + emit(state.copyWith(grades: [])); + }); + + on((event, emit) { + emit(state.copyWith(grades: [...state.grades]..removeWhere((grade) => grade == event.grade))); + }); + + on((event, emit) { + emit(state.copyWith(grades: [...state.grades, event.grade])); + }); + + on((event, emit) { + 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; + 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 getGradeFromIndex(int index) => isMiddleSchool() ? index + 1 : 15 - index; + + @override + GradeAveragesState? fromJson(Map json) => GradeAveragesState.fromJson(json); + @override + Map? toJson(GradeAveragesState state) => state.toJson(); +} diff --git a/lib/state/app/modules/gradeAverages/bloc/grade_averages_event.dart b/lib/state/app/modules/gradeAverages/bloc/grade_averages_event.dart new file mode 100644 index 0000000..0be46eb --- /dev/null +++ b/lib/state/app/modules/gradeAverages/bloc/grade_averages_event.dart @@ -0,0 +1,20 @@ + +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); +} diff --git a/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.dart b/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.dart new file mode 100644 index 0000000..0462541 --- /dev/null +++ b/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'grade_averages_state.freezed.dart'; +part 'grade_averages_state.g.dart'; + +@freezed +class GradeAveragesState with _$GradeAveragesState { + const factory GradeAveragesState({ + required GradeAveragesGradingSystem gradingSystem, + required List grades, + }) = _GradeAveragesState; + + factory GradeAveragesState.fromJson(Map json) => _$GradeAveragesStateFromJson(json); +} + +enum GradeAveragesGradingSystem { + highSchool, + middleSchool, +} diff --git a/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.freezed.dart b/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.freezed.dart new file mode 100644 index 0000000..c636f4d --- /dev/null +++ b/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.freezed.dart @@ -0,0 +1,180 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'grade_averages_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +GradeAveragesState _$GradeAveragesStateFromJson(Map json) { + return _GradeAveragesState.fromJson(json); +} + +/// @nodoc +mixin _$GradeAveragesState { + GradeAveragesGradingSystem get gradingSystem => + throw _privateConstructorUsedError; + List get grades => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $GradeAveragesStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GradeAveragesStateCopyWith<$Res> { + factory $GradeAveragesStateCopyWith( + GradeAveragesState value, $Res Function(GradeAveragesState) then) = + _$GradeAveragesStateCopyWithImpl<$Res, GradeAveragesState>; + @useResult + $Res call({GradeAveragesGradingSystem gradingSystem, List grades}); +} + +/// @nodoc +class _$GradeAveragesStateCopyWithImpl<$Res, $Val extends GradeAveragesState> + implements $GradeAveragesStateCopyWith<$Res> { + _$GradeAveragesStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? gradingSystem = null, + Object? grades = null, + }) { + return _then(_value.copyWith( + gradingSystem: null == gradingSystem + ? _value.gradingSystem + : gradingSystem // ignore: cast_nullable_to_non_nullable + as GradeAveragesGradingSystem, + grades: null == grades + ? _value.grades + : grades // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$GradeAveragesStateImplCopyWith<$Res> + implements $GradeAveragesStateCopyWith<$Res> { + factory _$$GradeAveragesStateImplCopyWith(_$GradeAveragesStateImpl value, + $Res Function(_$GradeAveragesStateImpl) then) = + __$$GradeAveragesStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({GradeAveragesGradingSystem gradingSystem, List grades}); +} + +/// @nodoc +class __$$GradeAveragesStateImplCopyWithImpl<$Res> + extends _$GradeAveragesStateCopyWithImpl<$Res, _$GradeAveragesStateImpl> + implements _$$GradeAveragesStateImplCopyWith<$Res> { + __$$GradeAveragesStateImplCopyWithImpl(_$GradeAveragesStateImpl _value, + $Res Function(_$GradeAveragesStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? gradingSystem = null, + Object? grades = null, + }) { + return _then(_$GradeAveragesStateImpl( + gradingSystem: null == gradingSystem + ? _value.gradingSystem + : gradingSystem // ignore: cast_nullable_to_non_nullable + as GradeAveragesGradingSystem, + grades: null == grades + ? _value._grades + : grades // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$GradeAveragesStateImpl implements _GradeAveragesState { + const _$GradeAveragesStateImpl( + {required this.gradingSystem, required final List grades}) + : _grades = grades; + + factory _$GradeAveragesStateImpl.fromJson(Map json) => + _$$GradeAveragesStateImplFromJson(json); + + @override + final GradeAveragesGradingSystem gradingSystem; + final List _grades; + @override + List get grades { + if (_grades is EqualUnmodifiableListView) return _grades; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_grades); + } + + @override + String toString() { + return 'GradeAveragesState(gradingSystem: $gradingSystem, grades: $grades)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GradeAveragesStateImpl && + (identical(other.gradingSystem, gradingSystem) || + other.gradingSystem == gradingSystem) && + const DeepCollectionEquality().equals(other._grades, _grades)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, gradingSystem, const DeepCollectionEquality().hash(_grades)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$GradeAveragesStateImplCopyWith<_$GradeAveragesStateImpl> get copyWith => + __$$GradeAveragesStateImplCopyWithImpl<_$GradeAveragesStateImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$GradeAveragesStateImplToJson( + this, + ); + } +} + +abstract class _GradeAveragesState implements GradeAveragesState { + const factory _GradeAveragesState( + {required final GradeAveragesGradingSystem gradingSystem, + required final List grades}) = _$GradeAveragesStateImpl; + + factory _GradeAveragesState.fromJson(Map json) = + _$GradeAveragesStateImpl.fromJson; + + @override + GradeAveragesGradingSystem get gradingSystem; + @override + List get grades; + @override + @JsonKey(ignore: true) + _$$GradeAveragesStateImplCopyWith<_$GradeAveragesStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.g.dart b/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.g.dart new file mode 100644 index 0000000..af7af59 --- /dev/null +++ b/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'grade_averages_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$GradeAveragesStateImpl _$$GradeAveragesStateImplFromJson( + Map json) => + _$GradeAveragesStateImpl( + gradingSystem: $enumDecode( + _$GradeAveragesGradingSystemEnumMap, json['gradingSystem']), + grades: (json['grades'] as List).map((e) => e as int).toList(), + ); + +Map _$$GradeAveragesStateImplToJson( + _$GradeAveragesStateImpl instance) => + { + 'gradingSystem': + _$GradeAveragesGradingSystemEnumMap[instance.gradingSystem]!, + 'grades': instance.grades, + }; + +const _$GradeAveragesGradingSystemEnumMap = { + GradeAveragesGradingSystem.highSchool: 'highSchool', + GradeAveragesGradingSystem.middleSchool: 'middleSchool', +}; diff --git a/lib/state/app/modules/gradeAverages/view/grade_averages_list_view.dart b/lib/state/app/modules/gradeAverages/view/grade_averages_list_view.dart new file mode 100644 index 0000000..3366a9c --- /dev/null +++ b/lib/state/app/modules/gradeAverages/view/grade_averages_list_view.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../bloc/grade_averages_bloc.dart'; +import '../bloc/grade_averages_event.dart'; + +class GradeAveragesListView extends StatelessWidget { + const GradeAveragesListView({super.key}); + + @override + Widget build(BuildContext context) { + var bloc = context.watch(); + + String getGradeDisplay(int grade) { + if(bloc.isMiddleSchool()) { + return 'Note $grade'; + } else { + return "$grade Punkt${grade > 1 ? "e" : ""}"; + } + } + + return ListView.builder( + itemCount: bloc.gradesInGradingSystem(), + itemBuilder: (context, index) { + var grade = bloc.getGradeFromIndex(index); + return Material( + child: ListTile( + tileColor: grade.isEven ? Colors.transparent : Colors.transparent.withAlpha(50), + title: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(getGradeDisplay(grade)), + const SizedBox(width: 30), + IconButton( + onPressed: () { + bloc.add(DecrementGrade(grade)); + }, + icon: const Icon(Icons.remove), + color: Theme.of(context).colorScheme.onSurface, + ), + Text('${bloc.countOfGrade(grade)}', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)), + IconButton( + onPressed: () { + bloc.add(IncrementGrade(grade)); + }, + icon: const Icon(Icons.add), + color: Theme.of(context).colorScheme.onSurface, + ), + ], + ), + ), + trailing: Visibility( + maintainState: true, + maintainAnimation: true, + maintainSize: true, + visible: bloc.canDecrementOrDelete(grade), + child: IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + bloc.add(ResetGrade(grade)); + }, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/state/app/modules/gradeAverages/view/grade_averages_view.dart b/lib/state/app/modules/gradeAverages/view/grade_averages_view.dart new file mode 100644 index 0000000..7c55e68 --- /dev/null +++ b/lib/state/app/modules/gradeAverages/view/grade_averages_view.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../../widget/confirmDialog.dart'; +import '../bloc/grade_averages_bloc.dart'; +import '../bloc/grade_averages_event.dart'; +import '../bloc/grade_averages_state.dart'; +import 'grade_averages_list_view.dart'; + +class GradeAveragesView extends StatelessWidget { + const GradeAveragesView({super.key}); + + @override + Widget build(BuildContext context) => BlocProvider( + create: (context) => GradeAveragesBloc(), + child: BlocBuilder( + builder: (context, state) { + var bloc = context.watch(); + + return Scaffold( + appBar: AppBar( + title: const Text('Notendurschnittsrechner'), + actions: [ + Visibility( + visible: bloc.state.grades.isNotEmpty, + child: IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + title: 'Zurücksetzen?', + content: 'Alle Einträge werden entfernt.', + confirmButton: 'Zurücksetzen', + onConfirm: () { + bloc.add(ResetAll()); + }, + ), + ); + }, + icon: const Icon(Icons.delete_forever)), + ), + PopupMenuButton( + initialValue: bloc.isMiddleSchool(), + icon: const Icon(Icons.more_horiz), + itemBuilder: (context) => [true, false].map((isMiddleSchool) => PopupMenuItem( + value: isMiddleSchool, + child: Row( + children: [ + Icon( + isMiddleSchool ? Icons.calculate_outlined : Icons.school_outlined, + color: Theme.of(context).colorScheme.onSurface + ), + const SizedBox(width: 15), + Text(isMiddleSchool ? 'Notensystem' : 'Punktesystem'), + ], + ), + )).toList(), + onSelected: (isMiddleSchool) { + if (bloc.state.grades.isNotEmpty) { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + title: 'Notensystem wechseln', + content: + 'Beim Wechsel des Notensystems werden alle Einträge zurückgesetzt.', + confirmButton: 'Fortfahren', + onConfirm: () => bloc.add(GradingSystemChanged(isMiddleSchool)), + ), + ); + } else { + bloc.add(GradingSystemChanged(isMiddleSchool)); + } + }, + ), + ], + ), + + body: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 30), + Text(bloc.average().toStringAsFixed(2), style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 10), + Text(bloc.isMiddleSchool() ? 'Wähle unten die Anzahl deiner jeweiligen Noten aus' : 'Wähle unten die Anzahl deiner jeweiligen Punkte aus'), + const SizedBox(height: 10), + const Expanded( + child: GradeAveragesListView() + ), + ], + ), + ); + }, + ), + ); +} diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart new file mode 100644 index 0000000..7876351 --- /dev/null +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart @@ -0,0 +1,24 @@ +import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart'; +import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; +import '../repository/marianum_message_repository.dart'; +import 'marianum_message_event.dart'; +import 'marianum_message_state.dart'; + +class MarianumMessageBloc extends LoadableHydratedBloc { + @override + Future gatherData() async { + var messages = await repo.getMessages(); + add(Emit((state) => state.copyWith(messageList: messages))); + } + + @override + MarianumMessageRepository repository() => MarianumMessageRepository(); + + @override + MarianumMessageState fromNothing() => const MarianumMessageState(messageList: MarianumMessageList(base: '', messages: [])); + + @override + MarianumMessageState fromStorage(Map json) => MarianumMessageState.fromJson(json); + @override + Map? toStorage(MarianumMessageState state) => state.toJson(); +} diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_event.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_event.dart new file mode 100644 index 0000000..43cbf2a --- /dev/null +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_event.dart @@ -0,0 +1,5 @@ +import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; +import 'marianum_message_state.dart'; + +sealed class MarianumMessageEvent extends LoadableHydratedBlocEvent {} +class MessageEvent extends MarianumMessageEvent {} diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.dart new file mode 100644 index 0000000..4f2d7c9 --- /dev/null +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.dart @@ -0,0 +1,41 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'marianum_message_state.freezed.dart'; +part 'marianum_message_state.g.dart'; + + +@freezed +class MarianumMessageState with _$MarianumMessageState { + const factory MarianumMessageState({ + required MarianumMessageList messageList, + }) = _MarianumMessageState; + + factory MarianumMessageState.fromJson(Map json) => _$MarianumMessageStateFromJson(json); +} + +@freezed +class MarianumMessageList with _$MarianumMessageList { + const factory MarianumMessageList({ + required String base, + required List messages, + }) = _MarianumMessageList; + + factory MarianumMessageList.fromJson(Map json) => _$MarianumMessageListFromJson(json); +} + +@freezed +class MarianumMessage with _$MarianumMessage { + const factory MarianumMessage({ + required String name, + required String date, + required String url, + }) = _MarianumMessage; + + factory MarianumMessage.fromJson(Map json) => _$MarianumMessageFromJson(json); +} + + +enum GradeAveragesGradingSystem { + highSchool, + middleSchool, +} diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.freezed.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.freezed.dart new file mode 100644 index 0000000..35105e1 --- /dev/null +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.freezed.dart @@ -0,0 +1,507 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'marianum_message_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +MarianumMessageState _$MarianumMessageStateFromJson(Map json) { + return _MarianumMessageState.fromJson(json); +} + +/// @nodoc +mixin _$MarianumMessageState { + MarianumMessageList get messageList => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MarianumMessageStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MarianumMessageStateCopyWith<$Res> { + factory $MarianumMessageStateCopyWith(MarianumMessageState value, + $Res Function(MarianumMessageState) then) = + _$MarianumMessageStateCopyWithImpl<$Res, MarianumMessageState>; + @useResult + $Res call({MarianumMessageList messageList}); + + $MarianumMessageListCopyWith<$Res> get messageList; +} + +/// @nodoc +class _$MarianumMessageStateCopyWithImpl<$Res, + $Val extends MarianumMessageState> + implements $MarianumMessageStateCopyWith<$Res> { + _$MarianumMessageStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? messageList = null, + }) { + return _then(_value.copyWith( + messageList: null == messageList + ? _value.messageList + : messageList // ignore: cast_nullable_to_non_nullable + as MarianumMessageList, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $MarianumMessageListCopyWith<$Res> get messageList { + return $MarianumMessageListCopyWith<$Res>(_value.messageList, (value) { + return _then(_value.copyWith(messageList: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$MarianumMessageStateImplCopyWith<$Res> + implements $MarianumMessageStateCopyWith<$Res> { + factory _$$MarianumMessageStateImplCopyWith(_$MarianumMessageStateImpl value, + $Res Function(_$MarianumMessageStateImpl) then) = + __$$MarianumMessageStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({MarianumMessageList messageList}); + + @override + $MarianumMessageListCopyWith<$Res> get messageList; +} + +/// @nodoc +class __$$MarianumMessageStateImplCopyWithImpl<$Res> + extends _$MarianumMessageStateCopyWithImpl<$Res, _$MarianumMessageStateImpl> + implements _$$MarianumMessageStateImplCopyWith<$Res> { + __$$MarianumMessageStateImplCopyWithImpl(_$MarianumMessageStateImpl _value, + $Res Function(_$MarianumMessageStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? messageList = null, + }) { + return _then(_$MarianumMessageStateImpl( + messageList: null == messageList + ? _value.messageList + : messageList // ignore: cast_nullable_to_non_nullable + as MarianumMessageList, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MarianumMessageStateImpl implements _MarianumMessageState { + const _$MarianumMessageStateImpl({required this.messageList}); + + factory _$MarianumMessageStateImpl.fromJson(Map json) => + _$$MarianumMessageStateImplFromJson(json); + + @override + final MarianumMessageList messageList; + + @override + String toString() { + return 'MarianumMessageState(messageList: $messageList)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MarianumMessageStateImpl && + (identical(other.messageList, messageList) || + other.messageList == messageList)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, messageList); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MarianumMessageStateImplCopyWith<_$MarianumMessageStateImpl> + get copyWith => + __$$MarianumMessageStateImplCopyWithImpl<_$MarianumMessageStateImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$MarianumMessageStateImplToJson( + this, + ); + } +} + +abstract class _MarianumMessageState implements MarianumMessageState { + const factory _MarianumMessageState( + {required final MarianumMessageList messageList}) = + _$MarianumMessageStateImpl; + + factory _MarianumMessageState.fromJson(Map json) = + _$MarianumMessageStateImpl.fromJson; + + @override + MarianumMessageList get messageList; + @override + @JsonKey(ignore: true) + _$$MarianumMessageStateImplCopyWith<_$MarianumMessageStateImpl> + get copyWith => throw _privateConstructorUsedError; +} + +MarianumMessageList _$MarianumMessageListFromJson(Map json) { + return _MarianumMessageList.fromJson(json); +} + +/// @nodoc +mixin _$MarianumMessageList { + String get base => throw _privateConstructorUsedError; + List get messages => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MarianumMessageListCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MarianumMessageListCopyWith<$Res> { + factory $MarianumMessageListCopyWith( + MarianumMessageList value, $Res Function(MarianumMessageList) then) = + _$MarianumMessageListCopyWithImpl<$Res, MarianumMessageList>; + @useResult + $Res call({String base, List messages}); +} + +/// @nodoc +class _$MarianumMessageListCopyWithImpl<$Res, $Val extends MarianumMessageList> + implements $MarianumMessageListCopyWith<$Res> { + _$MarianumMessageListCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? base = null, + Object? messages = null, + }) { + return _then(_value.copyWith( + base: null == base + ? _value.base + : base // ignore: cast_nullable_to_non_nullable + as String, + messages: null == messages + ? _value.messages + : messages // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MarianumMessageListImplCopyWith<$Res> + implements $MarianumMessageListCopyWith<$Res> { + factory _$$MarianumMessageListImplCopyWith(_$MarianumMessageListImpl value, + $Res Function(_$MarianumMessageListImpl) then) = + __$$MarianumMessageListImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String base, List messages}); +} + +/// @nodoc +class __$$MarianumMessageListImplCopyWithImpl<$Res> + extends _$MarianumMessageListCopyWithImpl<$Res, _$MarianumMessageListImpl> + implements _$$MarianumMessageListImplCopyWith<$Res> { + __$$MarianumMessageListImplCopyWithImpl(_$MarianumMessageListImpl _value, + $Res Function(_$MarianumMessageListImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? base = null, + Object? messages = null, + }) { + return _then(_$MarianumMessageListImpl( + base: null == base + ? _value.base + : base // ignore: cast_nullable_to_non_nullable + as String, + messages: null == messages + ? _value._messages + : messages // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MarianumMessageListImpl implements _MarianumMessageList { + const _$MarianumMessageListImpl( + {required this.base, required final List messages}) + : _messages = messages; + + factory _$MarianumMessageListImpl.fromJson(Map json) => + _$$MarianumMessageListImplFromJson(json); + + @override + final String base; + final List _messages; + @override + List get messages { + if (_messages is EqualUnmodifiableListView) return _messages; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_messages); + } + + @override + String toString() { + return 'MarianumMessageList(base: $base, messages: $messages)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MarianumMessageListImpl && + (identical(other.base, base) || other.base == base) && + const DeepCollectionEquality().equals(other._messages, _messages)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, base, const DeepCollectionEquality().hash(_messages)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MarianumMessageListImplCopyWith<_$MarianumMessageListImpl> get copyWith => + __$$MarianumMessageListImplCopyWithImpl<_$MarianumMessageListImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$MarianumMessageListImplToJson( + this, + ); + } +} + +abstract class _MarianumMessageList implements MarianumMessageList { + const factory _MarianumMessageList( + {required final String base, + required final List messages}) = + _$MarianumMessageListImpl; + + factory _MarianumMessageList.fromJson(Map json) = + _$MarianumMessageListImpl.fromJson; + + @override + String get base; + @override + List get messages; + @override + @JsonKey(ignore: true) + _$$MarianumMessageListImplCopyWith<_$MarianumMessageListImpl> get copyWith => + throw _privateConstructorUsedError; +} + +MarianumMessage _$MarianumMessageFromJson(Map json) { + return _MarianumMessage.fromJson(json); +} + +/// @nodoc +mixin _$MarianumMessage { + String get name => throw _privateConstructorUsedError; + String get date => throw _privateConstructorUsedError; + String get url => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MarianumMessageCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MarianumMessageCopyWith<$Res> { + factory $MarianumMessageCopyWith( + MarianumMessage value, $Res Function(MarianumMessage) then) = + _$MarianumMessageCopyWithImpl<$Res, MarianumMessage>; + @useResult + $Res call({String name, String date, String url}); +} + +/// @nodoc +class _$MarianumMessageCopyWithImpl<$Res, $Val extends MarianumMessage> + implements $MarianumMessageCopyWith<$Res> { + _$MarianumMessageCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? date = null, + Object? url = null, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as String, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MarianumMessageImplCopyWith<$Res> + implements $MarianumMessageCopyWith<$Res> { + factory _$$MarianumMessageImplCopyWith(_$MarianumMessageImpl value, + $Res Function(_$MarianumMessageImpl) then) = + __$$MarianumMessageImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String name, String date, String url}); +} + +/// @nodoc +class __$$MarianumMessageImplCopyWithImpl<$Res> + extends _$MarianumMessageCopyWithImpl<$Res, _$MarianumMessageImpl> + implements _$$MarianumMessageImplCopyWith<$Res> { + __$$MarianumMessageImplCopyWithImpl( + _$MarianumMessageImpl _value, $Res Function(_$MarianumMessageImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? date = null, + Object? url = null, + }) { + return _then(_$MarianumMessageImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as String, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MarianumMessageImpl implements _MarianumMessage { + const _$MarianumMessageImpl( + {required this.name, required this.date, required this.url}); + + factory _$MarianumMessageImpl.fromJson(Map json) => + _$$MarianumMessageImplFromJson(json); + + @override + final String name; + @override + final String date; + @override + final String url; + + @override + String toString() { + return 'MarianumMessage(name: $name, date: $date, url: $url)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MarianumMessageImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.date, date) || other.date == date) && + (identical(other.url, url) || other.url == url)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, name, date, url); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MarianumMessageImplCopyWith<_$MarianumMessageImpl> get copyWith => + __$$MarianumMessageImplCopyWithImpl<_$MarianumMessageImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$MarianumMessageImplToJson( + this, + ); + } +} + +abstract class _MarianumMessage implements MarianumMessage { + const factory _MarianumMessage( + {required final String name, + required final String date, + required final String url}) = _$MarianumMessageImpl; + + factory _MarianumMessage.fromJson(Map json) = + _$MarianumMessageImpl.fromJson; + + @override + String get name; + @override + String get date; + @override + String get url; + @override + @JsonKey(ignore: true) + _$$MarianumMessageImplCopyWith<_$MarianumMessageImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.g.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.g.dart new file mode 100644 index 0000000..80dc1a2 --- /dev/null +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.g.dart @@ -0,0 +1,52 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'marianum_message_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$MarianumMessageStateImpl _$$MarianumMessageStateImplFromJson( + Map json) => + _$MarianumMessageStateImpl( + messageList: MarianumMessageList.fromJson( + json['messageList'] as Map), + ); + +Map _$$MarianumMessageStateImplToJson( + _$MarianumMessageStateImpl instance) => + { + 'messageList': instance.messageList, + }; + +_$MarianumMessageListImpl _$$MarianumMessageListImplFromJson( + Map json) => + _$MarianumMessageListImpl( + base: json['base'] as String, + messages: (json['messages'] as List) + .map((e) => MarianumMessage.fromJson(e as Map)) + .toList(), + ); + +Map _$$MarianumMessageListImplToJson( + _$MarianumMessageListImpl instance) => + { + 'base': instance.base, + 'messages': instance.messages, + }; + +_$MarianumMessageImpl _$$MarianumMessageImplFromJson( + Map json) => + _$MarianumMessageImpl( + name: json['name'] as String, + date: json['date'] as String, + url: json['url'] as String, + ); + +Map _$$MarianumMessageImplToJson( + _$MarianumMessageImpl instance) => + { + 'name': instance.name, + 'date': instance.date, + 'url': instance.url, + }; diff --git a/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart new file mode 100644 index 0000000..cb7f9db --- /dev/null +++ b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart @@ -0,0 +1,12 @@ +import 'package:dio/dio.dart'; + +import '../../../infrastructure/dataLoader/data_loader.dart'; +import '../../../infrastructure/dataLoader/mhsl_data_loader.dart'; +import '../bloc/marianum_message_state.dart'; + +class MarianumMessageGetMessages extends MhslDataLoader { + @override + Future> fetch() async => dio.get('/message/messages.json'); + @override + MarianumMessageList assemble(DataLoaderResult data) => MarianumMessageList.fromJson(data.json); +} diff --git a/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart b/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart new file mode 100644 index 0000000..3148b92 --- /dev/null +++ b/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart @@ -0,0 +1,7 @@ +import '../../../infrastructure/repository/repository.dart'; +import '../bloc/marianum_message_state.dart'; +import '../dataProvider/marianum_message_get_messages.dart'; + +class MarianumMessageRepository extends Repository { + Future getMessages() => MarianumMessageGetMessages().run(); +} diff --git a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart new file mode 100644 index 0000000..1957886 --- /dev/null +++ b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +import 'marianum_message_view.dart'; +import '../../../infrastructure/loadableState/loadable_state.dart'; +import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart'; +import '../../../infrastructure/utilityWidgets/bloc_module.dart'; +import '../bloc/marianum_message_bloc.dart'; +import '../bloc/marianum_message_state.dart'; + +class MarianumMessageListView extends StatelessWidget { + const MarianumMessageListView({super.key}); + + @override + Widget build(BuildContext context) => BlocModule>( + create: (context) => MarianumMessageBloc(), + child: (context, bloc, state) => Scaffold( + appBar: AppBar( + title: const Text('Marianum Message'), + ), + body: LoadableStateConsumer( + child: (state, loading) => ListView.builder( + itemCount: state.messageList.messages.length, + itemBuilder: (context, index) { + var message = state.messageList.messages.toList()[index]; + return ListTile( + leading: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [Icon(Icons.newspaper)], + ), + title: Text(message.name, overflow: TextOverflow.ellipsis), + subtitle: Text('vom ${message.date}'), + trailing: const Icon(Icons.arrow_right), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: state.messageList.base, message: message))); + }, + ); + } + ), + ), + ) + ); +} diff --git a/lib/view/pages/more/message/messageView.dart b/lib/state/app/modules/marianumMessage/view/marianum_message_view.dart similarity index 91% rename from lib/view/pages/more/message/messageView.dart rename to lib/state/app/modules/marianumMessage/view/marianum_message_view.dart index 791a2ed..ae8fd36 100644 --- a/lib/view/pages/more/message/messageView.dart +++ b/lib/state/app/modules/marianumMessage/view/marianum_message_view.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../../api/mhsl/message/getMessages/getMessagesResponse.dart'; -import '../../../../widget/confirmDialog.dart'; +import '../bloc/marianum_message_state.dart'; +import '../../../../../widget/confirmDialog.dart'; class MessageView extends StatefulWidget { final String basePath; - final GetMessagesResponseObject message; + final MarianumMessage message; const MessageView({super.key, required this.basePath, required this.message}); @override diff --git a/lib/view/pages/files/fileElement.dart b/lib/view/pages/files/fileElement.dart index 478da24..96aa306 100644 --- a/lib/view/pages/files/fileElement.dart +++ b/lib/view/pages/files/fileElement.dart @@ -99,7 +99,7 @@ class _FileElementState extends State { onTap: () { if(widget.file.isDirectory) { Navigator.of(context).push(MaterialPageRoute( - builder: (context) => Files(widget.path.toList()..add(widget.file.name)), + builder: (context) => Files(path: widget.path.toList()..add(widget.file.name)), )); } else { if(EndpointData().getEndpointMode() == EndpointMode.stage) { diff --git a/lib/view/pages/files/files.dart b/lib/view/pages/files/files.dart index 8068408..86ab4f4 100644 --- a/lib/view/pages/files/files.dart +++ b/lib/view/pages/files/files.dart @@ -21,7 +21,7 @@ import 'filesUploadDialog.dart'; class Files extends StatefulWidget { final List path; - const Files(this.path, {super.key}); + Files({List? path, super.key}) : path = path ?? []; @override State createState() => _FilesState(); diff --git a/lib/view/pages/more/message/message.dart b/lib/view/pages/more/message/message.dart deleted file mode 100644 index d9be17f..0000000 --- a/lib/view/pages/more/message/message.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../../../../model/message/messageProps.dart'; -import '../../../../widget/loadingSpinner.dart'; -import 'messageView.dart'; - - -class Message extends StatefulWidget { - const Message({super.key}); - - @override - State createState() => _MessageState(); -} - -class _MessageState extends State { - @override - void initState() { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - Provider.of(context, listen: false).run(); - }); - super.initState(); - } - - @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Marianum Message'), - ), - body: Consumer(builder: (context, value, child) { - if(value.primaryLoading()) return const LoadingSpinner(); - - return RefreshIndicator( - child: ListView.builder( - itemCount: value.getMessagesResponse.messages.length, - itemBuilder: (context, index) { - var message = value.getMessagesResponse.messages.toList()[index]; - return ListTile( - leading: const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [Icon(Icons.newspaper)], - ), - title: Text(message.name, overflow: TextOverflow.ellipsis), - subtitle: Text('vom ${message.date}'), - trailing: const Icon(Icons.arrow_right), - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: value.getMessagesResponse.base, message: message))); - }, - ); - } - ), - onRefresh: () { - Provider.of(context, listen: false).run(renew: true); - return Future.delayed(const Duration(seconds: 3)); - }, - ); - }), - ); -} diff --git a/lib/view/pages/overhang.dart b/lib/view/pages/overhang.dart index 0fbe65a..6269307 100644 --- a/lib/view/pages/overhang.dart +++ b/lib/view/pages/overhang.dart @@ -6,15 +6,11 @@ import 'package:in_app_review/in_app_review.dart'; import '../../extensions/renderNotNull.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; -import '../../widget/ListItem.dart'; +import '../../state/app/modules/app_modules.dart'; import '../../widget/centeredLeading.dart'; import '../../widget/infoDialog.dart'; import '../settings/settings.dart'; import 'more/feedback/feedbackDialog.dart'; -import 'more/gradeAverages/gradeAverage.dart'; -import 'more/holidays/holidays.dart'; -import 'more/message/message.dart'; -import 'more/roomplan/roomplan.dart'; import 'more/share/selectShareTypeDialog.dart'; class Overhang extends StatelessWidget { @@ -30,11 +26,13 @@ class Overhang extends StatelessWidget { ), body: ListView( children: [ - const ListItemNavigator(icon: Icons.newspaper, text: 'Marianum Message', target: Message()), - const ListItemNavigator(icon: Icons.room, text: 'Raumplan', target: Roomplan()), - const ListItemNavigator(icon: Icons.calculate, text: 'Notendurschnittsrechner', target: GradeAverage()), - const ListItemNavigator(icon: Icons.calendar_month, text: 'Schulferien', target: Holidays()), + AppModule.getModule(Modules.marianumMessage).toListTile(context), + AppModule.getModule(Modules.roomPlan).toListTile(context), + AppModule.getModule(Modules.gradeAveragesCalculator).toListTile(context), + AppModule.getModule(Modules.holidays).toListTile(context), + const Divider(), + ListTile( leading: const Icon(Icons.share_outlined), title: const Text('Teile die App'), diff --git a/lib/view/settings/devToolsSettingsDialog.dart b/lib/view/settings/devToolsSettingsDialog.dart index c62cdde..463afa9 100644 --- a/lib/view/settings/devToolsSettingsDialog.dart +++ b/lib/view/settings/devToolsSettingsDialog.dart @@ -1,6 +1,7 @@ import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:provider/provider.dart'; import '../../storage/base/settingsProvider.dart'; @@ -109,6 +110,23 @@ class _DevToolsSettingsDialogState extends State { }, trailing: const Icon(Icons.arrow_right), ), + ListTile( + leading: const CenteredLeading(Icon(Icons.data_object)), + title: const Text('BLOC State cache'), + subtitle: const Text('Lange tippen um zu löschen'), + onTap: () { + // Navigator.push(context, MaterialPageRoute(builder: (context) => const CacheView())); + }, + onLongPress: () { + ConfirmDialog( + title: 'BLOC-Cache löschen', + content: 'Alle cache Einträge werden gelöscht. Der Cache wird bei Nutzung der App automatisch erneut aufgebaut', + confirmButton: 'Unwiederruflich löschen', + onConfirm: () => HydratedBloc.storage.clear(), + ).asDialog(context); + }, + trailing: const Icon(Icons.arrow_right), + ), ], ); } diff --git a/lib/widget/ListItem.dart b/lib/widget/ListItem.dart deleted file mode 100644 index 09c81c9..0000000 --- a/lib/widget/ListItem.dart +++ /dev/null @@ -1,29 +0,0 @@ - -import 'package:flutter/material.dart'; -import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; - -class ListItemNavigator extends StatelessWidget { - const ListItemNavigator({super.key, required this.icon, required this.text, required this.target, this.onLongPress, this.arrow = true}); - - final IconData icon; - final String text; - final bool arrow; - - final Widget target; - - final GestureLongPressCallback? onLongPress; - - @override - Widget build(BuildContext context) { - onTabAction() => pushScreen(context, withNavBar: false, screen: target); //Navigator.push(context, MaterialPageRoute(builder: (context) => target)); - onLongPressAction() => onLongPress; - - return ListTile( - leading: Icon(icon), - title: Text(text), - trailing: arrow ? const Icon(Icons.arrow_right) : null, - onTap: onTabAction, - onLongPress: onLongPressAction, - ); - } -} diff --git a/lib/widget/conditional_wrapper.dart b/lib/widget/conditional_wrapper.dart new file mode 100644 index 0000000..5526cf3 --- /dev/null +++ b/lib/widget/conditional_wrapper.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class ConditionalWrapper extends StatelessWidget { + final bool condition; + final Widget child; + final Widget Function(Widget child) wrapper; + + const ConditionalWrapper({ + required this.condition, + required this.child, + required this.wrapper, + super.key, + }); + + @override + Widget build(BuildContext context) => condition ? wrapper(child) : child; +} diff --git a/pubspec.yaml b/pubspec.yaml index 5f44576..c74885e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: shared_preferences: ^2.0.15 provider: ^6.0.4 jiffy: ^6.1.0 - json_annotation: ^4.8.0 + json_annotation: ^4.8.1 localstore: ^1.2.3 intl: ^0.18.0 nextcloud: @@ -98,12 +98,18 @@ dependencies: time_range_picker: ^2.2.0 in_app_review: ^2.0.8 emoji_picker_flutter: ^2.1.1 + bloc: ^8.1.4 + flutter_bloc: ^8.1.5 + freezed_annotation: ^2.4.1 + connectivity_plus: ^6.0.3 + hydrated_bloc: ^9.1.5 + dio: ^4.0.6 dev_dependencies: flutter_test: sdk: flutter - json_serializable: ^6.6.1 - build_runner: ^2.3.3 + json_serializable: ^6.7.1 + build_runner: ^2.4.9 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is @@ -111,6 +117,7 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^3.0.1 + freezed: ^2.5.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec