diff --git a/lib/app.dart b/lib/app.dart index 7802cfd..5ba4596 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; @@ -20,10 +21,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}); @@ -101,50 +99,30 @@ class _AppState extends State with WidgetsBindingObserver { 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: 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/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart index a2cb062..2ca0717 100644 --- a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart +++ b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart @@ -2,21 +2,47 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; +import '../loading_error.dart'; +import 'loadable_state_event.dart'; import 'loadable_state_state.dart'; -class LoadableStateBloc extends Bloc { +class LoadableStateBloc extends Bloc { late StreamSubscription> _updateStream; + LoadingError? loadingError; LoadableStateBloc() : super(const LoadableStateState(connections: null)) { - emitState(List v) => emit(state.copyWith(connections: v)); + on((event, emit) { + emit(event.state); + if(connectivityStatusKnown() && isConnected() && loadingError != null) { + if(!loadingError!.enableRetry) return; + loadingError!.retry!(); + } + }); - Connectivity().checkConnectivity().then(emitState); - _updateStream = Connectivity().onConnectivityChanged.listen(emitState); + emitConnectivity(List result) => add(ConnectivityChanged(LoadableStateState(connections: result))); + + Connectivity().checkConnectivity().then(emitConnectivity); + _updateStream = Connectivity().onConnectivityChanged.listen(emitConnectivity); } bool connectivityStatusKnown() => state.connections != null; - bool isConnected({bool? def}) => !(state.connections?.contains(ConnectivityResult.none) ?? def!); + bool isConnected() => !(state.connections?.contains(ConnectivityResult.none) ?? true); + bool allowRetry() => loadingError?.retry != null; + bool showErrorMessage() => isConnected() && loadingError != null; + + IconData connectionIcon() => connectivityStatusKnown() + ? isConnected() + ? Icons.nearby_error + : Icons.signal_wifi_connected_no_internet_4 + : Icons.device_unknown; + + String connectionText() => connectivityStatusKnown() + ? isConnected() + ? 'Verbindung fehlgeschlagen' + : 'Offline' + : 'Unbekannte Fehlerursache'; @override Future 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/loadable_state.dart b/lib/state/app/infrastructure/loadableState/loadable_state.dart index 6c25efc..98c87c1 100644 --- a/lib/state/app/infrastructure/loadableState/loadable_state.dart +++ b/lib/state/app/infrastructure/loadableState/loadable_state.dart @@ -1,5 +1,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; +import 'loading_error.dart'; + part 'loadable_state.freezed.dart'; @freezed @@ -9,10 +11,15 @@ class LoadableState with _$LoadableState { const factory LoadableState({ @Default(true) bool isLoading, @Default(null) TState? data, + @Default(null) LoadingError? error, }) = _LoadableState; - bool showPrimaryLoading() => isLoading && data == null; - bool showBackgroundLoading() => isLoading && data != null; - bool showError() => !isLoading && data == null; - bool showContent() => data != null; + 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 index 3bfb969..3b805ac 100644 --- a/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart +++ b/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart @@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$LoadableState { bool get isLoading => throw _privateConstructorUsedError; TState? get data => throw _privateConstructorUsedError; + LoadingError? get error => throw _privateConstructorUsedError; @JsonKey(ignore: true) $LoadableStateCopyWith> get copyWith => @@ -30,7 +31,9 @@ abstract class $LoadableStateCopyWith { $Res Function(LoadableState) then) = _$LoadableStateCopyWithImpl>; @useResult - $Res call({bool isLoading, TState? data}); + $Res call({bool isLoading, TState? data, LoadingError? error}); + + $LoadingErrorCopyWith<$Res>? get error; } /// @nodoc @@ -49,6 +52,7 @@ class _$LoadableStateCopyWithImpl? get error { + if (_value.error == null) { + return null; + } + + return $LoadingErrorCopyWith<$Res>(_value.error!, (value) { + return _then(_value.copyWith(error: value) as $Val); + }); + } } /// @nodoc @@ -71,7 +91,10 @@ abstract class _$$LoadableStateImplCopyWith __$$LoadableStateImplCopyWithImpl; @override @useResult - $Res call({bool isLoading, TState? data}); + $Res call({bool isLoading, TState? data, LoadingError? error}); + + @override + $LoadingErrorCopyWith<$Res>? get error; } /// @nodoc @@ -88,6 +111,7 @@ class __$$LoadableStateImplCopyWithImpl $Res call({ Object? isLoading = null, Object? data = freezed, + Object? error = freezed, }) { return _then(_$LoadableStateImpl( isLoading: null == isLoading @@ -98,6 +122,10 @@ class __$$LoadableStateImplCopyWithImpl ? _value.data : data // ignore: cast_nullable_to_non_nullable as TState?, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as LoadingError?, )); } } @@ -105,7 +133,8 @@ class __$$LoadableStateImplCopyWithImpl /// @nodoc class _$LoadableStateImpl extends _LoadableState { - const _$LoadableStateImpl({this.isLoading = true, this.data = null}) + const _$LoadableStateImpl( + {this.isLoading = true, this.data = null, this.error = null}) : super._(); @override @@ -114,10 +143,13 @@ class _$LoadableStateImpl extends _LoadableState { @override @JsonKey() final TState? data; + @override + @JsonKey() + final LoadingError? error; @override String toString() { - return 'LoadableState<$TState>(isLoading: $isLoading, data: $data)'; + return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, error: $error)'; } @override @@ -127,12 +159,13 @@ class _$LoadableStateImpl extends _LoadableState { other is _$LoadableStateImpl && (identical(other.isLoading, isLoading) || other.isLoading == isLoading) && - const DeepCollectionEquality().equals(other.data, data)); + const DeepCollectionEquality().equals(other.data, data) && + (identical(other.error, error) || other.error == error)); } @override int get hashCode => Object.hash( - runtimeType, isLoading, const DeepCollectionEquality().hash(data)); + runtimeType, isLoading, const DeepCollectionEquality().hash(data), error); @JsonKey(ignore: true) @override @@ -143,8 +176,10 @@ class _$LoadableStateImpl extends _LoadableState { } abstract class _LoadableState extends LoadableState { - const factory _LoadableState({final bool isLoading, final TState? data}) = - _$LoadableStateImpl; + const factory _LoadableState( + {final bool isLoading, + final TState? data, + final LoadingError? error}) = _$LoadableStateImpl; const _LoadableState._() : super._(); @override @@ -152,6 +187,8 @@ abstract class _LoadableState extends LoadableState { @override TState? get data; @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..909f6d7 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/loading_error.dart @@ -0,0 +1,12 @@ +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 enableRetry, + @Default(null) void Function()? retry, + }) = _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..2570c98 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/loading_error.freezed.dart @@ -0,0 +1,171 @@ +// 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 enableRetry => throw _privateConstructorUsedError; + void Function()? get retry => 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 enableRetry, void Function()? retry}); +} + +/// @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? enableRetry = null, + Object? retry = freezed, + }) { + return _then(_value.copyWith( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + enableRetry: null == enableRetry + ? _value.enableRetry + : enableRetry // ignore: cast_nullable_to_non_nullable + as bool, + retry: freezed == retry + ? _value.retry + : retry // ignore: cast_nullable_to_non_nullable + as void Function()?, + ) 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 enableRetry, void Function()? retry}); +} + +/// @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? enableRetry = null, + Object? retry = freezed, + }) { + return _then(_$LoadingErrorImpl( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + enableRetry: null == enableRetry + ? _value.enableRetry + : enableRetry // ignore: cast_nullable_to_non_nullable + as bool, + retry: freezed == retry + ? _value.retry + : retry // ignore: cast_nullable_to_non_nullable + as void Function()?, + )); + } +} + +/// @nodoc + +class _$LoadingErrorImpl implements _LoadingError { + const _$LoadingErrorImpl( + {required this.message, this.enableRetry = false, this.retry = null}); + + @override + final String message; + @override + @JsonKey() + final bool enableRetry; + @override + @JsonKey() + final void Function()? retry; + + @override + String toString() { + return 'LoadingError(message: $message, enableRetry: $enableRetry, retry: $retry)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoadingErrorImpl && + (identical(other.message, message) || other.message == message) && + (identical(other.enableRetry, enableRetry) || + other.enableRetry == enableRetry) && + (identical(other.retry, retry) || other.retry == retry)); + } + + @override + int get hashCode => Object.hash(runtimeType, message, enableRetry, retry); + + @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 enableRetry, + final void Function()? retry}) = _$LoadingErrorImpl; + + @override + String get message; + @override + bool get enableRetry; + @override + void Function()? get retry; + @override + @JsonKey(ignore: true) + _$$LoadingErrorImplCopyWith<_$LoadingErrorImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart index 513a13e..c895b7c 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart @@ -2,10 +2,14 @@ import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.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 { @@ -17,26 +21,51 @@ class LoadableStateConsumer().state; + var childWidget = RefreshIndicator( + onRefresh: () { + loadableState.error != null && loadableState.error!.retry != null + ? loadableState.error!.retry!() + : null; + return Future.value(); + }, + child: SizedBox( + height: MediaQuery.of(context).size.height, + child: loadableState.showContent() + ? child(loadableState.data, loadableState.isLoading) + : const SizedBox.shrink(), + ), + // SingleChildScrollView( // TODO not all childs are reloadable + // physics: const AlwaysScrollableScrollPhysics(), + // child: + // ), + ); - return Column( - children: [ - LoadableStateErrorBar(visible: loadableState.showError()), - Expanded( - child: Stack( - children: [ - LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()), - LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()), + return BlocModule( + create: (context) => LoadableStateBloc(), + child: (context, bloc, state) { + bloc.loadingError = loadableState.error; + return Column( + children: [ + LoadableStateErrorBar(visible: loadableState.showErrorBar()), + Expanded( + child: Stack( + children: [ + LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()), + LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()), + LoadableStateErrorScreen(visible: loadableState.showError()), - AnimatedOpacity( - opacity: loadableState.showContent() ? 1.0 : 0.0, - duration: animationDuration, - curve: Curves.easeInOut, - child: loadableState.showContent() ? child(loadableState.data, loadableState.isLoading) : const SizedBox.shrink() + 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 index b5c1f55..ee3ee67 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../utilityWidgets/bloc_module.dart'; import '../bloc/loadable_state_bloc.dart'; -import '../bloc/loadable_state_state.dart'; class LoadableStateErrorBar extends StatelessWidget { final bool visible; @@ -12,48 +10,49 @@ class LoadableStateErrorBar extends StatelessWidget { final Duration animationDuration = const Duration(milliseconds: 200); @override - Widget build(BuildContext context) => BlocModule( - create: (context) => LoadableStateBloc(), - child: (context, state) => 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 controller = context.watch(); - var status = controller.connectivityStatusKnown() && !controller.isConnected() - ? (icon: Icons.wifi_off_outlined, text: 'Offline', color: Colors.grey.shade600) - : (icon: Icons.wifi_find_outlined, text: 'Verbindung fehlgeschlagen', color: Theme.of(context).primaryColor); + 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(); + var status = ( + icon: bloc.connectionIcon(), + text: bloc.connectionText(), + color: bloc.connectivityStatusKnown() && !bloc.isConnected() + ? Colors.grey.shade600 + : Theme.of(context).primaryColor + ); - return Container( - height: 20, - decoration: BoxDecoration( - color: status.color, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(status.icon, size: 14), - const SizedBox(width: 10), - Text(status.text, style: const TextStyle(fontSize: 12)) - ], - ), - ); - }, - ) - ) - ), + return Container( + height: 20, + decoration: BoxDecoration( + color: status.color, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(status.icon, size: 14), + const SizedBox(width: 10), + Text(status.text, style: const TextStyle(fontSize: 12)) + ], + ), + ); + }, + ) + ) ), ); } 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..0737d28 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart @@ -0,0 +1,42 @@ +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; + const LoadableStateErrorScreen({required this.visible, 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.loadingError!.retry!(), child: const Text('Erneut versuschen')), + ], + + if(bloc.showErrorMessage()) ...[ + const SizedBox(height: 40), + Text(bloc.loadingError!.message, style: TextStyle(color: Theme.of(context).hintColor, fontSize: 12)) + ], + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/state/app/infrastructure/utilityWidgets/bloc_module.dart b/lib/state/app/infrastructure/utilityWidgets/bloc_module.dart index 3a61b9c..35297b7 100644 --- a/lib/state/app/infrastructure/utilityWidgets/bloc_module.dart +++ b/lib/state/app/infrastructure/utilityWidgets/bloc_module.dart @@ -3,9 +3,20 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class BlocModule, TState> extends StatelessWidget { final TBloc Function(BuildContext context) create; - final Widget Function(BuildContext context, TState state) child; - const BlocModule({required this.create, required this.child, super.key}); + 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: BlocBuilder(builder: child)); + 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 index 7f6d8a8..7751907 100644 --- a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart @@ -1,28 +1,58 @@ +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'; -abstract class LoadableHydratedBloc, TState, TRepository extends Repository> extends HydratedBloc, LoadableState> { +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())))); + on>((event, emit) => emit(LoadableState(isLoading: true, data: innerState))); on>((event, emit) => emit(const LoadableState())); - + on>((event, emit) => emit(LoadableState(isLoading: false, data: innerState, error: event.error))); + _repository = repository(); - loadState(); + fetch(); } TState? get innerState => state.data; TRepository get repo => _repository; + void fetch() { + gatherData().catchError( + (e) => add( + Error( + LoadingError( + message: e.toString(), + enableRetry: true, + retry: () { + log('Fetch retry on ${TState.toString()}'); + add(Reload()); + fetch(); + } + ) + ) + ) + ); + } + @override fromJson(Map json) => LoadableState(isLoading: true, data: fromStorage(json)); @override Map? toJson(LoadableState state) => state.data == null ? {} : state.data.toJson(); - Future loadState(); + Future gatherData(); TRepository repository(); TState fromNothing(); 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 index a4b3580..c38a1ba 100644 --- a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart @@ -1,7 +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 {} \ No newline at end of file +class ClearState extends LoadableHydratedBlocEvent {} +class Error extends LoadableHydratedBlocEvent { + final LoadingError error; + Error(this.error); +} +class Reload extends LoadableHydratedBlocEvent {} diff --git a/lib/state/app/modules/app_modules.dart b/lib/state/app/modules/app_modules.dart index 28849dc..83b93b5 100644 --- a/lib/state/app/modules/app_modules.dart +++ b/lib/state/app/modules/app_modules.dart @@ -1,8 +1,16 @@ 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; @@ -11,14 +19,37 @@ class AppModule { AppModule(this.name, this.icon, this.create); - static Map modules() => { - Module.timetable: AppModule('Vertretung', Icons.calendar_month, Timetable.new), - Module.talk: AppModule('Talk', Icons.chat, ChatList.new), - Module.files: AppModule('Files', Icons.folder, Files.new), + 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 Module { +enum Modules { timetable, talk, files, diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart index b960e9c..c2193f5 100644 --- a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart @@ -14,7 +14,7 @@ class MarianumMessageBloc extends LoadableHydratedBloc loadState() async { + Future gatherData() async { var messages = await repo.getMessages(); add(Emit((state) => state.copyWith(messageList: messages))); } 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 index 4e0f188..7957329 100644 --- a/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart +++ b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart @@ -6,6 +6,7 @@ class MarianumMessageGetMessages extends DataLoader { @override Future fetch() async { await Future.delayed(const Duration(seconds: 3)); + throw UnimplementedError("Test"); return const MarianumMessageList(base: '', messages: [MarianumMessage(date: '', name: 'RepoTest', url: '')]); } } 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 index 1fddef3..aaf6c68 100644 --- a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart +++ b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -18,14 +16,10 @@ class MarianumMessageListView extends StatelessWidget { @override Widget build(BuildContext context) => BlocModule>( create: (context) => MarianumMessageBloc(), - child: (context, state) { - // if(value.primaryLoading()) return const LoadingSpinner(); - log(state.toString()); - return Scaffold( + child: (context, bloc, state) => Scaffold( appBar: AppBar( title: const Text('Marianum Message'), actions: [ - IconButton(onPressed: () => context.read().add(MessageEvent()), icon: Icon(Icons.abc)), IconButton(onPressed: () => context.read().add(Emit((state) => MarianumMessageState(messageList: MarianumMessageList(base: "", messages: [MarianumMessage(url: "", name: "Teeest", date: "now")])))), icon: Icon(Icons.add)), IconButton(onPressed: () => context.read().add(ClearState()), icon: Icon(Icons.add)) ], @@ -50,7 +44,6 @@ class MarianumMessageListView extends StatelessWidget { } ), ), - ); - } + ) ); } diff --git a/lib/view/pages/overhang.dart b/lib/view/pages/overhang.dart index 599c8f6..dcbe81c 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 '../../state/app/modules/gradeAverages/view/grade_averages_view.dart'; -import '../../state/app/modules/marianumMessage/view/marianum_message_list_view.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/holidays/holidays.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: MarianumMessageListView()), - const ListItemNavigator(icon: Icons.room, text: 'Raumplan', target: Roomplan()), - const ListItemNavigator(icon: Icons.calculate, text: 'Notendurschnittsrechner', target: GradeAveragesView()), - 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/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, - ); - } -}