develop-bloc #68

Merged
MineTec merged 22 commits from develop-bloc into develop 2024-05-12 13:12:23 +00:00
19 changed files with 533 additions and 183 deletions
Showing only changes of commit 9fa711e460 - Show all commits

View File

@ -5,6 +5,7 @@ import 'dart:developer';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'state/app/modules/app_modules.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
@ -20,10 +21,7 @@ import 'notification/notificationController.dart';
import 'notification/notificationTasks.dart'; import 'notification/notificationTasks.dart';
import 'notification/notifyUpdater.dart'; import 'notification/notifyUpdater.dart';
import 'storage/base/settingsProvider.dart'; import 'storage/base/settingsProvider.dart';
import 'view/pages/files/files.dart';
import 'view/pages/overhang.dart'; import 'view/pages/overhang.dart';
import 'view/pages/talk/chatList.dart';
import 'view/pages/timetable/timetable.dart';
class App extends StatefulWidget { class App extends StatefulWidget {
const App({super.key}); const App({super.key});
@ -101,50 +99,30 @@ class _AppState extends State<App> with WidgetsBindingObserver {
screenTransitionAnimation: const ScreenTransitionAnimation(curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200)), screenTransitionAnimation: const ScreenTransitionAnimation(curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200)),
tabs: [ tabs: [
PersistentTabConfig( AppModule.getModule(Modules.timetable).toBottomTab(context),
screen: const Breaker(breaker: BreakerArea.timetable, child: Timetable()), AppModule.getModule(Modules.talk).toBottomTab(
item: ItemConfig( context,
activeForegroundColor: Theme.of(context).primaryColor, itemBuilder: (icon) => Consumer<ChatListProps>(
inactiveForegroundColor: Theme.of(context).colorScheme.secondary, builder: (context, value, child) {
icon: const Icon(Icons.calendar_month), if(value.primaryLoading()) return Icon(icon);
title: 'Vertretung' var messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b);
), return badges.Badge(
), showBadge: messages > 0,
PersistentTabConfig( position: badges.BadgePosition.topEnd(top: -3, end: -3),
screen: const Breaker(breaker: BreakerArea.talk, child: ChatList()), stackFit: StackFit.loose,
item: ItemConfig( badgeStyle: badges.BadgeStyle(
activeForegroundColor: Theme.of(context).primaryColor, padding: const EdgeInsets.all(3),
inactiveForegroundColor: Theme.of(context).colorScheme.secondary, badgeColor: Theme.of(context).primaryColor,
icon: Consumer<ChatListProps>( elevation: 1,
builder: (context, value, child) { ),
if(value.primaryLoading()) return const Icon(Icons.chat); badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
var messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b); child: Icon(icon),
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.files).toBottomTab(context),
PersistentTabConfig( PersistentTabConfig(
screen: const Breaker(breaker: BreakerArea.more, child: Overhang()), screen: const Breaker(breaker: BreakerArea.more, child: Overhang()),
item: ItemConfig( item: ItemConfig(

View File

@ -2,21 +2,47 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:connectivity_plus/connectivity_plus.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'; import 'loadable_state_state.dart';
class LoadableStateBloc extends Bloc<void, LoadableStateState> { class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState> {
late StreamSubscription<List<ConnectivityResult>> _updateStream; late StreamSubscription<List<ConnectivityResult>> _updateStream;
LoadingError? loadingError;
LoadableStateBloc() : super(const LoadableStateState(connections: null)) { LoadableStateBloc() : super(const LoadableStateState(connections: null)) {
emitState(List<ConnectivityResult> v) => emit(state.copyWith(connections: v)); on<ConnectivityChanged>((event, emit) {
emit(event.state);
if(connectivityStatusKnown() && isConnected() && loadingError != null) {
if(!loadingError!.enableRetry) return;
loadingError!.retry!();
}
});
Connectivity().checkConnectivity().then(emitState); emitConnectivity(List<ConnectivityResult> result) => add(ConnectivityChanged(LoadableStateState(connections: result)));
_updateStream = Connectivity().onConnectivityChanged.listen(emitState);
Connectivity().checkConnectivity().then(emitConnectivity);
_updateStream = Connectivity().onConnectivityChanged.listen(emitConnectivity);
} }
bool connectivityStatusKnown() => state.connections != null; 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 @override
Future<void> close() { Future<void> close() {

View File

@ -0,0 +1,7 @@
import 'loadable_state_state.dart';
sealed class LoadableStateEvent {}
final class ConnectivityChanged extends LoadableStateEvent {
final LoadableStateState state;
ConnectivityChanged(this.state);
}

View File

@ -1,5 +1,7 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'loading_error.dart';
part 'loadable_state.freezed.dart'; part 'loadable_state.freezed.dart';
@freezed @freezed
@ -9,10 +11,15 @@ class LoadableState<TState> with _$LoadableState {
const factory LoadableState({ const factory LoadableState({
@Default(true) bool isLoading, @Default(true) bool isLoading,
@Default(null) TState? data, @Default(null) TState? data,
@Default(null) LoadingError? error,
}) = _LoadableState; }) = _LoadableState;
bool showPrimaryLoading() => isLoading && data == null; bool _hasError() => error != null;
bool showBackgroundLoading() => isLoading && data != null; bool _hasData() => data != null;
bool showError() => !isLoading && data == null;
bool showContent() => data != null; bool showPrimaryLoading() => isLoading && !_hasData();
bool showBackgroundLoading() => isLoading && _hasData();
bool showErrorBar() => _hasError() && _hasData();
bool showError() => _hasError() && !_hasData();
bool showContent() => _hasData();
} }

View File

@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError(
mixin _$LoadableState<TState> { mixin _$LoadableState<TState> {
bool get isLoading => throw _privateConstructorUsedError; bool get isLoading => throw _privateConstructorUsedError;
TState? get data => throw _privateConstructorUsedError; TState? get data => throw _privateConstructorUsedError;
LoadingError? get error => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
$LoadableStateCopyWith<TState, LoadableState<TState>> get copyWith => $LoadableStateCopyWith<TState, LoadableState<TState>> get copyWith =>
@ -30,7 +31,9 @@ abstract class $LoadableStateCopyWith<TState, $Res> {
$Res Function(LoadableState<TState>) then) = $Res Function(LoadableState<TState>) then) =
_$LoadableStateCopyWithImpl<TState, $Res, LoadableState<TState>>; _$LoadableStateCopyWithImpl<TState, $Res, LoadableState<TState>>;
@useResult @useResult
$Res call({bool isLoading, TState? data}); $Res call({bool isLoading, TState? data, LoadingError? error});
$LoadingErrorCopyWith<$Res>? get error;
} }
/// @nodoc /// @nodoc
@ -49,6 +52,7 @@ class _$LoadableStateCopyWithImpl<TState, $Res,
$Res call({ $Res call({
Object? isLoading = null, Object? isLoading = null,
Object? data = freezed, Object? data = freezed,
Object? error = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
isLoading: null == isLoading isLoading: null == isLoading
@ -59,8 +63,24 @@ class _$LoadableStateCopyWithImpl<TState, $Res,
? _value.data ? _value.data
: data // ignore: cast_nullable_to_non_nullable : data // ignore: cast_nullable_to_non_nullable
as TState?, as TState?,
error: freezed == error
? _value.error
: error // ignore: cast_nullable_to_non_nullable
as LoadingError?,
) as $Val); ) 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 /// @nodoc
@ -71,7 +91,10 @@ abstract class _$$LoadableStateImplCopyWith<TState, $Res>
__$$LoadableStateImplCopyWithImpl<TState, $Res>; __$$LoadableStateImplCopyWithImpl<TState, $Res>;
@override @override
@useResult @useResult
$Res call({bool isLoading, TState? data}); $Res call({bool isLoading, TState? data, LoadingError? error});
@override
$LoadingErrorCopyWith<$Res>? get error;
} }
/// @nodoc /// @nodoc
@ -88,6 +111,7 @@ class __$$LoadableStateImplCopyWithImpl<TState, $Res>
$Res call({ $Res call({
Object? isLoading = null, Object? isLoading = null,
Object? data = freezed, Object? data = freezed,
Object? error = freezed,
}) { }) {
return _then(_$LoadableStateImpl<TState>( return _then(_$LoadableStateImpl<TState>(
isLoading: null == isLoading isLoading: null == isLoading
@ -98,6 +122,10 @@ class __$$LoadableStateImplCopyWithImpl<TState, $Res>
? _value.data ? _value.data
: data // ignore: cast_nullable_to_non_nullable : data // ignore: cast_nullable_to_non_nullable
as TState?, as TState?,
error: freezed == error
? _value.error
: error // ignore: cast_nullable_to_non_nullable
as LoadingError?,
)); ));
} }
} }
@ -105,7 +133,8 @@ class __$$LoadableStateImplCopyWithImpl<TState, $Res>
/// @nodoc /// @nodoc
class _$LoadableStateImpl<TState> extends _LoadableState<TState> { class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
const _$LoadableStateImpl({this.isLoading = true, this.data = null}) const _$LoadableStateImpl(
{this.isLoading = true, this.data = null, this.error = null})
: super._(); : super._();
@override @override
@ -114,10 +143,13 @@ class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
@override @override
@JsonKey() @JsonKey()
final TState? data; final TState? data;
@override
@JsonKey()
final LoadingError? error;
@override @override
String toString() { String toString() {
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data)'; return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, error: $error)';
} }
@override @override
@ -127,12 +159,13 @@ class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
other is _$LoadableStateImpl<TState> && other is _$LoadableStateImpl<TState> &&
(identical(other.isLoading, isLoading) || (identical(other.isLoading, isLoading) ||
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 @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, isLoading, const DeepCollectionEquality().hash(data)); runtimeType, isLoading, const DeepCollectionEquality().hash(data), error);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@ -143,8 +176,10 @@ class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
} }
abstract class _LoadableState<TState> extends LoadableState<TState> { abstract class _LoadableState<TState> extends LoadableState<TState> {
const factory _LoadableState({final bool isLoading, final TState? data}) = const factory _LoadableState(
_$LoadableStateImpl<TState>; {final bool isLoading,
final TState? data,
final LoadingError? error}) = _$LoadableStateImpl<TState>;
const _LoadableState._() : super._(); const _LoadableState._() : super._();
@override @override
@ -152,6 +187,8 @@ abstract class _LoadableState<TState> extends LoadableState<TState> {
@override @override
TState? get data; TState? get data;
@override @override
LoadingError? get error;
@override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$LoadableStateImplCopyWith<TState, _$LoadableStateImpl<TState>> _$$LoadableStateImplCopyWith<TState, _$LoadableStateImpl<TState>>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;

View File

@ -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;
}

View File

@ -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>(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<LoadingError> 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;
}

View File

@ -2,10 +2,14 @@ import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../utilityWidgets/bloc_module.dart';
import '../../utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.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.dart';
import 'loadable_state_background_loading.dart'; import 'loadable_state_background_loading.dart';
import 'loadable_state_error_bar.dart'; import 'loadable_state_error_bar.dart';
import 'loadable_state_error_screen.dart';
import 'loadable_state_primary_loading.dart'; import 'loadable_state_primary_loading.dart';
class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>>, TState> extends StatelessWidget { class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>>, TState> extends StatelessWidget {
@ -17,26 +21,51 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var loadableState = context.watch<TController>().state; var loadableState = context.watch<TController>().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( return BlocModule<LoadableStateBloc, LoadableStateState>(
children: [ create: (context) => LoadableStateBloc(),
LoadableStateErrorBar(visible: loadableState.showError()), child: (context, bloc, state) {
Expanded( bloc.loadingError = loadableState.error;
child: Stack( return Column(
children: [ children: [
LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()), LoadableStateErrorBar(visible: loadableState.showErrorBar()),
LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()), Expanded(
child: Stack(
children: [
LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()),
LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()),
LoadableStateErrorScreen(visible: loadableState.showError()),
AnimatedOpacity( AnimatedOpacity(
opacity: loadableState.showContent() ? 1.0 : 0.0, opacity: loadableState.showContent() ? 1.0 : 0.0,
duration: animationDuration, duration: animationDuration,
curve: Curves.easeInOut, curve: Curves.easeInOut,
child: loadableState.showContent() ? child(loadableState.data, loadableState.isLoading) : const SizedBox.shrink() child: childWidget,
),
],
), ),
], )
), ],
) );
], }
); );
} }
} }

View File

@ -1,9 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../utilityWidgets/bloc_module.dart';
import '../bloc/loadable_state_bloc.dart'; import '../bloc/loadable_state_bloc.dart';
import '../bloc/loadable_state_state.dart';
class LoadableStateErrorBar extends StatelessWidget { class LoadableStateErrorBar extends StatelessWidget {
final bool visible; final bool visible;
@ -12,48 +10,49 @@ class LoadableStateErrorBar extends StatelessWidget {
final Duration animationDuration = const Duration(milliseconds: 200); final Duration animationDuration = const Duration(milliseconds: 200);
@override @override
Widget build(BuildContext context) => BlocModule<LoadableStateBloc, LoadableStateState>( Widget build(BuildContext context) => AnimatedSize(
create: (context) => LoadableStateBloc(), duration: animationDuration,
child: (context, state) => AnimatedSize( child: AnimatedSwitcher(
duration: animationDuration, duration: animationDuration,
child: AnimatedSwitcher( transitionBuilder: (Widget child, Animation<double> animation) => SlideTransition(
duration: animationDuration, position: Tween<Offset>(
transitionBuilder: (Widget child, Animation<double> animation) => SlideTransition( begin: const Offset(0.0, -1.0),
position: Tween<Offset>( end: Offset.zero,
begin: const Offset(0.0, -1.0), ).animate(animation),
end: Offset.zero, child: child,
).animate(animation), ),
child: child, child: Visibility(
), key: Key(visible.hashCode.toString()),
child: Visibility( visible: visible,
key: Key(visible.hashCode.toString()), replacement: const SizedBox(width: double.infinity),
visible: visible, child: Builder(
replacement: const SizedBox(width: double.infinity), builder: (context) {
child: Builder( var bloc = context.watch<LoadableStateBloc>();
builder: (context) { var status = (
var controller = context.watch<LoadableStateBloc>(); icon: bloc.connectionIcon(),
var status = controller.connectivityStatusKnown() && !controller.isConnected() text: bloc.connectionText(),
? (icon: Icons.wifi_off_outlined, text: 'Offline', color: Colors.grey.shade600) color: bloc.connectivityStatusKnown() && !bloc.isConnected()
: (icon: Icons.wifi_find_outlined, text: 'Verbindung fehlgeschlagen', color: Theme.of(context).primaryColor); ? Colors.grey.shade600
: Theme.of(context).primaryColor
);
return Container( return Container(
height: 20, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
color: status.color, color: status.color,
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(status.icon, size: 14), Icon(status.icon, size: 14),
const SizedBox(width: 10), const SizedBox(width: 10),
Text(status.text, style: const TextStyle(fontSize: 12)) Text(status.text, style: const TextStyle(fontSize: 12))
], ],
), ),
); );
}, },
) )
) )
),
), ),
); );
} }

View File

@ -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<LoadableStateBloc>();
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))
],
],
),
),
);
}
}

View File

@ -3,9 +3,20 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class BlocModule<TBloc extends StateStreamableSource<TState>, TState> extends StatelessWidget { class BlocModule<TBloc extends StateStreamableSource<TState>, TState> extends StatelessWidget {
final TBloc Function(BuildContext context) create; final TBloc Function(BuildContext context) create;
final Widget Function(BuildContext context, TState state) child; final Widget Function(BuildContext context, TBloc bloc, TState state) child;
const BlocModule({required this.create, required this.child, super.key}); final bool autoRebuild;
const BlocModule({required this.create, required this.child, this.autoRebuild = false, super.key});
Widget rebuildChild(BuildContext context) => child(context, context.watch<TBloc>(), context.watch<TBloc>().state);
Widget staticChild(BuildContext context) => child(context, context.read<TBloc>(), context.read<TBloc>().state);
@override @override
Widget build(BuildContext context) => BlocProvider<TBloc>(create: create, child: BlocBuilder<TBloc, TState>(builder: child)); Widget build(BuildContext context) => BlocProvider<TBloc>(
create: create,
child: Builder(
builder: (context) => autoRebuild
? rebuildChild(context)
: staticChild(context)
)
);
} }

View File

@ -1,28 +1,58 @@
import 'dart:developer';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import '../../loadableState/loading_error.dart';
import '../../repository/repository.dart'; import '../../repository/repository.dart';
import 'loadable_hydrated_bloc_event.dart'; import 'loadable_hydrated_bloc_event.dart';
import '../../loadableState/loadable_state.dart'; import '../../loadableState/loadable_state.dart';
abstract class LoadableHydratedBloc<TEvent extends LoadableHydratedBlocEvent<TState>, TState, TRepository extends Repository<TState>> extends HydratedBloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>> { abstract class LoadableHydratedBloc<
TEvent extends LoadableHydratedBlocEvent<TState>,
TState,
TRepository extends Repository<TState>
> extends HydratedBloc<
LoadableHydratedBlocEvent<TState>,
LoadableState<TState>
> {
late TRepository _repository; late TRepository _repository;
LoadableHydratedBloc() : super(const LoadableState()) { LoadableHydratedBloc() : super(const LoadableState()) {
on<Emit<TState>>((event, emit) => emit(LoadableState(isLoading: event.loading, data: event.state(innerState ?? fromNothing())))); on<Emit<TState>>((event, emit) => emit(LoadableState(isLoading: event.loading, data: event.state(innerState ?? fromNothing()))));
on<Reload<TState>>((event, emit) => emit(LoadableState(isLoading: true, data: innerState)));
on<ClearState<TState>>((event, emit) => emit(const LoadableState())); on<ClearState<TState>>((event, emit) => emit(const LoadableState()));
on<Error<TState>>((event, emit) => emit(LoadableState(isLoading: false, data: innerState, error: event.error)));
_repository = repository(); _repository = repository();
loadState(); fetch();
} }
TState? get innerState => state.data; TState? get innerState => state.data;
TRepository get repo => _repository; 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<TState>());
fetch();
}
)
)
)
);
}
@override @override
fromJson(Map<String, dynamic> json) => LoadableState(isLoading: true, data: fromStorage(json)); fromJson(Map<String, dynamic> json) => LoadableState(isLoading: true, data: fromStorage(json));
@override @override
Map<String, dynamic>? toJson(LoadableState<TState> state) => state.data == null ? {} : state.data.toJson(); Map<String, dynamic>? toJson(LoadableState<TState> state) => state.data == null ? {} : state.data.toJson();
Future<void> loadState(); Future<void> gatherData();
TRepository repository(); TRepository repository();
TState fromNothing(); TState fromNothing();

View File

@ -1,7 +1,14 @@
import '../../loadableState/loading_error.dart';
class LoadableHydratedBlocEvent<TState> {} class LoadableHydratedBlocEvent<TState> {}
class Emit<TState> extends LoadableHydratedBlocEvent<TState> { class Emit<TState> extends LoadableHydratedBlocEvent<TState> {
final TState Function(TState state) state; final TState Function(TState state) state;
final bool loading; final bool loading;
Emit(this.state, {this.loading = false}); Emit(this.state, {this.loading = false});
} }
class ClearState<TState> extends LoadableHydratedBlocEvent<TState> {} class ClearState<TState> extends LoadableHydratedBlocEvent<TState> {}
class Error<TState> extends LoadableHydratedBlocEvent<TState> {
final LoadingError error;
Error(this.error);
}
class Reload<TState> extends LoadableHydratedBlocEvent<TState> {}

View File

@ -1,8 +1,16 @@
import 'package:flutter/material.dart'; 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/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/talk/chatList.dart';
import '../../../view/pages/timetable/timetable.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 { class AppModule {
String name; String name;
@ -11,14 +19,37 @@ class AppModule {
AppModule(this.name, this.icon, this.create); AppModule(this.name, this.icon, this.create);
static Map<Module, AppModule> modules() => { static Map<Modules, AppModule> modules() => {
Module.timetable: AppModule('Vertretung', Icons.calendar_month, Timetable.new), Modules.timetable: AppModule('Vertretung', Icons.calendar_month, Timetable.new),
Module.talk: AppModule('Talk', Icons.chat, ChatList.new), Modules.talk: AppModule('Talk', Icons.chat, ChatList.new),
Module.files: AppModule('Files', Icons.folder, Files.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, timetable,
talk, talk,
files, files,

View File

@ -14,7 +14,7 @@ class MarianumMessageBloc extends LoadableHydratedBloc<MarianumMessageEvent, Mar
} }
@override @override
Future<void> loadState() async { Future<void> gatherData() async {
var messages = await repo.getMessages(); var messages = await repo.getMessages();
add(Emit((state) => state.copyWith(messageList: messages))); add(Emit((state) => state.copyWith(messageList: messages)));
} }

View File

@ -6,6 +6,7 @@ class MarianumMessageGetMessages extends DataLoader<MarianumMessageList> {
@override @override
Future<MarianumMessageList> fetch() async { Future<MarianumMessageList> fetch() async {
await Future.delayed(const Duration(seconds: 3)); await Future.delayed(const Duration(seconds: 3));
throw UnimplementedError("Test");
return const MarianumMessageList(base: '', messages: [MarianumMessage(date: '', name: 'RepoTest', url: '')]); return const MarianumMessageList(base: '', messages: [MarianumMessage(date: '', name: 'RepoTest', url: '')]);
} }
} }

View File

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -18,14 +16,10 @@ class MarianumMessageListView extends StatelessWidget {
@override @override
Widget build(BuildContext context) => BlocModule<MarianumMessageBloc, LoadableState<MarianumMessageState>>( Widget build(BuildContext context) => BlocModule<MarianumMessageBloc, LoadableState<MarianumMessageState>>(
create: (context) => MarianumMessageBloc(), create: (context) => MarianumMessageBloc(),
child: (context, state) { child: (context, bloc, state) => Scaffold(
// if(value.primaryLoading()) return const LoadingSpinner();
log(state.toString());
return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Marianum Message'), title: const Text('Marianum Message'),
actions: [ actions: [
IconButton(onPressed: () => context.read<MarianumMessageBloc>().add(MessageEvent()), icon: Icon(Icons.abc)),
IconButton(onPressed: () => context.read<MarianumMessageBloc>().add(Emit((state) => MarianumMessageState(messageList: MarianumMessageList(base: "", messages: [MarianumMessage(url: "", name: "Teeest", date: "now")])))), icon: Icon(Icons.add)), IconButton(onPressed: () => context.read<MarianumMessageBloc>().add(Emit((state) => MarianumMessageState(messageList: MarianumMessageList(base: "", messages: [MarianumMessage(url: "", name: "Teeest", date: "now")])))), icon: Icon(Icons.add)),
IconButton(onPressed: () => context.read<MarianumMessageBloc>().add(ClearState()), icon: Icon(Icons.add)) IconButton(onPressed: () => context.read<MarianumMessageBloc>().add(ClearState()), icon: Icon(Icons.add))
], ],
@ -50,7 +44,6 @@ class MarianumMessageListView extends StatelessWidget {
} }
), ),
), ),
); )
}
); );
} }

View File

@ -6,15 +6,11 @@ import 'package:in_app_review/in_app_review.dart';
import '../../extensions/renderNotNull.dart'; import '../../extensions/renderNotNull.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import '../../state/app/modules/gradeAverages/view/grade_averages_view.dart'; import '../../state/app/modules/app_modules.dart';
import '../../state/app/modules/marianumMessage/view/marianum_message_list_view.dart';
import '../../widget/ListItem.dart';
import '../../widget/centeredLeading.dart'; import '../../widget/centeredLeading.dart';
import '../../widget/infoDialog.dart'; import '../../widget/infoDialog.dart';
import '../settings/settings.dart'; import '../settings/settings.dart';
import 'more/feedback/feedbackDialog.dart'; import 'more/feedback/feedbackDialog.dart';
import 'more/holidays/holidays.dart';
import 'more/roomplan/roomplan.dart';
import 'more/share/selectShareTypeDialog.dart'; import 'more/share/selectShareTypeDialog.dart';
class Overhang extends StatelessWidget { class Overhang extends StatelessWidget {
@ -30,11 +26,13 @@ class Overhang extends StatelessWidget {
), ),
body: ListView( body: ListView(
children: [ children: [
const ListItemNavigator(icon: Icons.newspaper, text: 'Marianum Message', target: MarianumMessageListView()), AppModule.getModule(Modules.marianumMessage).toListTile(context),
const ListItemNavigator(icon: Icons.room, text: 'Raumplan', target: Roomplan()), AppModule.getModule(Modules.roomPlan).toListTile(context),
const ListItemNavigator(icon: Icons.calculate, text: 'Notendurschnittsrechner', target: GradeAveragesView()), AppModule.getModule(Modules.gradeAveragesCalculator).toListTile(context),
const ListItemNavigator(icon: Icons.calendar_month, text: 'Schulferien', target: Holidays()), AppModule.getModule(Modules.holidays).toListTile(context),
const Divider(), const Divider(),
ListTile( ListTile(
leading: const Icon(Icons.share_outlined), leading: const Icon(Icons.share_outlined),
title: const Text('Teile die App'), title: const Text('Teile die App'),

View File

@ -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,
);
}
}