loadable error screen, reload actions, autoreload
This commit is contained in:
		
							
								
								
									
										68
									
								
								lib/app.dart
									
									
									
									
									
								
							
							
						
						
									
										68
									
								
								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<App> 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<ChatListProps>(
 | 
			
		||||
              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<ChatListProps>(
 | 
			
		||||
            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(
 | 
			
		||||
 
 | 
			
		||||
@@ -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<void, LoadableStateState> {
 | 
			
		||||
class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState> {
 | 
			
		||||
  late StreamSubscription<List<ConnectivityResult>> _updateStream;
 | 
			
		||||
  LoadingError? loadingError;
 | 
			
		||||
 | 
			
		||||
  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);
 | 
			
		||||
    _updateStream = Connectivity().onConnectivityChanged.listen(emitState);
 | 
			
		||||
    emitConnectivity(List<ConnectivityResult> 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<void> close() {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
import 'loadable_state_state.dart';
 | 
			
		||||
 | 
			
		||||
sealed class LoadableStateEvent {}
 | 
			
		||||
final class ConnectivityChanged extends LoadableStateEvent {
 | 
			
		||||
  final LoadableStateState state;
 | 
			
		||||
  ConnectivityChanged(this.state);
 | 
			
		||||
}
 | 
			
		||||
@@ -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<TState> 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();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError(
 | 
			
		||||
mixin _$LoadableState<TState> {
 | 
			
		||||
  bool get isLoading => throw _privateConstructorUsedError;
 | 
			
		||||
  TState? get data => throw _privateConstructorUsedError;
 | 
			
		||||
  LoadingError? get error => throw _privateConstructorUsedError;
 | 
			
		||||
 | 
			
		||||
  @JsonKey(ignore: true)
 | 
			
		||||
  $LoadableStateCopyWith<TState, LoadableState<TState>> get copyWith =>
 | 
			
		||||
@@ -30,7 +31,9 @@ abstract class $LoadableStateCopyWith<TState, $Res> {
 | 
			
		||||
          $Res Function(LoadableState<TState>) then) =
 | 
			
		||||
      _$LoadableStateCopyWithImpl<TState, $Res, LoadableState<TState>>;
 | 
			
		||||
  @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<TState, $Res,
 | 
			
		||||
  $Res call({
 | 
			
		||||
    Object? isLoading = null,
 | 
			
		||||
    Object? data = freezed,
 | 
			
		||||
    Object? error = freezed,
 | 
			
		||||
  }) {
 | 
			
		||||
    return _then(_value.copyWith(
 | 
			
		||||
      isLoading: null == isLoading
 | 
			
		||||
@@ -59,8 +63,24 @@ class _$LoadableStateCopyWithImpl<TState, $Res,
 | 
			
		||||
          ? _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?,
 | 
			
		||||
    ) 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
 | 
			
		||||
@@ -71,7 +91,10 @@ abstract class _$$LoadableStateImplCopyWith<TState, $Res>
 | 
			
		||||
      __$$LoadableStateImplCopyWithImpl<TState, $Res>;
 | 
			
		||||
  @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<TState, $Res>
 | 
			
		||||
  $Res call({
 | 
			
		||||
    Object? isLoading = null,
 | 
			
		||||
    Object? data = freezed,
 | 
			
		||||
    Object? error = freezed,
 | 
			
		||||
  }) {
 | 
			
		||||
    return _then(_$LoadableStateImpl<TState>(
 | 
			
		||||
      isLoading: null == isLoading
 | 
			
		||||
@@ -98,6 +122,10 @@ class __$$LoadableStateImplCopyWithImpl<TState, $Res>
 | 
			
		||||
          ? _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<TState, $Res>
 | 
			
		||||
/// @nodoc
 | 
			
		||||
 | 
			
		||||
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._();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -114,10 +143,13 @@ class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
 | 
			
		||||
  @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<TState> extends _LoadableState<TState> {
 | 
			
		||||
            other is _$LoadableStateImpl<TState> &&
 | 
			
		||||
            (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<TState> extends _LoadableState<TState> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
abstract class _LoadableState<TState> extends LoadableState<TState> {
 | 
			
		||||
  const factory _LoadableState({final bool isLoading, final TState? data}) =
 | 
			
		||||
      _$LoadableStateImpl<TState>;
 | 
			
		||||
  const factory _LoadableState(
 | 
			
		||||
      {final bool isLoading,
 | 
			
		||||
      final TState? data,
 | 
			
		||||
      final LoadingError? error}) = _$LoadableStateImpl<TState>;
 | 
			
		||||
  const _LoadableState._() : super._();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -152,6 +187,8 @@ abstract class _LoadableState<TState> extends LoadableState<TState> {
 | 
			
		||||
  @override
 | 
			
		||||
  TState? get data;
 | 
			
		||||
  @override
 | 
			
		||||
  LoadingError? get error;
 | 
			
		||||
  @override
 | 
			
		||||
  @JsonKey(ignore: true)
 | 
			
		||||
  _$$LoadableStateImplCopyWith<TState, _$LoadableStateImpl<TState>>
 | 
			
		||||
      get copyWith => throw _privateConstructorUsedError;
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
@@ -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<TController extends Bloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>>, TState> extends StatelessWidget {
 | 
			
		||||
@@ -17,26 +21,51 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    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(
 | 
			
		||||
      children: [
 | 
			
		||||
        LoadableStateErrorBar(visible: loadableState.showError()),
 | 
			
		||||
        Expanded(
 | 
			
		||||
          child: Stack(
 | 
			
		||||
            children: [
 | 
			
		||||
              LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()),
 | 
			
		||||
              LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()),
 | 
			
		||||
    return BlocModule<LoadableStateBloc, LoadableStateState>(
 | 
			
		||||
      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,
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        )
 | 
			
		||||
      ],
 | 
			
		||||
            )
 | 
			
		||||
          ],
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<LoadableStateBloc, LoadableStateState>(
 | 
			
		||||
    create: (context) => LoadableStateBloc(),
 | 
			
		||||
    child: (context, state) => AnimatedSize(
 | 
			
		||||
      duration: animationDuration,
 | 
			
		||||
      child: AnimatedSwitcher(
 | 
			
		||||
          duration: animationDuration,
 | 
			
		||||
          transitionBuilder: (Widget child, Animation<double> animation) => SlideTransition(
 | 
			
		||||
            position: Tween<Offset>(
 | 
			
		||||
              begin: const Offset(0.0, -1.0),
 | 
			
		||||
              end: Offset.zero,
 | 
			
		||||
            ).animate(animation),
 | 
			
		||||
            child: child,
 | 
			
		||||
          ),
 | 
			
		||||
          child: Visibility(
 | 
			
		||||
              key: Key(visible.hashCode.toString()),
 | 
			
		||||
              visible: visible,
 | 
			
		||||
              replacement: const SizedBox(width: double.infinity),
 | 
			
		||||
              child: Builder(
 | 
			
		||||
                builder: (context) {
 | 
			
		||||
                  var controller = context.watch<LoadableStateBloc>();
 | 
			
		||||
                  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<double> animation) => SlideTransition(
 | 
			
		||||
          position: Tween<Offset>(
 | 
			
		||||
            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<LoadableStateBloc>();
 | 
			
		||||
                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))
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    ),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -3,9 +3,20 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 | 
			
		||||
 | 
			
		||||
class BlocModule<TBloc extends StateStreamableSource<TState>, 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<TBloc>(), context.watch<TBloc>().state);
 | 
			
		||||
  Widget staticChild(BuildContext context) => child(context, context.read<TBloc>(), context.read<TBloc>().state);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) => BlocProvider<TBloc>(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)
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<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;
 | 
			
		||||
  LoadableHydratedBloc() : super(const LoadableState()) {
 | 
			
		||||
    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<Error<TState>>((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<TState>());
 | 
			
		||||
              fetch();
 | 
			
		||||
            }
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  fromJson(Map<String, dynamic> json) => LoadableState(isLoading: true, data: fromStorage(json));
 | 
			
		||||
  @override
 | 
			
		||||
  Map<String, dynamic>? toJson(LoadableState<TState> state) => state.data == null ? {} : state.data.toJson();
 | 
			
		||||
 | 
			
		||||
  Future<void> loadState();
 | 
			
		||||
  Future<void> gatherData();
 | 
			
		||||
  TRepository repository();
 | 
			
		||||
 | 
			
		||||
  TState fromNothing();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,14 @@
 | 
			
		||||
import '../../loadableState/loading_error.dart';
 | 
			
		||||
 | 
			
		||||
class LoadableHydratedBlocEvent<TState> {}
 | 
			
		||||
class Emit<TState> extends LoadableHydratedBlocEvent<TState> {
 | 
			
		||||
  final TState Function(TState state) state;
 | 
			
		||||
  final bool loading;
 | 
			
		||||
  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> {}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Module, AppModule> 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, AppModule> 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,
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ class MarianumMessageBloc extends LoadableHydratedBloc<MarianumMessageEvent, Mar
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<void> loadState() async {
 | 
			
		||||
  Future<void> gatherData() async {
 | 
			
		||||
    var messages = await repo.getMessages();
 | 
			
		||||
    add(Emit((state) => state.copyWith(messageList: messages)));
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ class MarianumMessageGetMessages extends DataLoader<MarianumMessageList> {
 | 
			
		||||
  @override
 | 
			
		||||
  Future<MarianumMessageList> fetch() async {
 | 
			
		||||
    await Future.delayed(const Duration(seconds: 3));
 | 
			
		||||
    throw UnimplementedError("Test");
 | 
			
		||||
    return const MarianumMessageList(base: '', messages: [MarianumMessage(date: '', name: 'RepoTest', url: '')]);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<MarianumMessageBloc, LoadableState<MarianumMessageState>>(
 | 
			
		||||
    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<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(ClearState()), icon: Icon(Icons.add))
 | 
			
		||||
          ],
 | 
			
		||||
@@ -50,7 +44,6 @@ class MarianumMessageListView extends StatelessWidget {
 | 
			
		||||
            }
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
      )
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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'),
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user