From 7129c0dee8bc402a4fd34072afef445e0cb875c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Mon, 22 Apr 2024 23:02:03 +0200 Subject: [PATCH 01/19] wip basics for bloc based state management --- analysis_options.yaml | 4 + build.yaml | 6 + .../grade_averages_controller.dart | 10 + .../gradeAverages/grade_averages_state.dart | 19 ++ .../grade_averages_state.freezed.dart | 179 ++++++++++++++++++ .../gradeAverages/grade_averages_state.g.dart | 27 +++ .../marianum_message_controller.dart | 24 +++ .../marianum_message_state.dart | 10 + .../marianum_message_state.freezed.dart | 143 ++++++++++++++ .../app/base/account/account_controller.dart | 0 lib/state/infrastructure/controller.dart | 5 + lib/state/infrastructure/loadable_state.dart | 31 +++ .../infrastructure/state_extensions.dart | 7 + lib/state/widgets/controller_consumer.dart | 12 ++ lib/state/widgets/controller_provider.dart | 21 ++ lib/state/widgets/controllers_provider.dart | 17 ++ .../widgets/loadable_controller_consumer.dart | 39 ++++ .../sub_selected_controller_consumer.dart | 11 ++ lib/view/pages/more/test.dart | 44 +++++ lib/view/pages/overhang.dart | 5 + .../pages/talk/components/chatMessage.dart | 2 +- pubspec.yaml | 10 +- 22 files changed, 622 insertions(+), 4 deletions(-) create mode 100644 build.yaml create mode 100644 lib/state/app/application/gradeAverages/grade_averages_controller.dart create mode 100644 lib/state/app/application/gradeAverages/grade_averages_state.dart create mode 100644 lib/state/app/application/gradeAverages/grade_averages_state.freezed.dart create mode 100644 lib/state/app/application/gradeAverages/grade_averages_state.g.dart create mode 100644 lib/state/app/application/marianumMessage/marianum_message_controller.dart create mode 100644 lib/state/app/application/marianumMessage/marianum_message_state.dart create mode 100644 lib/state/app/application/marianumMessage/marianum_message_state.freezed.dart create mode 100644 lib/state/app/base/account/account_controller.dart create mode 100644 lib/state/infrastructure/controller.dart create mode 100644 lib/state/infrastructure/loadable_state.dart create mode 100644 lib/state/infrastructure/state_extensions.dart create mode 100644 lib/state/widgets/controller_consumer.dart create mode 100644 lib/state/widgets/controller_provider.dart create mode 100644 lib/state/widgets/controllers_provider.dart create mode 100644 lib/state/widgets/loadable_controller_consumer.dart create mode 100644 lib/state/widgets/sub_selected_controller_consumer.dart create mode 100644 lib/view/pages/more/test.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index ebc4a78..a40338c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,10 @@ # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +analyzer: + errors: + invalid_annotation_target: ignore + linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..24cf608 --- /dev/null +++ b/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + builders: + json_serializable: + options: + explicit_to_json: false \ No newline at end of file diff --git a/lib/state/app/application/gradeAverages/grade_averages_controller.dart b/lib/state/app/application/gradeAverages/grade_averages_controller.dart new file mode 100644 index 0000000..6e0f829 --- /dev/null +++ b/lib/state/app/application/gradeAverages/grade_averages_controller.dart @@ -0,0 +1,10 @@ + +import '../../../infrastructure/controller.dart'; +import 'grade_averages_state.dart'; + +class GradeAveragesController extends Controller { + GradeAveragesController(super.initialState); + + void setGradeType(GradingSchemes scheme) => emit(state.copyWith(gradingScheme: scheme)); + double average() => state.grades.reduce((a, b) => a + b) / state.grades.length; +} diff --git a/lib/state/app/application/gradeAverages/grade_averages_state.dart b/lib/state/app/application/gradeAverages/grade_averages_state.dart new file mode 100644 index 0000000..c9df18e --- /dev/null +++ b/lib/state/app/application/gradeAverages/grade_averages_state.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'grade_averages_state.freezed.dart'; +part 'grade_averages_state.g.dart'; + +@freezed +class GradeAveragesState with _$GradeAveragesState { + const factory GradeAveragesState({ + required GradingSchemes gradingScheme, + required List grades, + }) = _GradeAveragesState; + + factory GradeAveragesState.fromJson(Map json) => _$GradeAveragesStateFromJson(json); +} + +enum GradingSchemes { + middleSchool, + highSchool, +} diff --git a/lib/state/app/application/gradeAverages/grade_averages_state.freezed.dart b/lib/state/app/application/gradeAverages/grade_averages_state.freezed.dart new file mode 100644 index 0000000..9985e09 --- /dev/null +++ b/lib/state/app/application/gradeAverages/grade_averages_state.freezed.dart @@ -0,0 +1,179 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'grade_averages_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +GradeAveragesState _$GradeAveragesStateFromJson(Map json) { + return _GradeAveragesState.fromJson(json); +} + +/// @nodoc +mixin _$GradeAveragesState { + GradingSchemes get gradingScheme => throw _privateConstructorUsedError; + List get grades => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $GradeAveragesStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GradeAveragesStateCopyWith<$Res> { + factory $GradeAveragesStateCopyWith( + GradeAveragesState value, $Res Function(GradeAveragesState) then) = + _$GradeAveragesStateCopyWithImpl<$Res, GradeAveragesState>; + @useResult + $Res call({GradingSchemes gradingScheme, List grades}); +} + +/// @nodoc +class _$GradeAveragesStateCopyWithImpl<$Res, $Val extends GradeAveragesState> + implements $GradeAveragesStateCopyWith<$Res> { + _$GradeAveragesStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? gradingScheme = null, + Object? grades = null, + }) { + return _then(_value.copyWith( + gradingScheme: null == gradingScheme + ? _value.gradingScheme + : gradingScheme // ignore: cast_nullable_to_non_nullable + as GradingSchemes, + grades: null == grades + ? _value.grades + : grades // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$GradeAveragesStateImplCopyWith<$Res> + implements $GradeAveragesStateCopyWith<$Res> { + factory _$$GradeAveragesStateImplCopyWith(_$GradeAveragesStateImpl value, + $Res Function(_$GradeAveragesStateImpl) then) = + __$$GradeAveragesStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({GradingSchemes gradingScheme, List grades}); +} + +/// @nodoc +class __$$GradeAveragesStateImplCopyWithImpl<$Res> + extends _$GradeAveragesStateCopyWithImpl<$Res, _$GradeAveragesStateImpl> + implements _$$GradeAveragesStateImplCopyWith<$Res> { + __$$GradeAveragesStateImplCopyWithImpl(_$GradeAveragesStateImpl _value, + $Res Function(_$GradeAveragesStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? gradingScheme = null, + Object? grades = null, + }) { + return _then(_$GradeAveragesStateImpl( + gradingScheme: null == gradingScheme + ? _value.gradingScheme + : gradingScheme // ignore: cast_nullable_to_non_nullable + as GradingSchemes, + grades: null == grades + ? _value._grades + : grades // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$GradeAveragesStateImpl implements _GradeAveragesState { + const _$GradeAveragesStateImpl( + {required this.gradingScheme, required final List grades}) + : _grades = grades; + + factory _$GradeAveragesStateImpl.fromJson(Map json) => + _$$GradeAveragesStateImplFromJson(json); + + @override + final GradingSchemes gradingScheme; + final List _grades; + @override + List get grades { + if (_grades is EqualUnmodifiableListView) return _grades; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_grades); + } + + @override + String toString() { + return 'GradeAveragesState(gradingScheme: $gradingScheme, grades: $grades)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GradeAveragesStateImpl && + (identical(other.gradingScheme, gradingScheme) || + other.gradingScheme == gradingScheme) && + const DeepCollectionEquality().equals(other._grades, _grades)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, gradingScheme, const DeepCollectionEquality().hash(_grades)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$GradeAveragesStateImplCopyWith<_$GradeAveragesStateImpl> get copyWith => + __$$GradeAveragesStateImplCopyWithImpl<_$GradeAveragesStateImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$GradeAveragesStateImplToJson( + this, + ); + } +} + +abstract class _GradeAveragesState implements GradeAveragesState { + const factory _GradeAveragesState( + {required final GradingSchemes gradingScheme, + required final List grades}) = _$GradeAveragesStateImpl; + + factory _GradeAveragesState.fromJson(Map json) = + _$GradeAveragesStateImpl.fromJson; + + @override + GradingSchemes get gradingScheme; + @override + List get grades; + @override + @JsonKey(ignore: true) + _$$GradeAveragesStateImplCopyWith<_$GradeAveragesStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/state/app/application/gradeAverages/grade_averages_state.g.dart b/lib/state/app/application/gradeAverages/grade_averages_state.g.dart new file mode 100644 index 0000000..056d665 --- /dev/null +++ b/lib/state/app/application/gradeAverages/grade_averages_state.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'grade_averages_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$GradeAveragesStateImpl _$$GradeAveragesStateImplFromJson( + Map json) => + _$GradeAveragesStateImpl( + gradingScheme: + $enumDecode(_$GradingSchemesEnumMap, json['gradingScheme']), + grades: (json['grades'] as List).map((e) => e as int).toList(), + ); + +Map _$$GradeAveragesStateImplToJson( + _$GradeAveragesStateImpl instance) => + { + 'gradingScheme': _$GradingSchemesEnumMap[instance.gradingScheme]!, + 'grades': instance.grades, + }; + +const _$GradingSchemesEnumMap = { + GradingSchemes.middleSchool: 'middleSchool', + GradingSchemes.highSchool: 'highSchool', +}; diff --git a/lib/state/app/application/marianumMessage/marianum_message_controller.dart b/lib/state/app/application/marianumMessage/marianum_message_controller.dart new file mode 100644 index 0000000..47e7881 --- /dev/null +++ b/lib/state/app/application/marianumMessage/marianum_message_controller.dart @@ -0,0 +1,24 @@ +import '../../../infrastructure/controller.dart'; +import '../../../infrastructure/loadable_state.dart'; +import 'marianum_message_state.dart'; + +class MarianumMessageController extends Controller> { + MarianumMessageController() : super(const LoadableState(loadingState: LoadingState.none, data: MarianumMessageState(test: []))); + + void loading() { + emit(state.loading()); + Future.delayed(const Duration(seconds: 3)).then((value) => emit(state.done(const MarianumMessageState(test: [])))); + } + + void backgroundLoading() { + emit(state.cached(const MarianumMessageState(test: []))); + } + + void done() { + emit(state.done(const MarianumMessageState(test: []))); + } + + void error() { + + } +} diff --git a/lib/state/app/application/marianumMessage/marianum_message_state.dart b/lib/state/app/application/marianumMessage/marianum_message_state.dart new file mode 100644 index 0000000..9d893f0 --- /dev/null +++ b/lib/state/app/application/marianumMessage/marianum_message_state.dart @@ -0,0 +1,10 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'marianum_message_state.freezed.dart'; + +@freezed +class MarianumMessageState with _$MarianumMessageState { + const factory MarianumMessageState({ + required List test + }) = _MarianumMessageState; +} diff --git a/lib/state/app/application/marianumMessage/marianum_message_state.freezed.dart b/lib/state/app/application/marianumMessage/marianum_message_state.freezed.dart new file mode 100644 index 0000000..49a75a1 --- /dev/null +++ b/lib/state/app/application/marianumMessage/marianum_message_state.freezed.dart @@ -0,0 +1,143 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'marianum_message_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$MarianumMessageState { + List get test => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $MarianumMessageStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MarianumMessageStateCopyWith<$Res> { + factory $MarianumMessageStateCopyWith(MarianumMessageState value, + $Res Function(MarianumMessageState) then) = + _$MarianumMessageStateCopyWithImpl<$Res, MarianumMessageState>; + @useResult + $Res call({List test}); +} + +/// @nodoc +class _$MarianumMessageStateCopyWithImpl<$Res, + $Val extends MarianumMessageState> + implements $MarianumMessageStateCopyWith<$Res> { + _$MarianumMessageStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? test = null, + }) { + return _then(_value.copyWith( + test: null == test + ? _value.test + : test // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MarianumMessageStateImplCopyWith<$Res> + implements $MarianumMessageStateCopyWith<$Res> { + factory _$$MarianumMessageStateImplCopyWith(_$MarianumMessageStateImpl value, + $Res Function(_$MarianumMessageStateImpl) then) = + __$$MarianumMessageStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List test}); +} + +/// @nodoc +class __$$MarianumMessageStateImplCopyWithImpl<$Res> + extends _$MarianumMessageStateCopyWithImpl<$Res, _$MarianumMessageStateImpl> + implements _$$MarianumMessageStateImplCopyWith<$Res> { + __$$MarianumMessageStateImplCopyWithImpl(_$MarianumMessageStateImpl _value, + $Res Function(_$MarianumMessageStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? test = null, + }) { + return _then(_$MarianumMessageStateImpl( + test: null == test + ? _value._test + : test // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$MarianumMessageStateImpl implements _MarianumMessageState { + const _$MarianumMessageStateImpl({required final List test}) + : _test = test; + + final List _test; + @override + List get test { + if (_test is EqualUnmodifiableListView) return _test; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_test); + } + + @override + String toString() { + return 'MarianumMessageState(test: $test)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MarianumMessageStateImpl && + const DeepCollectionEquality().equals(other._test, _test)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(_test)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MarianumMessageStateImplCopyWith<_$MarianumMessageStateImpl> + get copyWith => + __$$MarianumMessageStateImplCopyWithImpl<_$MarianumMessageStateImpl>( + this, _$identity); +} + +abstract class _MarianumMessageState implements MarianumMessageState { + const factory _MarianumMessageState({required final List test}) = + _$MarianumMessageStateImpl; + + @override + List get test; + @override + @JsonKey(ignore: true) + _$$MarianumMessageStateImplCopyWith<_$MarianumMessageStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/state/app/base/account/account_controller.dart b/lib/state/app/base/account/account_controller.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/state/infrastructure/controller.dart b/lib/state/infrastructure/controller.dart new file mode 100644 index 0000000..2ceca86 --- /dev/null +++ b/lib/state/infrastructure/controller.dart @@ -0,0 +1,5 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +abstract class Controller extends Cubit { + Controller(super.initialState); +} diff --git a/lib/state/infrastructure/loadable_state.dart b/lib/state/infrastructure/loadable_state.dart new file mode 100644 index 0000000..c3b0b12 --- /dev/null +++ b/lib/state/infrastructure/loadable_state.dart @@ -0,0 +1,31 @@ + +class LoadableState { + final LoadingState loadingState; + final TState? data; + + const LoadableState({required this.loadingState, required this.data}); + + LoadableState loading() => + LoadableState(loadingState: LoadingState.loading, data: null); + + LoadableState cached(TState state) => + LoadableState(loadingState: LoadingState.loading, data: state); + + LoadableState done(TState state) => + LoadableState(loadingState: LoadingState.none, data: state); + + LoadableState error(TState state) => + LoadableState(loadingState: LoadingState.none, data: state); + + bool isBackgroundLoading() => + loadingState == LoadingState.loading && data != null; + + bool hasStateData() => + data != null; +} + +enum LoadingState { + loading, + failed, + none, +} diff --git a/lib/state/infrastructure/state_extensions.dart b/lib/state/infrastructure/state_extensions.dart new file mode 100644 index 0000000..1a828e1 --- /dev/null +++ b/lib/state/infrastructure/state_extensions.dart @@ -0,0 +1,7 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +extension StateExtensions on BuildContext { + TState readController() => read(); + TState watchController() => watch(); +} diff --git a/lib/state/widgets/controller_consumer.dart b/lib/state/widgets/controller_consumer.dart new file mode 100644 index 0000000..70b5302 --- /dev/null +++ b/lib/state/widgets/controller_consumer.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../infrastructure/controller.dart'; + +class ControllerConsumer, TState> extends StatelessWidget { + final Widget Function(BuildContext context, TState state) child; + const ControllerConsumer({required this.child, super.key}); + + @override + Widget build(BuildContext context) => BlocBuilder(builder: child); +} diff --git a/lib/state/widgets/controller_provider.dart b/lib/state/widgets/controller_provider.dart new file mode 100644 index 0000000..af9d9d7 --- /dev/null +++ b/lib/state/widgets/controller_provider.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/single_child_widget.dart'; + +import '../infrastructure/controller.dart'; + + +class ControllerProvider extends SingleChildStatelessWidget { + final TState Function(BuildContext context) create; + final bool lazy; + final Widget Function(BuildContext context) child; + ControllerProvider({required this.create, this.lazy = true, required this.child, super.key}) + : super(child: Builder(builder: child)); + + @override + Widget buildWithChild(BuildContext context, Widget? child) => BlocProvider( + create: create, + lazy: lazy, + child: child, + ); +} diff --git a/lib/state/widgets/controllers_provider.dart b/lib/state/widgets/controllers_provider.dart new file mode 100644 index 0000000..0474581 --- /dev/null +++ b/lib/state/widgets/controllers_provider.dart @@ -0,0 +1,17 @@ + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'controller_provider.dart'; + +class ControllersProvider extends StatelessWidget { + final List controllers; + final Widget Function(BuildContext context) child; + const ControllersProvider({required this.controllers, required this.child, super.key}); + + @override + Widget build(BuildContext context) => MultiBlocProvider( + providers: controllers, + child: Builder(builder: child) + ); +} diff --git a/lib/state/widgets/loadable_controller_consumer.dart b/lib/state/widgets/loadable_controller_consumer.dart new file mode 100644 index 0000000..3f82187 --- /dev/null +++ b/lib/state/widgets/loadable_controller_consumer.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +import '../infrastructure/controller.dart'; +import '../infrastructure/loadable_state.dart'; +import '../infrastructure/state_extensions.dart'; + +class LoadableControllerConsumer, TState extends LoadableState> extends StatelessWidget { + final Widget Function(BuildContext context, TState state) child; + const LoadableControllerConsumer({required this.child, super.key}); + + @override + Widget build(BuildContext context) { + var state = context.readController().state; + return Stack( + children: [ + if(!state.hasStateData()) const Center(child: CircularProgressIndicator()), + + AnimatedSwitcher( + duration: const Duration(milliseconds: 100), + transitionBuilder: (Widget child, Animation animation) => SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: state.isBackgroundLoading() ? const LinearProgressIndicator() : const SizedBox.shrink(), + ), + + AnimatedOpacity( + opacity: state.hasStateData() ? 1.0 : 0.0, + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + child: state.hasStateData() ? child(context, state) : const SizedBox.shrink() + ), + ], + ); + } +} diff --git a/lib/state/widgets/sub_selected_controller_consumer.dart b/lib/state/widgets/sub_selected_controller_consumer.dart new file mode 100644 index 0000000..d57730b --- /dev/null +++ b/lib/state/widgets/sub_selected_controller_consumer.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SubSelectedControllerConsumer, TFullState, TFilteredState> extends StatelessWidget { + final Widget Function(BuildContext context, TFilteredState state) child; + final TFilteredState Function(TFullState state) subselect; + const SubSelectedControllerConsumer({required this.subselect, required this.child, super.key}); + + @override + Widget build(BuildContext context) => BlocSelector(selector: subselect, builder: child); +} diff --git a/lib/view/pages/more/test.dart b/lib/view/pages/more/test.dart new file mode 100644 index 0000000..dcac21a --- /dev/null +++ b/lib/view/pages/more/test.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import '../../../state/app/application/marianumMessage/marianum_message_controller.dart'; +import '../../../state/app/application/marianumMessage/marianum_message_state.dart'; +import '../../../state/infrastructure/loadable_state.dart'; +import '../../../state/infrastructure/state_extensions.dart'; +import '../../../state/widgets/controller_consumer.dart'; +import '../../../state/widgets/loadable_controller_consumer.dart'; +import '../../../state/widgets/sub_selected_controller_consumer.dart'; +import '../../../state/widgets/controller_provider.dart'; + +class Test extends StatelessWidget { + const Test({super.key}); + + @override + Widget build(BuildContext context) => ControllerProvider( + create: (context) => MarianumMessageController(), + child: (context) => Scaffold( + appBar: AppBar(title: const Text("TEST")), + body: LoadableControllerConsumer>( + child: (context, data) => Column( + children: [ + TextButton( + onPressed: () => context.readController().loading(), + child: Text(data.loadingState.toString()) + ), + TextButton( + onPressed: () => context.readController().backgroundLoading(), + child: Text(context.watchController().state.loadingState.toString()) + ), + TextButton( + onPressed: () => context.readController().done(), + child: Text(context.watchController().state.loadingState.toString()) + ), + ControllerConsumer>(child: (context, state) => Text(state.data!.test.toString())), + SubSelectedControllerConsumer, LoadingState>( + subselect: (state) => state.loadingState, + child: (context, state) => Text(state.toString()), + ) + ], + ), + ), + ), + ); +} diff --git a/lib/view/pages/overhang.dart b/lib/view/pages/overhang.dart index 0fbe65a..79f59bf 100644 --- a/lib/view/pages/overhang.dart +++ b/lib/view/pages/overhang.dart @@ -16,6 +16,7 @@ import 'more/holidays/holidays.dart'; import 'more/message/message.dart'; import 'more/roomplan/roomplan.dart'; import 'more/share/selectShareTypeDialog.dart'; +import 'more/test.dart'; class Overhang extends StatelessWidget { const Overhang({super.key}); @@ -74,6 +75,10 @@ class Overhang extends StatelessWidget { trailing: const Icon(Icons.arrow_right), onTap: () => pushScreen(context, withNavBar: false, screen: const FeedbackDialog()), ), + ListTile( + leading: const Icon(Icons.science_outlined), + onTap: () => pushScreen(context, withNavBar: false, screen: const Test()), + ) ], ), ); diff --git a/lib/view/pages/talk/components/chatMessage.dart b/lib/view/pages/talk/components/chatMessage.dart index 48bcaf9..7767874 100644 --- a/lib/view/pages/talk/components/chatMessage.dart +++ b/lib/view/pages/talk/components/chatMessage.dart @@ -52,7 +52,7 @@ class ChatMessage { fadeInDuration: Duration.zero, fadeOutDuration: Duration.zero, errorListener: (value) {}, - imageUrl: 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=100&y=-1&a=1', + imageUrl: 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=100&sub_selected_controller_consumer.dart=-1&a=1', ); } diff --git a/pubspec.yaml b/pubspec.yaml index 5f44576..a97a3c9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: shared_preferences: ^2.0.15 provider: ^6.0.4 jiffy: ^6.1.0 - json_annotation: ^4.8.0 + json_annotation: ^4.8.1 localstore: ^1.2.3 intl: ^0.18.0 nextcloud: @@ -98,12 +98,15 @@ dependencies: time_range_picker: ^2.2.0 in_app_review: ^2.0.8 emoji_picker_flutter: ^2.1.1 + bloc: ^8.1.4 + flutter_bloc: ^8.1.5 + freezed_annotation: ^2.4.1 dev_dependencies: flutter_test: sdk: flutter - json_serializable: ^6.6.1 - build_runner: ^2.3.3 + json_serializable: ^6.7.1 + build_runner: ^2.4.9 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is @@ -111,6 +114,7 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^3.0.1 + freezed: ^2.5.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From 450c26b187cac54c4a725c226f901a355fea9adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Tue, 23 Apr 2024 14:48:12 +0200 Subject: [PATCH 02/19] WIP state management loadable errorbar --- .../marianum_message_controller.dart | 2 +- lib/state/infrastructure/loadable_state.dart | 10 ++- .../widgets/loadable_controller_consumer.dart | 72 +++++++++++++++---- lib/view/pages/more/test.dart | 10 ++- 4 files changed, 73 insertions(+), 21 deletions(-) diff --git a/lib/state/app/application/marianumMessage/marianum_message_controller.dart b/lib/state/app/application/marianumMessage/marianum_message_controller.dart index 47e7881..22ebc2b 100644 --- a/lib/state/app/application/marianumMessage/marianum_message_controller.dart +++ b/lib/state/app/application/marianumMessage/marianum_message_controller.dart @@ -19,6 +19,6 @@ class MarianumMessageController extends Controller { LoadableState done(TState state) => LoadableState(loadingState: LoadingState.none, data: state); - LoadableState error(TState state) => - LoadableState(loadingState: LoadingState.none, data: state); + LoadableState error({TState? state}) => + LoadableState(loadingState: LoadingState.failed, data: state); bool isBackgroundLoading() => loadingState == LoadingState.loading && data != null; + bool hasError() => + loadingState == LoadingState.failed; + bool hasStateData() => data != null; + + bool errorBarVisible() => + hasError() && hasStateData(); } enum LoadingState { diff --git a/lib/state/widgets/loadable_controller_consumer.dart b/lib/state/widgets/loadable_controller_consumer.dart index 3f82187..7ac0412 100644 --- a/lib/state/widgets/loadable_controller_consumer.dart +++ b/lib/state/widgets/loadable_controller_consumer.dart @@ -5,35 +5,77 @@ import '../infrastructure/loadable_state.dart'; import '../infrastructure/state_extensions.dart'; class LoadableControllerConsumer, TState extends LoadableState> extends StatelessWidget { - final Widget Function(BuildContext context, TState state) child; + final Widget child; const LoadableControllerConsumer({required this.child, super.key}); + final Duration animationDuration = const Duration(milliseconds: 200); + @override Widget build(BuildContext context) { var state = context.readController().state; - return Stack( + + var loadableContent = Stack( children: [ - if(!state.hasStateData()) const Center(child: CircularProgressIndicator()), + AnimatedOpacity( + opacity: !state.hasStateData() ? 1.0 : 0.0, + duration: animationDuration, + curve: Curves.easeInOut, + child: const Center(child: CircularProgressIndicator()), + ), AnimatedSwitcher( - duration: const Duration(milliseconds: 100), + duration: animationDuration, transitionBuilder: (Widget child, Animation animation) => SlideTransition( - position: Tween( - begin: const Offset(0.0, -1.0), - end: Offset.zero, - ).animate(animation), - child: child, - ), - child: state.isBackgroundLoading() ? const LinearProgressIndicator() : const SizedBox.shrink(), + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: state.isBackgroundLoading() && !state.errorBarVisible() ? const LinearProgressIndicator() : const SizedBox.shrink(), ), AnimatedOpacity( - opacity: state.hasStateData() ? 1.0 : 0.0, - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - child: state.hasStateData() ? child(context, state) : const SizedBox.shrink() + opacity: state.hasStateData() ? 1.0 : 0.0, + duration: animationDuration, + curve: Curves.easeInOut, + child: state.hasStateData() ? child : const SizedBox.shrink() ), ], ); + + var errorBar = AnimatedSwitcher( + duration: animationDuration, + transitionBuilder: (Widget child, Animation animation) => SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: !state.errorBarVisible() + ? const SizedBox.shrink() + : Container( + height: 20, + decoration: const BoxDecoration( + color: Colors.red, + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.wifi_find_outlined, size: 12), + SizedBox(width: 10), + Text('Keine Verbindung', style: TextStyle(fontSize: 12)) + ], + ), + ), + ); + + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [errorBar, loadableContent], + ); } } diff --git a/lib/view/pages/more/test.dart b/lib/view/pages/more/test.dart index dcac21a..fb1966a 100644 --- a/lib/view/pages/more/test.dart +++ b/lib/view/pages/more/test.dart @@ -15,13 +15,13 @@ class Test extends StatelessWidget { Widget build(BuildContext context) => ControllerProvider( create: (context) => MarianumMessageController(), child: (context) => Scaffold( - appBar: AppBar(title: const Text("TEST")), + appBar: AppBar(title: const Text('TEST')), body: LoadableControllerConsumer>( - child: (context, data) => Column( + child: Column( children: [ TextButton( onPressed: () => context.readController().loading(), - child: Text(data.loadingState.toString()) + child: Text(context.watchController().state.loadingState.toString()) ), TextButton( onPressed: () => context.readController().backgroundLoading(), @@ -31,6 +31,10 @@ class Test extends StatelessWidget { onPressed: () => context.readController().done(), child: Text(context.watchController().state.loadingState.toString()) ), + TextButton( + onPressed: () => context.readController().error(), + child: Text(context.watchController().state.loadingState.toString()) + ), ControllerConsumer>(child: (context, state) => Text(state.data!.test.toString())), SubSelectedControllerConsumer, LoadingState>( subselect: (state) => state.loadingState, From 04e8ce9c0a98ebe421a7093bc458359990c5d238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Tue, 23 Apr 2024 22:40:18 +0200 Subject: [PATCH 03/19] loadable state is now detecting device connection status on failure --- .../errorBar/error_bar_controller.dart | 26 ++++ .../errorBar/error_bar_state.dart | 11 ++ .../errorBar/error_bar_state.freezed.dart | 146 ++++++++++++++++++ .../background_loading_indicator.dart | 21 +++ lib/state/widgets/components/error_bar.dart | 53 +++++++ .../components/primary_loading_indicator.dart | 16 ++ .../widgets/loadable_controller_consumer.dart | 77 +++------ .../sub_selected_controller_consumer.dart | 6 +- lib/view/pages/more/test.dart | 2 +- pubspec.yaml | 1 + 10 files changed, 296 insertions(+), 63 deletions(-) create mode 100644 lib/state/app/base/infrastructure/errorBar/error_bar_controller.dart create mode 100644 lib/state/app/base/infrastructure/errorBar/error_bar_state.dart create mode 100644 lib/state/app/base/infrastructure/errorBar/error_bar_state.freezed.dart create mode 100644 lib/state/widgets/components/background_loading_indicator.dart create mode 100644 lib/state/widgets/components/error_bar.dart create mode 100644 lib/state/widgets/components/primary_loading_indicator.dart diff --git a/lib/state/app/base/infrastructure/errorBar/error_bar_controller.dart b/lib/state/app/base/infrastructure/errorBar/error_bar_controller.dart new file mode 100644 index 0000000..b5a7858 --- /dev/null +++ b/lib/state/app/base/infrastructure/errorBar/error_bar_controller.dart @@ -0,0 +1,26 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; + +import '../../../../infrastructure/controller.dart'; +import 'error_bar_state.dart'; + +class ErrorBarController extends Controller { + late StreamSubscription> _updateStream; + + ErrorBarController() : super(const ErrorBarState(connections: null)) { + emitState(List v) => emit(state.copyWith(connections: v)); + + Connectivity().checkConnectivity().then(emitState); + _updateStream = Connectivity().onConnectivityChanged.listen(emitState); + } + + bool connectivityStatusKnown() => state.connections != null; + bool isConnected({bool? def}) => !(state.connections?.contains(ConnectivityResult.none) ?? def!); + + @override + Future close() { + _updateStream.cancel(); + return super.close(); + } +} diff --git a/lib/state/app/base/infrastructure/errorBar/error_bar_state.dart b/lib/state/app/base/infrastructure/errorBar/error_bar_state.dart new file mode 100644 index 0000000..59a8b22 --- /dev/null +++ b/lib/state/app/base/infrastructure/errorBar/error_bar_state.dart @@ -0,0 +1,11 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'error_bar_state.freezed.dart'; + +@freezed +class ErrorBarState with _$ErrorBarState { + const factory ErrorBarState({ + required List? connections, + }) = _ErrorBarState; +} diff --git a/lib/state/app/base/infrastructure/errorBar/error_bar_state.freezed.dart b/lib/state/app/base/infrastructure/errorBar/error_bar_state.freezed.dart new file mode 100644 index 0000000..2e60c16 --- /dev/null +++ b/lib/state/app/base/infrastructure/errorBar/error_bar_state.freezed.dart @@ -0,0 +1,146 @@ +// 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 'error_bar_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ErrorBarState { + List? get connections => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ErrorBarStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ErrorBarStateCopyWith<$Res> { + factory $ErrorBarStateCopyWith( + ErrorBarState value, $Res Function(ErrorBarState) then) = + _$ErrorBarStateCopyWithImpl<$Res, ErrorBarState>; + @useResult + $Res call({List? connections}); +} + +/// @nodoc +class _$ErrorBarStateCopyWithImpl<$Res, $Val extends ErrorBarState> + implements $ErrorBarStateCopyWith<$Res> { + _$ErrorBarStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? connections = freezed, + }) { + return _then(_value.copyWith( + connections: freezed == connections + ? _value.connections + : connections // ignore: cast_nullable_to_non_nullable + as List?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ErrorBarStateImplCopyWith<$Res> + implements $ErrorBarStateCopyWith<$Res> { + factory _$$ErrorBarStateImplCopyWith( + _$ErrorBarStateImpl value, $Res Function(_$ErrorBarStateImpl) then) = + __$$ErrorBarStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List? connections}); +} + +/// @nodoc +class __$$ErrorBarStateImplCopyWithImpl<$Res> + extends _$ErrorBarStateCopyWithImpl<$Res, _$ErrorBarStateImpl> + implements _$$ErrorBarStateImplCopyWith<$Res> { + __$$ErrorBarStateImplCopyWithImpl( + _$ErrorBarStateImpl _value, $Res Function(_$ErrorBarStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? connections = freezed, + }) { + return _then(_$ErrorBarStateImpl( + connections: freezed == connections + ? _value._connections + : connections // ignore: cast_nullable_to_non_nullable + as List?, + )); + } +} + +/// @nodoc + +class _$ErrorBarStateImpl implements _ErrorBarState { + const _$ErrorBarStateImpl( + {required final List? connections}) + : _connections = connections; + + final List? _connections; + @override + List? get connections { + final value = _connections; + if (value == null) return null; + if (_connections is EqualUnmodifiableListView) return _connections; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'ErrorBarState(connections: $connections)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ErrorBarStateImpl && + const DeepCollectionEquality() + .equals(other._connections, _connections)); + } + + @override + int get hashCode => Object.hash( + runtimeType, const DeepCollectionEquality().hash(_connections)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ErrorBarStateImplCopyWith<_$ErrorBarStateImpl> get copyWith => + __$$ErrorBarStateImplCopyWithImpl<_$ErrorBarStateImpl>(this, _$identity); +} + +abstract class _ErrorBarState implements ErrorBarState { + const factory _ErrorBarState( + {required final List? connections}) = + _$ErrorBarStateImpl; + + @override + List? get connections; + @override + @JsonKey(ignore: true) + _$$ErrorBarStateImplCopyWith<_$ErrorBarStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/state/widgets/components/background_loading_indicator.dart b/lib/state/widgets/components/background_loading_indicator.dart new file mode 100644 index 0000000..a7c4048 --- /dev/null +++ b/lib/state/widgets/components/background_loading_indicator.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class BackgroundLoadingIndicator extends StatelessWidget { + final bool visible; + const BackgroundLoadingIndicator({required this.visible, super.key}); + + final Duration animationDuration = const Duration(milliseconds: 200); + + @override + Widget build(BuildContext context) => AnimatedSwitcher( + duration: animationDuration, + transitionBuilder: (Widget child, Animation animation) => SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: visible ? const LinearProgressIndicator() : const SizedBox.shrink(), + ); +} diff --git a/lib/state/widgets/components/error_bar.dart b/lib/state/widgets/components/error_bar.dart new file mode 100644 index 0000000..a4e6a9a --- /dev/null +++ b/lib/state/widgets/components/error_bar.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +import '../../app/base/infrastructure/errorBar/error_bar_controller.dart'; +import '../../infrastructure/state_extensions.dart'; +import '../controller_provider.dart'; + +class ErrorBar extends StatelessWidget { + final bool visible; + const ErrorBar({required this.visible, super.key}); + + final Duration animationDuration = const Duration(milliseconds: 200); + + @override + Widget build(BuildContext context) => ControllerProvider( + create: (context) => ErrorBarController(), + child: (context) => AnimatedSwitcher( + duration: animationDuration, + transitionBuilder: (Widget child, Animation animation) => SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: Visibility( + visible: visible, + child: Builder( + builder: (context) { + var controller = context.watchController(); + 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); + + return Container( + height: 20, + decoration: BoxDecoration( + color: status.color, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(status.icon, size: 14), + const SizedBox(width: 10), + Text(status.text, style: const TextStyle(fontSize: 12)) + ], + ), + ); + }, + ) + ) + ), + ); +} diff --git a/lib/state/widgets/components/primary_loading_indicator.dart b/lib/state/widgets/components/primary_loading_indicator.dart new file mode 100644 index 0000000..4f21fc1 --- /dev/null +++ b/lib/state/widgets/components/primary_loading_indicator.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class PrimaryLoadingIndicator extends StatelessWidget { + final bool visible; + const PrimaryLoadingIndicator({required this.visible, super.key}); + + final Duration animationDuration = const Duration(milliseconds: 200); + + @override + Widget build(BuildContext context) => AnimatedOpacity( + opacity: visible ? 1.0 : 0.0, + duration: animationDuration, + curve: Curves.easeInOut, + child: const Center(child: CircularProgressIndicator()), + ); +} diff --git a/lib/state/widgets/loadable_controller_consumer.dart b/lib/state/widgets/loadable_controller_consumer.dart index 7ac0412..8fa0d33 100644 --- a/lib/state/widgets/loadable_controller_consumer.dart +++ b/lib/state/widgets/loadable_controller_consumer.dart @@ -3,6 +3,9 @@ import 'package:flutter/material.dart'; import '../infrastructure/controller.dart'; import '../infrastructure/loadable_state.dart'; import '../infrastructure/state_extensions.dart'; +import 'components/background_loading_indicator.dart'; +import 'components/error_bar.dart'; +import 'components/primary_loading_indicator.dart'; class LoadableControllerConsumer, TState extends LoadableState> extends StatelessWidget { final Widget child; @@ -13,69 +16,25 @@ class LoadableControllerConsumer, TState @override Widget build(BuildContext context) { var state = context.readController().state; - - var loadableContent = Stack( + return Column( children: [ - AnimatedOpacity( - opacity: !state.hasStateData() ? 1.0 : 0.0, - duration: animationDuration, - curve: Curves.easeInOut, - child: const Center(child: CircularProgressIndicator()), - ), - - AnimatedSwitcher( - duration: animationDuration, - transitionBuilder: (Widget child, Animation animation) => SlideTransition( - position: Tween( - begin: const Offset(0.0, -1.0), - end: Offset.zero, - ).animate(animation), - child: child, - ), - child: state.isBackgroundLoading() && !state.errorBarVisible() ? const LinearProgressIndicator() : const SizedBox.shrink(), - ), - - AnimatedOpacity( - opacity: state.hasStateData() ? 1.0 : 0.0, - duration: animationDuration, - curve: Curves.easeInOut, - child: state.hasStateData() ? child : const SizedBox.shrink() - ), - ], - ); - - var errorBar = AnimatedSwitcher( - duration: animationDuration, - transitionBuilder: (Widget child, Animation animation) => SlideTransition( - position: Tween( - begin: const Offset(0.0, -1.0), - end: Offset.zero, - ).animate(animation), - child: child, - ), - child: !state.errorBarVisible() - ? const SizedBox.shrink() - : Container( - height: 20, - decoration: const BoxDecoration( - color: Colors.red, - ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, + ErrorBar(visible: state.errorBarVisible()), + Expanded( + child: Stack( children: [ - Icon(Icons.wifi_find_outlined, size: 12), - SizedBox(width: 10), - Text('Keine Verbindung', style: TextStyle(fontSize: 12)) + PrimaryLoadingIndicator(visible: !state.hasStateData()), + BackgroundLoadingIndicator(visible: state.isBackgroundLoading() && !state.errorBarVisible()), + + AnimatedOpacity( + opacity: state.hasStateData() ? 1.0 : 0.0, + duration: animationDuration, + curve: Curves.easeInOut, + child: state.hasStateData() ? child : const SizedBox.shrink() + ), ], ), - ), - ); - - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [errorBar, loadableContent], + ) + ], ); } } diff --git a/lib/state/widgets/sub_selected_controller_consumer.dart b/lib/state/widgets/sub_selected_controller_consumer.dart index d57730b..fdbe68f 100644 --- a/lib/state/widgets/sub_selected_controller_consumer.dart +++ b/lib/state/widgets/sub_selected_controller_consumer.dart @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class SubSelectedControllerConsumer, TFullState, TFilteredState> extends StatelessWidget { final Widget Function(BuildContext context, TFilteredState state) child; - final TFilteredState Function(TFullState state) subselect; - const SubSelectedControllerConsumer({required this.subselect, required this.child, super.key}); + final TFilteredState Function(TFullState state) subSelect; + const SubSelectedControllerConsumer({required this.subSelect, required this.child, super.key}); @override - Widget build(BuildContext context) => BlocSelector(selector: subselect, builder: child); + Widget build(BuildContext context) => BlocSelector(selector: subSelect, builder: child); } diff --git a/lib/view/pages/more/test.dart b/lib/view/pages/more/test.dart index fb1966a..e6c69ad 100644 --- a/lib/view/pages/more/test.dart +++ b/lib/view/pages/more/test.dart @@ -37,7 +37,7 @@ class Test extends StatelessWidget { ), ControllerConsumer>(child: (context, state) => Text(state.data!.test.toString())), SubSelectedControllerConsumer, LoadingState>( - subselect: (state) => state.loadingState, + subSelect: (state) => state.loadingState, child: (context, state) => Text(state.toString()), ) ], diff --git a/pubspec.yaml b/pubspec.yaml index a97a3c9..e9de63c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -101,6 +101,7 @@ dependencies: bloc: ^8.1.4 flutter_bloc: ^8.1.5 freezed_annotation: ^2.4.1 + connectivity_plus: ^6.0.3 dev_dependencies: flutter_test: From 3802e4a5b9596ba5df2343cd36bef6700a95abf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Wed, 24 Apr 2024 18:45:10 +0200 Subject: [PATCH 04/19] fixed missing animation on error bar --- lib/state/widgets/components/error_bar.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/state/widgets/components/error_bar.dart b/lib/state/widgets/components/error_bar.dart index a4e6a9a..56875d1 100644 --- a/lib/state/widgets/components/error_bar.dart +++ b/lib/state/widgets/components/error_bar.dart @@ -23,6 +23,7 @@ class ErrorBar extends StatelessWidget { child: child, ), child: Visibility( + key: Key(visible.hashCode.toString()), visible: visible, child: Builder( builder: (context) { From 107a5bdbf8ba26f1d78934b08e647dc9bbb94d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Wed, 24 Apr 2024 19:23:32 +0200 Subject: [PATCH 05/19] added content position animation when error bar is displayed --- lib/state/widgets/components/error_bar.dart | 71 +++++++++++---------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/lib/state/widgets/components/error_bar.dart b/lib/state/widgets/components/error_bar.dart index 56875d1..6d63b25 100644 --- a/lib/state/widgets/components/error_bar.dart +++ b/lib/state/widgets/components/error_bar.dart @@ -13,42 +13,45 @@ class ErrorBar extends StatelessWidget { @override Widget build(BuildContext context) => ControllerProvider( create: (context) => ErrorBarController(), - child: (context) => AnimatedSwitcher( + child: (context) => AnimatedSize( duration: animationDuration, - transitionBuilder: (Widget child, Animation animation) => SlideTransition( - position: Tween( - begin: const Offset(0.0, -1.0), - end: Offset.zero, - ).animate(animation), - child: child, - ), - child: Visibility( - key: Key(visible.hashCode.toString()), - visible: visible, - child: Builder( - builder: (context) { - var controller = context.watchController(); - 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); + child: AnimatedSwitcher( + duration: animationDuration, + transitionBuilder: (Widget child, Animation animation) => SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: Visibility( + key: Key(visible.hashCode.toString()), + visible: visible, + child: Builder( + builder: (context) { + var controller = context.watchController(); + 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); - 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)) + ], + ), + ); + }, + ) + ) + ), ), ); } From 2db3b29359dd98ddabc6fabb9dd9417e48c3e3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Wed, 24 Apr 2024 19:23:32 +0200 Subject: [PATCH 06/19] added content position animation when error bar is displayed --- lib/state/widgets/components/error_bar.dart | 72 +++++++++++---------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/lib/state/widgets/components/error_bar.dart b/lib/state/widgets/components/error_bar.dart index 56875d1..af63231 100644 --- a/lib/state/widgets/components/error_bar.dart +++ b/lib/state/widgets/components/error_bar.dart @@ -13,42 +13,46 @@ class ErrorBar extends StatelessWidget { @override Widget build(BuildContext context) => ControllerProvider( create: (context) => ErrorBarController(), - child: (context) => AnimatedSwitcher( + child: (context) => AnimatedSize( duration: animationDuration, - transitionBuilder: (Widget child, Animation animation) => SlideTransition( - position: Tween( - begin: const Offset(0.0, -1.0), - end: Offset.zero, - ).animate(animation), - child: child, - ), - child: Visibility( - key: Key(visible.hashCode.toString()), - visible: visible, - child: Builder( - builder: (context) { - var controller = context.watchController(); - 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); + child: AnimatedSwitcher( + duration: animationDuration, + transitionBuilder: (Widget child, Animation animation) => SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: Visibility( + key: Key(visible.hashCode.toString()), + visible: visible, + replacement: const SizedBox(width: double.infinity), + child: Builder( + builder: (context) { + var controller = context.watchController(); + 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); - 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)) + ], + ), + ); + }, + ) + ) + ), ), ); } From f58a2ec8cd4d560ac427302ef8e6d226a56eeab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 5 May 2024 15:48:26 +0200 Subject: [PATCH 07/19] revamp on bloc approach --- build.yaml | 3 +- lib/app.dart | 2 +- lib/main.dart | 29 ++-- .../grade_averages_controller.dart | 10 -- .../marianum_message_controller.dart | 24 --- .../marianum_message_state.dart | 10 -- .../marianum_message_state.freezed.dart | 143 ------------------ lib/state/app/modules/app_modules.dart | 29 ++++ .../bloc/grade_averages_bloc.dart | 49 ++++++ .../bloc/grade_averages_event.dart | 20 +++ .../bloc}/grade_averages_state.dart | 8 +- .../bloc}/grade_averages_state.freezed.dart | 43 +++--- .../bloc}/grade_averages_state.g.dart | 13 +- .../screens/grade_averages_list_view.dart | 70 +++++++++ .../screens/grade_averages_view.dart | 98 ++++++++++++ lib/state/infrastructure/controller.dart | 5 - lib/state/infrastructure/loadable_state.dart | 37 ----- .../infrastructure/state_extensions.dart | 7 - lib/state/widgets/components/error_bar.dart | 116 +++++++------- lib/state/widgets/controller_consumer.dart | 24 +-- lib/state/widgets/controller_provider.dart | 42 ++--- lib/state/widgets/controllers_provider.dart | 34 ++--- .../widgets/loadable_controller_consumer.dart | 82 +++++----- lib/view/pages/files/fileElement.dart | 2 +- lib/view/pages/files/files.dart | 2 +- lib/view/pages/more/test.dart | 96 ++++++------ lib/view/pages/overhang.dart | 4 +- pubspec.yaml | 1 + 28 files changed, 523 insertions(+), 480 deletions(-) delete mode 100644 lib/state/app/application/gradeAverages/grade_averages_controller.dart delete mode 100644 lib/state/app/application/marianumMessage/marianum_message_controller.dart delete mode 100644 lib/state/app/application/marianumMessage/marianum_message_state.dart delete mode 100644 lib/state/app/application/marianumMessage/marianum_message_state.freezed.dart create mode 100644 lib/state/app/modules/app_modules.dart create mode 100644 lib/state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart create mode 100644 lib/state/app/modules/gradeAverages/bloc/grade_averages_event.dart rename lib/state/app/{application/gradeAverages => modules/gradeAverages/bloc}/grade_averages_state.dart (77%) rename lib/state/app/{application/gradeAverages => modules/gradeAverages/bloc}/grade_averages_state.freezed.dart (81%) rename lib/state/app/{application/gradeAverages => modules/gradeAverages/bloc}/grade_averages_state.g.dart (64%) create mode 100644 lib/state/app/modules/gradeAverages/screens/grade_averages_list_view.dart create mode 100644 lib/state/app/modules/gradeAverages/screens/grade_averages_view.dart delete mode 100644 lib/state/infrastructure/controller.dart delete mode 100644 lib/state/infrastructure/loadable_state.dart delete mode 100644 lib/state/infrastructure/state_extensions.dart diff --git a/build.yaml b/build.yaml index 24cf608..dcc8576 100644 --- a/build.yaml +++ b/build.yaml @@ -3,4 +3,5 @@ targets: builders: json_serializable: options: - explicit_to_json: false \ No newline at end of file + explicit_to_json: false + generic_argument_factories: true \ No newline at end of file diff --git a/lib/app.dart b/lib/app.dart index 11ea0ac..7802cfd 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -137,7 +137,7 @@ class _AppState extends State with WidgetsBindingObserver { ), ), PersistentTabConfig( - screen: const Breaker(breaker: BreakerArea.files, child: Files([])), + screen: Breaker(breaker: BreakerArea.files, child: Files()), item: ItemConfig( activeForegroundColor: Theme.of(context).primaryColor, inactiveForegroundColor: Theme.of(context).colorScheme.secondary, diff --git a/lib/main.dart b/lib/main.dart index 10b741b..53189a2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,8 +7,10 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:jiffy/jiffy.dart'; import 'package:loader_overlay/loader_overlay.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -33,22 +35,27 @@ import 'widget/placeholderView.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + + var initialisationTasks = [ + Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform) + .then((value) async => log("Firebase token: ${await FirebaseMessaging.instance.getToken() ?? "Error: no Firebase token!"}")) + .onError((error, stackTrace) => log('Error initializing Firebase: $error')), - try { - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - log("Firebase token: ${await FirebaseMessaging.instance.getToken() ?? "Error: no Firebase token!"}"); - } catch (e) { - log('Error initializing Firebase app!'); - } + PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem') + .then((certificate) => SecurityContext.defaultContext.setTrustedCertificatesBytes(certificate.buffer.asUint8List())), - var data = await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem'); - SecurityContext.defaultContext.setTrustedCertificatesBytes(data.buffer.asUint8List()); + Future(() async { + await HydratedStorage.build(storageDirectory: await getTemporaryDirectory()).then((storage) => HydratedBloc.storage = storage); + }) + ]; + + await Future.wait(initialisationTasks); if(kReleaseMode) { ErrorWidget.builder = (error) => PlaceholderView( - icon: Icons.phonelink_erase_rounded, - text: error.toStringShort(), - ); + icon: Icons.phonelink_erase_rounded, + text: error.toStringShort(), + ); } runApp( diff --git a/lib/state/app/application/gradeAverages/grade_averages_controller.dart b/lib/state/app/application/gradeAverages/grade_averages_controller.dart deleted file mode 100644 index 6e0f829..0000000 --- a/lib/state/app/application/gradeAverages/grade_averages_controller.dart +++ /dev/null @@ -1,10 +0,0 @@ - -import '../../../infrastructure/controller.dart'; -import 'grade_averages_state.dart'; - -class GradeAveragesController extends Controller { - GradeAveragesController(super.initialState); - - void setGradeType(GradingSchemes scheme) => emit(state.copyWith(gradingScheme: scheme)); - double average() => state.grades.reduce((a, b) => a + b) / state.grades.length; -} diff --git a/lib/state/app/application/marianumMessage/marianum_message_controller.dart b/lib/state/app/application/marianumMessage/marianum_message_controller.dart deleted file mode 100644 index 22ebc2b..0000000 --- a/lib/state/app/application/marianumMessage/marianum_message_controller.dart +++ /dev/null @@ -1,24 +0,0 @@ -import '../../../infrastructure/controller.dart'; -import '../../../infrastructure/loadable_state.dart'; -import 'marianum_message_state.dart'; - -class MarianumMessageController extends Controller> { - MarianumMessageController() : super(const LoadableState(loadingState: LoadingState.none, data: MarianumMessageState(test: []))); - - void loading() { - emit(state.loading()); - Future.delayed(const Duration(seconds: 3)).then((value) => emit(state.done(const MarianumMessageState(test: [])))); - } - - void backgroundLoading() { - emit(state.cached(const MarianumMessageState(test: []))); - } - - void done() { - emit(state.done(const MarianumMessageState(test: []))); - } - - void error() { - emit(state.error(state: const MarianumMessageState(test: []))); - } -} diff --git a/lib/state/app/application/marianumMessage/marianum_message_state.dart b/lib/state/app/application/marianumMessage/marianum_message_state.dart deleted file mode 100644 index 9d893f0..0000000 --- a/lib/state/app/application/marianumMessage/marianum_message_state.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'marianum_message_state.freezed.dart'; - -@freezed -class MarianumMessageState with _$MarianumMessageState { - const factory MarianumMessageState({ - required List test - }) = _MarianumMessageState; -} diff --git a/lib/state/app/application/marianumMessage/marianum_message_state.freezed.dart b/lib/state/app/application/marianumMessage/marianum_message_state.freezed.dart deleted file mode 100644 index 49a75a1..0000000 --- a/lib/state/app/application/marianumMessage/marianum_message_state.freezed.dart +++ /dev/null @@ -1,143 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'marianum_message_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -/// @nodoc -mixin _$MarianumMessageState { - List get test => throw _privateConstructorUsedError; - - @JsonKey(ignore: true) - $MarianumMessageStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $MarianumMessageStateCopyWith<$Res> { - factory $MarianumMessageStateCopyWith(MarianumMessageState value, - $Res Function(MarianumMessageState) then) = - _$MarianumMessageStateCopyWithImpl<$Res, MarianumMessageState>; - @useResult - $Res call({List test}); -} - -/// @nodoc -class _$MarianumMessageStateCopyWithImpl<$Res, - $Val extends MarianumMessageState> - implements $MarianumMessageStateCopyWith<$Res> { - _$MarianumMessageStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? test = null, - }) { - return _then(_value.copyWith( - test: null == test - ? _value.test - : test // ignore: cast_nullable_to_non_nullable - as List, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$MarianumMessageStateImplCopyWith<$Res> - implements $MarianumMessageStateCopyWith<$Res> { - factory _$$MarianumMessageStateImplCopyWith(_$MarianumMessageStateImpl value, - $Res Function(_$MarianumMessageStateImpl) then) = - __$$MarianumMessageStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({List test}); -} - -/// @nodoc -class __$$MarianumMessageStateImplCopyWithImpl<$Res> - extends _$MarianumMessageStateCopyWithImpl<$Res, _$MarianumMessageStateImpl> - implements _$$MarianumMessageStateImplCopyWith<$Res> { - __$$MarianumMessageStateImplCopyWithImpl(_$MarianumMessageStateImpl _value, - $Res Function(_$MarianumMessageStateImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? test = null, - }) { - return _then(_$MarianumMessageStateImpl( - test: null == test - ? _value._test - : test // ignore: cast_nullable_to_non_nullable - as List, - )); - } -} - -/// @nodoc - -class _$MarianumMessageStateImpl implements _MarianumMessageState { - const _$MarianumMessageStateImpl({required final List test}) - : _test = test; - - final List _test; - @override - List get test { - if (_test is EqualUnmodifiableListView) return _test; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_test); - } - - @override - String toString() { - return 'MarianumMessageState(test: $test)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$MarianumMessageStateImpl && - const DeepCollectionEquality().equals(other._test, _test)); - } - - @override - int get hashCode => - Object.hash(runtimeType, const DeepCollectionEquality().hash(_test)); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$MarianumMessageStateImplCopyWith<_$MarianumMessageStateImpl> - get copyWith => - __$$MarianumMessageStateImplCopyWithImpl<_$MarianumMessageStateImpl>( - this, _$identity); -} - -abstract class _MarianumMessageState implements MarianumMessageState { - const factory _MarianumMessageState({required final List test}) = - _$MarianumMessageStateImpl; - - @override - List get test; - @override - @JsonKey(ignore: true) - _$$MarianumMessageStateImplCopyWith<_$MarianumMessageStateImpl> - get copyWith => throw _privateConstructorUsedError; -} diff --git a/lib/state/app/modules/app_modules.dart b/lib/state/app/modules/app_modules.dart new file mode 100644 index 0000000..28849dc --- /dev/null +++ b/lib/state/app/modules/app_modules.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import '../../../view/pages/files/files.dart'; +import '../../../view/pages/talk/chatList.dart'; +import '../../../view/pages/timetable/timetable.dart'; + +class AppModule { + String name; + IconData icon; + Widget Function() create; + + AppModule(this.name, this.icon, this.create); + + static Map modules() => { + Module.timetable: AppModule('Vertretung', Icons.calendar_month, Timetable.new), + Module.talk: AppModule('Talk', Icons.chat, ChatList.new), + Module.files: AppModule('Files', Icons.folder, Files.new), + }; +} + +enum Module { + timetable, + talk, + files, + marianumMessage, + roomPlan, + gradeAveragesCalculator, + holidays, +} diff --git a/lib/state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart b/lib/state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart new file mode 100644 index 0000000..6a084ca --- /dev/null +++ b/lib/state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart @@ -0,0 +1,49 @@ +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'grade_averages_event.dart'; +import 'grade_averages_state.dart'; + +class GradeAveragesBloc extends HydratedBloc { + GradeAveragesBloc() : super(const GradeAveragesState(gradingSystem: GradeAveragesGradingSystem.middleSchool, grades: [])) { + + on((event, emit) { + add(ResetAll()); + emit( + state.copyWith( + gradingSystem: event.isMiddleSchool + ? GradeAveragesGradingSystem.middleSchool + : GradeAveragesGradingSystem.highSchool + ) + ); + }); + + on((event, emit) { + emit(state.copyWith(grades: [])); + }); + + on((event, emit) { + emit(state.copyWith(grades: [...state.grades]..removeWhere((grade) => grade == event.grade))); + }); + + on((event, emit) { + emit(state.copyWith(grades: [...state.grades, event.grade])); + }); + + on((event, emit) { + emit(state.copyWith(grades: List.from(state.grades)..remove(event.grade))); + }); + + } + + double average() => state.grades.isEmpty ? 0 : state.grades.reduce((a, b) => a + b) / state.grades.length; + bool isMiddleSchool() => state.gradingSystem == GradeAveragesGradingSystem.middleSchool; + bool canDecrementOrDelete(int grade) => state.grades.contains(grade); + int countOfGrade(int grade) => state.grades.where((g) => g == grade).length; + int gradesInGradingSystem() => state.gradingSystem == GradeAveragesGradingSystem.middleSchool ? 6 : 16; + int getGradeFromIndex(int index) => isMiddleSchool() ? index + 1 : 15 - index; + + @override + GradeAveragesState? fromJson(Map json) => GradeAveragesState.fromJson(json); + @override + Map? toJson(GradeAveragesState state) => state.toJson(); +} diff --git a/lib/state/app/modules/gradeAverages/bloc/grade_averages_event.dart b/lib/state/app/modules/gradeAverages/bloc/grade_averages_event.dart new file mode 100644 index 0000000..0be46eb --- /dev/null +++ b/lib/state/app/modules/gradeAverages/bloc/grade_averages_event.dart @@ -0,0 +1,20 @@ + +sealed class GradeAveragesEvent {} + +final class GradingSystemChanged extends GradeAveragesEvent { + final bool isMiddleSchool; + GradingSystemChanged(this.isMiddleSchool); +} +final class ResetAll extends GradeAveragesEvent {} +final class ResetGrade extends GradeAveragesEvent { + final int grade; + ResetGrade(this.grade); +} +final class IncrementGrade extends GradeAveragesEvent { + final int grade; + IncrementGrade(this.grade); +} +final class DecrementGrade extends GradeAveragesEvent { + final int grade; + DecrementGrade(this.grade); +} diff --git a/lib/state/app/application/gradeAverages/grade_averages_state.dart b/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.dart similarity index 77% rename from lib/state/app/application/gradeAverages/grade_averages_state.dart rename to lib/state/app/modules/gradeAverages/bloc/grade_averages_state.dart index c9df18e..0462541 100644 --- a/lib/state/app/application/gradeAverages/grade_averages_state.dart +++ b/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.dart @@ -6,14 +6,14 @@ part 'grade_averages_state.g.dart'; @freezed class GradeAveragesState with _$GradeAveragesState { const factory GradeAveragesState({ - required GradingSchemes gradingScheme, - required List grades, + required GradeAveragesGradingSystem gradingSystem, + required List grades, }) = _GradeAveragesState; factory GradeAveragesState.fromJson(Map json) => _$GradeAveragesStateFromJson(json); } -enum GradingSchemes { - middleSchool, +enum GradeAveragesGradingSystem { highSchool, + middleSchool, } diff --git a/lib/state/app/application/gradeAverages/grade_averages_state.freezed.dart b/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.freezed.dart similarity index 81% rename from lib/state/app/application/gradeAverages/grade_averages_state.freezed.dart rename to lib/state/app/modules/gradeAverages/bloc/grade_averages_state.freezed.dart index 9985e09..c636f4d 100644 --- a/lib/state/app/application/gradeAverages/grade_averages_state.freezed.dart +++ b/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.freezed.dart @@ -20,7 +20,8 @@ GradeAveragesState _$GradeAveragesStateFromJson(Map json) { /// @nodoc mixin _$GradeAveragesState { - GradingSchemes get gradingScheme => throw _privateConstructorUsedError; + GradeAveragesGradingSystem get gradingSystem => + throw _privateConstructorUsedError; List get grades => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @@ -35,7 +36,7 @@ abstract class $GradeAveragesStateCopyWith<$Res> { GradeAveragesState value, $Res Function(GradeAveragesState) then) = _$GradeAveragesStateCopyWithImpl<$Res, GradeAveragesState>; @useResult - $Res call({GradingSchemes gradingScheme, List grades}); + $Res call({GradeAveragesGradingSystem gradingSystem, List grades}); } /// @nodoc @@ -51,14 +52,14 @@ class _$GradeAveragesStateCopyWithImpl<$Res, $Val extends GradeAveragesState> @pragma('vm:prefer-inline') @override $Res call({ - Object? gradingScheme = null, + Object? gradingSystem = null, Object? grades = null, }) { return _then(_value.copyWith( - gradingScheme: null == gradingScheme - ? _value.gradingScheme - : gradingScheme // ignore: cast_nullable_to_non_nullable - as GradingSchemes, + gradingSystem: null == gradingSystem + ? _value.gradingSystem + : gradingSystem // ignore: cast_nullable_to_non_nullable + as GradeAveragesGradingSystem, grades: null == grades ? _value.grades : grades // ignore: cast_nullable_to_non_nullable @@ -75,7 +76,7 @@ abstract class _$$GradeAveragesStateImplCopyWith<$Res> __$$GradeAveragesStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({GradingSchemes gradingScheme, List grades}); + $Res call({GradeAveragesGradingSystem gradingSystem, List grades}); } /// @nodoc @@ -89,14 +90,14 @@ class __$$GradeAveragesStateImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? gradingScheme = null, + Object? gradingSystem = null, Object? grades = null, }) { return _then(_$GradeAveragesStateImpl( - gradingScheme: null == gradingScheme - ? _value.gradingScheme - : gradingScheme // ignore: cast_nullable_to_non_nullable - as GradingSchemes, + gradingSystem: null == gradingSystem + ? _value.gradingSystem + : gradingSystem // ignore: cast_nullable_to_non_nullable + as GradeAveragesGradingSystem, grades: null == grades ? _value._grades : grades // ignore: cast_nullable_to_non_nullable @@ -109,14 +110,14 @@ class __$$GradeAveragesStateImplCopyWithImpl<$Res> @JsonSerializable() class _$GradeAveragesStateImpl implements _GradeAveragesState { const _$GradeAveragesStateImpl( - {required this.gradingScheme, required final List grades}) + {required this.gradingSystem, required final List grades}) : _grades = grades; factory _$GradeAveragesStateImpl.fromJson(Map json) => _$$GradeAveragesStateImplFromJson(json); @override - final GradingSchemes gradingScheme; + final GradeAveragesGradingSystem gradingSystem; final List _grades; @override List get grades { @@ -127,7 +128,7 @@ class _$GradeAveragesStateImpl implements _GradeAveragesState { @override String toString() { - return 'GradeAveragesState(gradingScheme: $gradingScheme, grades: $grades)'; + return 'GradeAveragesState(gradingSystem: $gradingSystem, grades: $grades)'; } @override @@ -135,15 +136,15 @@ class _$GradeAveragesStateImpl implements _GradeAveragesState { return identical(this, other) || (other.runtimeType == runtimeType && other is _$GradeAveragesStateImpl && - (identical(other.gradingScheme, gradingScheme) || - other.gradingScheme == gradingScheme) && + (identical(other.gradingSystem, gradingSystem) || + other.gradingSystem == gradingSystem) && const DeepCollectionEquality().equals(other._grades, _grades)); } @JsonKey(ignore: true) @override int get hashCode => Object.hash( - runtimeType, gradingScheme, const DeepCollectionEquality().hash(_grades)); + runtimeType, gradingSystem, const DeepCollectionEquality().hash(_grades)); @JsonKey(ignore: true) @override @@ -162,14 +163,14 @@ class _$GradeAveragesStateImpl implements _GradeAveragesState { abstract class _GradeAveragesState implements GradeAveragesState { const factory _GradeAveragesState( - {required final GradingSchemes gradingScheme, + {required final GradeAveragesGradingSystem gradingSystem, required final List grades}) = _$GradeAveragesStateImpl; factory _GradeAveragesState.fromJson(Map json) = _$GradeAveragesStateImpl.fromJson; @override - GradingSchemes get gradingScheme; + GradeAveragesGradingSystem get gradingSystem; @override List get grades; @override diff --git a/lib/state/app/application/gradeAverages/grade_averages_state.g.dart b/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.g.dart similarity index 64% rename from lib/state/app/application/gradeAverages/grade_averages_state.g.dart rename to lib/state/app/modules/gradeAverages/bloc/grade_averages_state.g.dart index 056d665..af7af59 100644 --- a/lib/state/app/application/gradeAverages/grade_averages_state.g.dart +++ b/lib/state/app/modules/gradeAverages/bloc/grade_averages_state.g.dart @@ -9,19 +9,20 @@ part of 'grade_averages_state.dart'; _$GradeAveragesStateImpl _$$GradeAveragesStateImplFromJson( Map json) => _$GradeAveragesStateImpl( - gradingScheme: - $enumDecode(_$GradingSchemesEnumMap, json['gradingScheme']), + gradingSystem: $enumDecode( + _$GradeAveragesGradingSystemEnumMap, json['gradingSystem']), grades: (json['grades'] as List).map((e) => e as int).toList(), ); Map _$$GradeAveragesStateImplToJson( _$GradeAveragesStateImpl instance) => { - 'gradingScheme': _$GradingSchemesEnumMap[instance.gradingScheme]!, + 'gradingSystem': + _$GradeAveragesGradingSystemEnumMap[instance.gradingSystem]!, 'grades': instance.grades, }; -const _$GradingSchemesEnumMap = { - GradingSchemes.middleSchool: 'middleSchool', - GradingSchemes.highSchool: 'highSchool', +const _$GradeAveragesGradingSystemEnumMap = { + GradeAveragesGradingSystem.highSchool: 'highSchool', + GradeAveragesGradingSystem.middleSchool: 'middleSchool', }; diff --git a/lib/state/app/modules/gradeAverages/screens/grade_averages_list_view.dart b/lib/state/app/modules/gradeAverages/screens/grade_averages_list_view.dart new file mode 100644 index 0000000..3366a9c --- /dev/null +++ b/lib/state/app/modules/gradeAverages/screens/grade_averages_list_view.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../bloc/grade_averages_bloc.dart'; +import '../bloc/grade_averages_event.dart'; + +class GradeAveragesListView extends StatelessWidget { + const GradeAveragesListView({super.key}); + + @override + Widget build(BuildContext context) { + var bloc = context.watch(); + + String getGradeDisplay(int grade) { + if(bloc.isMiddleSchool()) { + return 'Note $grade'; + } else { + return "$grade Punkt${grade > 1 ? "e" : ""}"; + } + } + + return ListView.builder( + itemCount: bloc.gradesInGradingSystem(), + itemBuilder: (context, index) { + var grade = bloc.getGradeFromIndex(index); + return Material( + child: ListTile( + tileColor: grade.isEven ? Colors.transparent : Colors.transparent.withAlpha(50), + title: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(getGradeDisplay(grade)), + const SizedBox(width: 30), + IconButton( + onPressed: () { + bloc.add(DecrementGrade(grade)); + }, + icon: const Icon(Icons.remove), + color: Theme.of(context).colorScheme.onSurface, + ), + Text('${bloc.countOfGrade(grade)}', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)), + IconButton( + onPressed: () { + bloc.add(IncrementGrade(grade)); + }, + icon: const Icon(Icons.add), + color: Theme.of(context).colorScheme.onSurface, + ), + ], + ), + ), + trailing: Visibility( + maintainState: true, + maintainAnimation: true, + maintainSize: true, + visible: bloc.canDecrementOrDelete(grade), + child: IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + bloc.add(ResetGrade(grade)); + }, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/state/app/modules/gradeAverages/screens/grade_averages_view.dart b/lib/state/app/modules/gradeAverages/screens/grade_averages_view.dart new file mode 100644 index 0000000..ccda360 --- /dev/null +++ b/lib/state/app/modules/gradeAverages/screens/grade_averages_view.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../../widget/confirmDialog.dart'; +import '../bloc/grade_averages_bloc.dart'; +import '../bloc/grade_averages_event.dart'; +import '../bloc/grade_averages_state.dart'; +import 'grade_averages_list_view.dart'; + +class GradeAveragesScreen extends StatelessWidget { + const GradeAveragesScreen({super.key}); + + @override + Widget build(BuildContext context) => BlocProvider( + create: (context) => GradeAveragesBloc(), + child: BlocBuilder( + builder: (context, state) { + var bloc = context.watch(); + + return Scaffold( + appBar: AppBar( + title: const Text('Notendurschnittsrechner'), + actions: [ + Visibility( + visible: bloc.state.grades.isNotEmpty, + child: IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + title: 'Zurücksetzen?', + content: 'Alle Einträge werden entfernt.', + confirmButton: 'Zurücksetzen', + onConfirm: () { + bloc.add(ResetAll()); + }, + ), + ); + }, + icon: const Icon(Icons.delete_forever)), + ), + PopupMenuButton( + initialValue: bloc.isMiddleSchool(), + icon: const Icon(Icons.more_horiz), + itemBuilder: (context) => [true, false].map((isMiddleSchool) => PopupMenuItem( + value: isMiddleSchool, + child: Row( + children: [ + Icon( + isMiddleSchool ? Icons.calculate_outlined : Icons.school_outlined, + color: Theme.of(context).colorScheme.onSurface + ), + const SizedBox(width: 15), + Text(isMiddleSchool ? 'Notensystem' : 'Punktesystem'), + ], + ), + )).toList(), + onSelected: (isMiddleSchool) { + if (bloc.state.grades.isNotEmpty) { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + title: 'Notensystem wechseln', + content: + 'Beim wechsel des Notensystems werden alle Einträge zurückgesetzt.', + confirmButton: 'Fortfahren', + onConfirm: () => bloc.add(GradingSystemChanged(isMiddleSchool)), + ), + ); + } else { + bloc.add(GradingSystemChanged(isMiddleSchool)); + } + }, + ), + ], + ), + + body: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 30), + Text(bloc.average().toStringAsFixed(2), style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 10), + Text(bloc.isMiddleSchool() ? 'Wähle unten die Anzahl deiner jewiligen Noten aus' : 'Wähle unten die Anzahl deiner jeweiligen Punkte aus'), + const SizedBox(height: 10), + const Expanded( + child: GradeAveragesListView() + ), + ], + ), + ); + }, + ), + ); +} diff --git a/lib/state/infrastructure/controller.dart b/lib/state/infrastructure/controller.dart deleted file mode 100644 index 2ceca86..0000000 --- a/lib/state/infrastructure/controller.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; - -abstract class Controller extends Cubit { - Controller(super.initialState); -} diff --git a/lib/state/infrastructure/loadable_state.dart b/lib/state/infrastructure/loadable_state.dart deleted file mode 100644 index 65299c6..0000000 --- a/lib/state/infrastructure/loadable_state.dart +++ /dev/null @@ -1,37 +0,0 @@ - -class LoadableState { - final LoadingState loadingState; - final TState? data; - - const LoadableState({required this.loadingState, required this.data}); - - LoadableState loading() => - LoadableState(loadingState: LoadingState.loading, data: null); - - LoadableState cached(TState state) => - LoadableState(loadingState: LoadingState.loading, data: state); - - LoadableState done(TState state) => - LoadableState(loadingState: LoadingState.none, data: state); - - LoadableState error({TState? state}) => - LoadableState(loadingState: LoadingState.failed, data: state); - - bool isBackgroundLoading() => - loadingState == LoadingState.loading && data != null; - - bool hasError() => - loadingState == LoadingState.failed; - - bool hasStateData() => - data != null; - - bool errorBarVisible() => - hasError() && hasStateData(); -} - -enum LoadingState { - loading, - failed, - none, -} diff --git a/lib/state/infrastructure/state_extensions.dart b/lib/state/infrastructure/state_extensions.dart deleted file mode 100644 index 1a828e1..0000000 --- a/lib/state/infrastructure/state_extensions.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -extension StateExtensions on BuildContext { - TState readController() => read(); - TState watchController() => watch(); -} diff --git a/lib/state/widgets/components/error_bar.dart b/lib/state/widgets/components/error_bar.dart index af63231..0d1c8bf 100644 --- a/lib/state/widgets/components/error_bar.dart +++ b/lib/state/widgets/components/error_bar.dart @@ -1,58 +1,58 @@ -import 'package:flutter/material.dart'; - -import '../../app/base/infrastructure/errorBar/error_bar_controller.dart'; -import '../../infrastructure/state_extensions.dart'; -import '../controller_provider.dart'; - -class ErrorBar extends StatelessWidget { - final bool visible; - const ErrorBar({required this.visible, super.key}); - - final Duration animationDuration = const Duration(milliseconds: 200); - - @override - Widget build(BuildContext context) => ControllerProvider( - create: (context) => ErrorBarController(), - child: (context) => AnimatedSize( - duration: animationDuration, - child: AnimatedSwitcher( - duration: animationDuration, - transitionBuilder: (Widget child, Animation animation) => SlideTransition( - position: Tween( - begin: const Offset(0.0, -1.0), - end: Offset.zero, - ).animate(animation), - child: child, - ), - child: Visibility( - key: Key(visible.hashCode.toString()), - visible: visible, - replacement: const SizedBox(width: double.infinity), - child: Builder( - builder: (context) { - var controller = context.watchController(); - 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); - - 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)) - ], - ), - ); - }, - ) - ) - ), - ), - ); -} +// import 'package:flutter/material.dart'; +// +// import '../../app/base/infrastructure/errorBar/error_bar_controller.dart'; +// import '../../infrastructure/state_extensions.dart'; +// import '../controller_provider.dart'; +// +// class ErrorBar extends StatelessWidget { +// final bool visible; +// const ErrorBar({required this.visible, super.key}); +// +// final Duration animationDuration = const Duration(milliseconds: 200); +// +// @override +// Widget build(BuildContext context) => ControllerProvider( +// create: (context) => ErrorBarController(), +// child: (context) => AnimatedSize( +// duration: animationDuration, +// child: AnimatedSwitcher( +// duration: animationDuration, +// transitionBuilder: (Widget child, Animation animation) => SlideTransition( +// position: Tween( +// begin: const Offset(0.0, -1.0), +// end: Offset.zero, +// ).animate(animation), +// child: child, +// ), +// child: Visibility( +// key: Key(visible.hashCode.toString()), +// visible: visible, +// replacement: const SizedBox(width: double.infinity), +// child: Builder( +// builder: (context) { +// var controller = context.watchController(); +// 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); +// +// return Container( +// height: 20, +// decoration: BoxDecoration( +// color: status.color, +// ), +// child: Row( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// Icon(status.icon, size: 14), +// const SizedBox(width: 10), +// Text(status.text, style: const TextStyle(fontSize: 12)) +// ], +// ), +// ); +// }, +// ) +// ) +// ), +// ), +// ); +// } diff --git a/lib/state/widgets/controller_consumer.dart b/lib/state/widgets/controller_consumer.dart index 70b5302..c814d94 100644 --- a/lib/state/widgets/controller_consumer.dart +++ b/lib/state/widgets/controller_consumer.dart @@ -1,12 +1,12 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../infrastructure/controller.dart'; - -class ControllerConsumer, TState> extends StatelessWidget { - final Widget Function(BuildContext context, TState state) child; - const ControllerConsumer({required this.child, super.key}); - - @override - Widget build(BuildContext context) => BlocBuilder(builder: child); -} +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// +// import '../infrastructure/controller.dart'; +// +// class ControllerConsumer, TState> extends StatelessWidget { +// final Widget Function(BuildContext context, TState state) child; +// const ControllerConsumer({required this.child, super.key}); +// +// @override +// Widget build(BuildContext context) => BlocBuilder(builder: child); +// } diff --git a/lib/state/widgets/controller_provider.dart b/lib/state/widgets/controller_provider.dart index af9d9d7..e232ac3 100644 --- a/lib/state/widgets/controller_provider.dart +++ b/lib/state/widgets/controller_provider.dart @@ -1,21 +1,21 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/single_child_widget.dart'; - -import '../infrastructure/controller.dart'; - - -class ControllerProvider extends SingleChildStatelessWidget { - final TState Function(BuildContext context) create; - final bool lazy; - final Widget Function(BuildContext context) child; - ControllerProvider({required this.create, this.lazy = true, required this.child, super.key}) - : super(child: Builder(builder: child)); - - @override - Widget buildWithChild(BuildContext context, Widget? child) => BlocProvider( - create: create, - lazy: lazy, - child: child, - ); -} +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:provider/single_child_widget.dart'; +// +// import '../infrastructure/controller.dart'; +// +// +// class ControllerProvider extends SingleChildStatelessWidget { +// final TState Function(BuildContext context) create; +// final bool lazy; +// final Widget Function(BuildContext context) child; +// ControllerProvider({required this.create, this.lazy = true, required this.child, super.key}) +// : super(child: Builder(builder: child)); +// +// @override +// Widget buildWithChild(BuildContext context, Widget? child) => BlocProvider( +// create: create, +// lazy: lazy, +// child: child, +// ); +// } diff --git a/lib/state/widgets/controllers_provider.dart b/lib/state/widgets/controllers_provider.dart index 0474581..1b5417b 100644 --- a/lib/state/widgets/controllers_provider.dart +++ b/lib/state/widgets/controllers_provider.dart @@ -1,17 +1,17 @@ - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'controller_provider.dart'; - -class ControllersProvider extends StatelessWidget { - final List controllers; - final Widget Function(BuildContext context) child; - const ControllersProvider({required this.controllers, required this.child, super.key}); - - @override - Widget build(BuildContext context) => MultiBlocProvider( - providers: controllers, - child: Builder(builder: child) - ); -} +// +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// +// import 'controller_provider.dart'; +// +// class ControllersProvider extends StatelessWidget { +// final List controllers; +// final Widget Function(BuildContext context) child; +// const ControllersProvider({required this.controllers, required this.child, super.key}); +// +// @override +// Widget build(BuildContext context) => MultiBlocProvider( +// providers: controllers, +// child: Builder(builder: child) +// ); +// } diff --git a/lib/state/widgets/loadable_controller_consumer.dart b/lib/state/widgets/loadable_controller_consumer.dart index 8fa0d33..5fa8b48 100644 --- a/lib/state/widgets/loadable_controller_consumer.dart +++ b/lib/state/widgets/loadable_controller_consumer.dart @@ -1,40 +1,42 @@ -import 'package:flutter/material.dart'; - -import '../infrastructure/controller.dart'; -import '../infrastructure/loadable_state.dart'; -import '../infrastructure/state_extensions.dart'; -import 'components/background_loading_indicator.dart'; -import 'components/error_bar.dart'; -import 'components/primary_loading_indicator.dart'; - -class LoadableControllerConsumer, TState extends LoadableState> extends StatelessWidget { - final Widget child; - const LoadableControllerConsumer({required this.child, super.key}); - - final Duration animationDuration = const Duration(milliseconds: 200); - - @override - Widget build(BuildContext context) { - var state = context.readController().state; - return Column( - children: [ - ErrorBar(visible: state.errorBarVisible()), - Expanded( - child: Stack( - children: [ - PrimaryLoadingIndicator(visible: !state.hasStateData()), - BackgroundLoadingIndicator(visible: state.isBackgroundLoading() && !state.errorBarVisible()), - - AnimatedOpacity( - opacity: state.hasStateData() ? 1.0 : 0.0, - duration: animationDuration, - curve: Curves.easeInOut, - child: state.hasStateData() ? child : const SizedBox.shrink() - ), - ], - ), - ) - ], - ); - } -} +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// +// import '../infrastructure/controller.dart'; +// import '../infrastructure/loadable_state.dart'; +// import '../infrastructure/state_extensions.dart'; +// import 'components/background_loading_indicator.dart'; +// import 'components/error_bar.dart'; +// import 'components/primary_loading_indicator.dart'; +// +// class LoadableControllerConsumer, TState extends LoadableState> extends StatelessWidget { +// final Widget child; +// const LoadableControllerConsumer({required this.child, super.key}); +// +// final Duration animationDuration = const Duration(milliseconds: 200); +// +// @override +// Widget build(BuildContext context) { +// var state = context.readController().state; +// +// return Column( +// children: [ +// // ErrorBar(visible: state.errorBarVisible()), +// // Expanded( +// // child: Stack( +// // children: [ +// // PrimaryLoadingIndicator(visible: !state.hasStateData()), +// // BackgroundLoadingIndicator(visible: state.isBackgroundLoading() && !state.errorBarVisible()), +// // +// // AnimatedOpacity( +// // opacity: state.hasStateData() ? 1.0 : 0.0, +// // duration: animationDuration, +// // curve: Curves.easeInOut, +// // child: state.hasStateData() ? child : const SizedBox.shrink() +// // ), +// // ], +// // ), +// // ) +// ], +// ); +// } +// } diff --git a/lib/view/pages/files/fileElement.dart b/lib/view/pages/files/fileElement.dart index 478da24..96aa306 100644 --- a/lib/view/pages/files/fileElement.dart +++ b/lib/view/pages/files/fileElement.dart @@ -99,7 +99,7 @@ class _FileElementState extends State { onTap: () { if(widget.file.isDirectory) { Navigator.of(context).push(MaterialPageRoute( - builder: (context) => Files(widget.path.toList()..add(widget.file.name)), + builder: (context) => Files(path: widget.path.toList()..add(widget.file.name)), )); } else { if(EndpointData().getEndpointMode() == EndpointMode.stage) { diff --git a/lib/view/pages/files/files.dart b/lib/view/pages/files/files.dart index 8068408..86ab4f4 100644 --- a/lib/view/pages/files/files.dart +++ b/lib/view/pages/files/files.dart @@ -21,7 +21,7 @@ import 'filesUploadDialog.dart'; class Files extends StatefulWidget { final List path; - const Files(this.path, {super.key}); + Files({List? path, super.key}) : path = path ?? []; @override State createState() => _FilesState(); diff --git a/lib/view/pages/more/test.dart b/lib/view/pages/more/test.dart index e6c69ad..36ec438 100644 --- a/lib/view/pages/more/test.dart +++ b/lib/view/pages/more/test.dart @@ -1,48 +1,48 @@ -import 'package:flutter/material.dart'; -import '../../../state/app/application/marianumMessage/marianum_message_controller.dart'; -import '../../../state/app/application/marianumMessage/marianum_message_state.dart'; -import '../../../state/infrastructure/loadable_state.dart'; -import '../../../state/infrastructure/state_extensions.dart'; -import '../../../state/widgets/controller_consumer.dart'; -import '../../../state/widgets/loadable_controller_consumer.dart'; -import '../../../state/widgets/sub_selected_controller_consumer.dart'; -import '../../../state/widgets/controller_provider.dart'; - -class Test extends StatelessWidget { - const Test({super.key}); - - @override - Widget build(BuildContext context) => ControllerProvider( - create: (context) => MarianumMessageController(), - child: (context) => Scaffold( - appBar: AppBar(title: const Text('TEST')), - body: LoadableControllerConsumer>( - child: Column( - children: [ - TextButton( - onPressed: () => context.readController().loading(), - child: Text(context.watchController().state.loadingState.toString()) - ), - TextButton( - onPressed: () => context.readController().backgroundLoading(), - child: Text(context.watchController().state.loadingState.toString()) - ), - TextButton( - onPressed: () => context.readController().done(), - child: Text(context.watchController().state.loadingState.toString()) - ), - TextButton( - onPressed: () => context.readController().error(), - child: Text(context.watchController().state.loadingState.toString()) - ), - ControllerConsumer>(child: (context, state) => Text(state.data!.test.toString())), - SubSelectedControllerConsumer, LoadingState>( - subSelect: (state) => state.loadingState, - child: (context, state) => Text(state.toString()), - ) - ], - ), - ), - ), - ); -} +// import 'package:flutter/material.dart'; +// import '../../../state/app/application/marianumMessage/marianum_message_controller.dart'; +// import '../../../state/app/application/marianumMessage/marianum_message_state.dart'; +// import '../../../state/infrastructure/loadable_state.dart'; +// import '../../../state/infrastructure/state_extensions.dart'; +// import '../../../state/widgets/controller_consumer.dart'; +// import '../../../state/widgets/loadable_controller_consumer.dart'; +// import '../../../state/widgets/sub_selected_controller_consumer.dart'; +// import '../../../state/widgets/controller_provider.dart'; +// +// class Test extends StatelessWidget { +// const Test({super.key}); +// +// @override +// Widget build(BuildContext context) => ControllerProvider( +// create: (context) => MarianumMessageController(), +// child: (context) => Scaffold( +// appBar: AppBar(title: const Text('TEST')), +// body: ControllerConsumer( +// child: (context, state) => Column( +// children: [ +// TextButton( +// onPressed: () => context.readController().someaction(), +// child: Text(context.watchController().state.toString()) +// ), +// TextButton( +// onPressed: () => context.readController().backgroundLoading(), +// child: Text(context.watchController().state.toString()) +// ), +// TextButton( +// onPressed: () => context.readController().done(), +// child: Text(context.watchController().state.toString()) +// ), +// TextButton( +// onPressed: () => context.readController().error(), +// child: Text(context.watchController().state.toString()) +// ), +// ControllerConsumer(child: (context, state) => Text(state.base.toString())), +// SubSelectedControllerConsumer( +// subSelect: (state) => state.messages, +// child: (context, state) => Text(state.toString()), +// ) +// ], +// ), +// ), +// ), +// ); +// } diff --git a/lib/view/pages/overhang.dart b/lib/view/pages/overhang.dart index 79f59bf..184953b 100644 --- a/lib/view/pages/overhang.dart +++ b/lib/view/pages/overhang.dart @@ -6,6 +6,7 @@ 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/screens/grade_averages_view.dart'; import '../../widget/ListItem.dart'; import '../../widget/centeredLeading.dart'; import '../../widget/infoDialog.dart'; @@ -16,7 +17,6 @@ import 'more/holidays/holidays.dart'; import 'more/message/message.dart'; import 'more/roomplan/roomplan.dart'; import 'more/share/selectShareTypeDialog.dart'; -import 'more/test.dart'; class Overhang extends StatelessWidget { const Overhang({super.key}); @@ -77,7 +77,7 @@ class Overhang extends StatelessWidget { ), ListTile( leading: const Icon(Icons.science_outlined), - onTap: () => pushScreen(context, withNavBar: false, screen: const Test()), + onTap: () => pushScreen(context, withNavBar: false, screen: const GradeAveragesScreen()), ) ], ), diff --git a/pubspec.yaml b/pubspec.yaml index e9de63c..61599a4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -102,6 +102,7 @@ dependencies: flutter_bloc: ^8.1.5 freezed_annotation: ^2.4.1 connectivity_plus: ^6.0.3 + hydrated_bloc: ^9.1.5 dev_dependencies: flutter_test: From 6ad8203b6a1bdbddbffc042808c06e36fa66bd44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 5 May 2024 22:58:40 +0200 Subject: [PATCH 08/19] implemented new loadable state concept --- .../dataLoader/data_loader.dart | 4 + .../bloc/loadable_state_bloc.dart} | 10 +- .../bloc/loadable_state_state.dart} | 8 +- .../bloc/loadable_state_state.freezed.dart} | 63 +-- .../loadableState/loadable_hydrated_bloc.dart | 25 + .../loadableState/loadable_state.dart | 18 + .../loadableState/loadable_state.freezed.dart | 158 ++++++ .../loadable_state_background_loading.dart} | 4 +- .../view/loadable_state_consumer.dart | 42 ++ .../view/loadable_state_error_bar.dart | 59 ++ .../view/loadable_state_primary_loading.dart} | 4 +- .../infrastructure/repository/repository.dart | 13 + .../bloc_providing_builder.dart | 11 + .../grade_averages_list_view.dart | 0 .../grade_averages_view.dart | 4 +- .../bloc/marianum_message_bloc.dart | 17 + .../bloc/marianum_message_state.dart | 41 ++ .../bloc/marianum_message_state.freezed.dart | 507 ++++++++++++++++++ .../bloc/marianum_message_state.g.dart | 52 ++ .../marianum_message_get_messages.dart | 6 + .../marianum_message_repository.dart | 9 + .../view/marianum_message_list_view.dart | 49 ++ lib/state/widgets/components/error_bar.dart | 58 -- lib/state/widgets/controller_consumer.dart | 12 - lib/state/widgets/controller_provider.dart | 21 - lib/state/widgets/controllers_provider.dart | 17 - .../widgets/loadable_controller_consumer.dart | 42 -- .../sub_selected_controller_consumer.dart | 11 - lib/view/pages/more/test.dart | 2 +- lib/view/pages/overhang.dart | 11 +- lib/view/settings/devToolsSettingsDialog.dart | 21 + 31 files changed, 1085 insertions(+), 214 deletions(-) create mode 100644 lib/state/app/infrastructure/dataLoader/data_loader.dart rename lib/state/app/{base/infrastructure/errorBar/error_bar_controller.dart => infrastructure/loadableState/bloc/loadable_state_bloc.dart} (73%) rename lib/state/app/{base/infrastructure/errorBar/error_bar_state.dart => infrastructure/loadableState/bloc/loadable_state_state.dart} (53%) rename lib/state/app/{base/infrastructure/errorBar/error_bar_state.freezed.dart => infrastructure/loadableState/bloc/loadable_state_state.freezed.dart} (64%) create mode 100644 lib/state/app/infrastructure/loadableState/loadable_hydrated_bloc.dart create mode 100644 lib/state/app/infrastructure/loadableState/loadable_state.dart create mode 100644 lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart rename lib/state/{widgets/components/background_loading_indicator.dart => app/infrastructure/loadableState/view/loadable_state_background_loading.dart} (80%) create mode 100644 lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart create mode 100644 lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart rename lib/state/{widgets/components/primary_loading_indicator.dart => app/infrastructure/loadableState/view/loadable_state_primary_loading.dart} (74%) create mode 100644 lib/state/app/infrastructure/repository/repository.dart create mode 100644 lib/state/app/infrastructure/utilityWidgets/bloc_providing_builder.dart rename lib/state/app/modules/gradeAverages/{screens => view}/grade_averages_list_view.dart (100%) rename lib/state/app/modules/gradeAverages/{screens => view}/grade_averages_view.dart (97%) create mode 100644 lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart create mode 100644 lib/state/app/modules/marianumMessage/bloc/marianum_message_state.dart create mode 100644 lib/state/app/modules/marianumMessage/bloc/marianum_message_state.freezed.dart create mode 100644 lib/state/app/modules/marianumMessage/bloc/marianum_message_state.g.dart create mode 100644 lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart create mode 100644 lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart create mode 100644 lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart delete mode 100644 lib/state/widgets/components/error_bar.dart delete mode 100644 lib/state/widgets/controller_consumer.dart delete mode 100644 lib/state/widgets/controller_provider.dart delete mode 100644 lib/state/widgets/controllers_provider.dart delete mode 100644 lib/state/widgets/loadable_controller_consumer.dart delete mode 100644 lib/state/widgets/sub_selected_controller_consumer.dart diff --git a/lib/state/app/infrastructure/dataLoader/data_loader.dart b/lib/state/app/infrastructure/dataLoader/data_loader.dart new file mode 100644 index 0000000..6f84cb1 --- /dev/null +++ b/lib/state/app/infrastructure/dataLoader/data_loader.dart @@ -0,0 +1,4 @@ +abstract class DataLoader { + + Future fetch(); +} diff --git a/lib/state/app/base/infrastructure/errorBar/error_bar_controller.dart b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart similarity index 73% rename from lib/state/app/base/infrastructure/errorBar/error_bar_controller.dart rename to lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart index b5a7858..a2cb062 100644 --- a/lib/state/app/base/infrastructure/errorBar/error_bar_controller.dart +++ b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart @@ -1,16 +1,16 @@ import 'dart:async'; +import 'package:bloc/bloc.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; -import '../../../../infrastructure/controller.dart'; -import 'error_bar_state.dart'; +import 'loadable_state_state.dart'; -class ErrorBarController extends Controller { +class LoadableStateBloc extends Bloc { late StreamSubscription> _updateStream; - ErrorBarController() : super(const ErrorBarState(connections: null)) { + LoadableStateBloc() : super(const LoadableStateState(connections: null)) { emitState(List v) => emit(state.copyWith(connections: v)); - + Connectivity().checkConnectivity().then(emitState); _updateStream = Connectivity().onConnectivityChanged.listen(emitState); } diff --git a/lib/state/app/base/infrastructure/errorBar/error_bar_state.dart b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_state.dart similarity index 53% rename from lib/state/app/base/infrastructure/errorBar/error_bar_state.dart rename to lib/state/app/infrastructure/loadableState/bloc/loadable_state_state.dart index 59a8b22..712c9fd 100644 --- a/lib/state/app/base/infrastructure/errorBar/error_bar_state.dart +++ b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_state.dart @@ -1,11 +1,11 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -part 'error_bar_state.freezed.dart'; +part 'loadable_state_state.freezed.dart'; @freezed -class ErrorBarState with _$ErrorBarState { - const factory ErrorBarState({ +class LoadableStateState with _$LoadableStateState { + const factory LoadableStateState({ required List? connections, - }) = _ErrorBarState; + }) = _LoadableStateState; } diff --git a/lib/state/app/base/infrastructure/errorBar/error_bar_state.freezed.dart b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_state.freezed.dart similarity index 64% rename from lib/state/app/base/infrastructure/errorBar/error_bar_state.freezed.dart rename to lib/state/app/infrastructure/loadableState/bloc/loadable_state_state.freezed.dart index 2e60c16..66011f9 100644 --- a/lib/state/app/base/infrastructure/errorBar/error_bar_state.freezed.dart +++ b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_state.freezed.dart @@ -3,7 +3,7 @@ // 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 'error_bar_state.dart'; +part of 'loadable_state_state.dart'; // ************************************************************************** // FreezedGenerator @@ -15,28 +15,28 @@ 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 _$ErrorBarState { +mixin _$LoadableStateState { List? get connections => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $ErrorBarStateCopyWith get copyWith => + $LoadableStateStateCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class $ErrorBarStateCopyWith<$Res> { - factory $ErrorBarStateCopyWith( - ErrorBarState value, $Res Function(ErrorBarState) then) = - _$ErrorBarStateCopyWithImpl<$Res, ErrorBarState>; +abstract class $LoadableStateStateCopyWith<$Res> { + factory $LoadableStateStateCopyWith( + LoadableStateState value, $Res Function(LoadableStateState) then) = + _$LoadableStateStateCopyWithImpl<$Res, LoadableStateState>; @useResult $Res call({List? connections}); } /// @nodoc -class _$ErrorBarStateCopyWithImpl<$Res, $Val extends ErrorBarState> - implements $ErrorBarStateCopyWith<$Res> { - _$ErrorBarStateCopyWithImpl(this._value, this._then); +class _$LoadableStateStateCopyWithImpl<$Res, $Val extends LoadableStateState> + implements $LoadableStateStateCopyWith<$Res> { + _$LoadableStateStateCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; @@ -58,22 +58,22 @@ class _$ErrorBarStateCopyWithImpl<$Res, $Val extends ErrorBarState> } /// @nodoc -abstract class _$$ErrorBarStateImplCopyWith<$Res> - implements $ErrorBarStateCopyWith<$Res> { - factory _$$ErrorBarStateImplCopyWith( - _$ErrorBarStateImpl value, $Res Function(_$ErrorBarStateImpl) then) = - __$$ErrorBarStateImplCopyWithImpl<$Res>; +abstract class _$$LoadableStateStateImplCopyWith<$Res> + implements $LoadableStateStateCopyWith<$Res> { + factory _$$LoadableStateStateImplCopyWith(_$LoadableStateStateImpl value, + $Res Function(_$LoadableStateStateImpl) then) = + __$$LoadableStateStateImplCopyWithImpl<$Res>; @override @useResult $Res call({List? connections}); } /// @nodoc -class __$$ErrorBarStateImplCopyWithImpl<$Res> - extends _$ErrorBarStateCopyWithImpl<$Res, _$ErrorBarStateImpl> - implements _$$ErrorBarStateImplCopyWith<$Res> { - __$$ErrorBarStateImplCopyWithImpl( - _$ErrorBarStateImpl _value, $Res Function(_$ErrorBarStateImpl) _then) +class __$$LoadableStateStateImplCopyWithImpl<$Res> + extends _$LoadableStateStateCopyWithImpl<$Res, _$LoadableStateStateImpl> + implements _$$LoadableStateStateImplCopyWith<$Res> { + __$$LoadableStateStateImplCopyWithImpl(_$LoadableStateStateImpl _value, + $Res Function(_$LoadableStateStateImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @@ -81,7 +81,7 @@ class __$$ErrorBarStateImplCopyWithImpl<$Res> $Res call({ Object? connections = freezed, }) { - return _then(_$ErrorBarStateImpl( + return _then(_$LoadableStateStateImpl( connections: freezed == connections ? _value._connections : connections // ignore: cast_nullable_to_non_nullable @@ -92,8 +92,8 @@ class __$$ErrorBarStateImplCopyWithImpl<$Res> /// @nodoc -class _$ErrorBarStateImpl implements _ErrorBarState { - const _$ErrorBarStateImpl( +class _$LoadableStateStateImpl implements _LoadableStateState { + const _$LoadableStateStateImpl( {required final List? connections}) : _connections = connections; @@ -109,14 +109,14 @@ class _$ErrorBarStateImpl implements _ErrorBarState { @override String toString() { - return 'ErrorBarState(connections: $connections)'; + return 'LoadableStateState(connections: $connections)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$ErrorBarStateImpl && + other is _$LoadableStateStateImpl && const DeepCollectionEquality() .equals(other._connections, _connections)); } @@ -128,19 +128,20 @@ class _$ErrorBarStateImpl implements _ErrorBarState { @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$ErrorBarStateImplCopyWith<_$ErrorBarStateImpl> get copyWith => - __$$ErrorBarStateImplCopyWithImpl<_$ErrorBarStateImpl>(this, _$identity); + _$$LoadableStateStateImplCopyWith<_$LoadableStateStateImpl> get copyWith => + __$$LoadableStateStateImplCopyWithImpl<_$LoadableStateStateImpl>( + this, _$identity); } -abstract class _ErrorBarState implements ErrorBarState { - const factory _ErrorBarState( +abstract class _LoadableStateState implements LoadableStateState { + const factory _LoadableStateState( {required final List? connections}) = - _$ErrorBarStateImpl; + _$LoadableStateStateImpl; @override List? get connections; @override @JsonKey(ignore: true) - _$$ErrorBarStateImplCopyWith<_$ErrorBarStateImpl> get copyWith => + _$$LoadableStateStateImplCopyWith<_$LoadableStateStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/state/app/infrastructure/loadableState/loadable_hydrated_bloc.dart b/lib/state/app/infrastructure/loadableState/loadable_hydrated_bloc.dart new file mode 100644 index 0000000..5694249 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/loadable_hydrated_bloc.dart @@ -0,0 +1,25 @@ +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import '../repository/repository.dart'; +import 'loadable_state.dart'; + +sealed class LoadableHydratedBlocEvent {} +class DataRecieved extends LoadableHydratedBlocEvent {} + +abstract class LoadableHydratedBloc extends HydratedBloc> { + LoadableHydratedBloc() : super(const LoadableState()) { + repository().load(); + + } + + Repository repository(); + + + @override + fromJson(Map json) => LoadableState(isLoading: true, data: fromStorage(json)); + @override + Map? toJson(state) => state.data.toJson(); + + TState fromStorage(Map json); + Map? toStorage(TState state); +} diff --git a/lib/state/app/infrastructure/loadableState/loadable_state.dart b/lib/state/app/infrastructure/loadableState/loadable_state.dart new file mode 100644 index 0000000..6c25efc --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/loadable_state.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'loadable_state.freezed.dart'; + +@freezed +class LoadableState with _$LoadableState { + const LoadableState._(); + + const factory LoadableState({ + @Default(true) bool isLoading, + @Default(null) TState? data, + }) = _LoadableState; + + bool showPrimaryLoading() => isLoading && data == null; + bool showBackgroundLoading() => isLoading && data != null; + bool showError() => !isLoading && data == null; + bool showContent() => data != null; +} diff --git a/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart b/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart new file mode 100644 index 0000000..3bfb969 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart @@ -0,0 +1,158 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'loadable_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$LoadableState { + bool get isLoading => throw _privateConstructorUsedError; + TState? get data => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $LoadableStateCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoadableStateCopyWith { + factory $LoadableStateCopyWith(LoadableState value, + $Res Function(LoadableState) then) = + _$LoadableStateCopyWithImpl>; + @useResult + $Res call({bool isLoading, TState? data}); +} + +/// @nodoc +class _$LoadableStateCopyWithImpl> + implements $LoadableStateCopyWith { + _$LoadableStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isLoading = null, + Object? data = freezed, + }) { + return _then(_value.copyWith( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as TState?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LoadableStateImplCopyWith + implements $LoadableStateCopyWith { + factory _$$LoadableStateImplCopyWith(_$LoadableStateImpl value, + $Res Function(_$LoadableStateImpl) then) = + __$$LoadableStateImplCopyWithImpl; + @override + @useResult + $Res call({bool isLoading, TState? data}); +} + +/// @nodoc +class __$$LoadableStateImplCopyWithImpl + extends _$LoadableStateCopyWithImpl> + implements _$$LoadableStateImplCopyWith { + __$$LoadableStateImplCopyWithImpl(_$LoadableStateImpl _value, + $Res Function(_$LoadableStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isLoading = null, + Object? data = freezed, + }) { + return _then(_$LoadableStateImpl( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as TState?, + )); + } +} + +/// @nodoc + +class _$LoadableStateImpl extends _LoadableState { + const _$LoadableStateImpl({this.isLoading = true, this.data = null}) + : super._(); + + @override + @JsonKey() + final bool isLoading; + @override + @JsonKey() + final TState? data; + + @override + String toString() { + return 'LoadableState<$TState>(isLoading: $isLoading, data: $data)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoadableStateImpl && + (identical(other.isLoading, isLoading) || + other.isLoading == isLoading) && + const DeepCollectionEquality().equals(other.data, data)); + } + + @override + int get hashCode => Object.hash( + runtimeType, isLoading, const DeepCollectionEquality().hash(data)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LoadableStateImplCopyWith> + get copyWith => __$$LoadableStateImplCopyWithImpl>(this, _$identity); +} + +abstract class _LoadableState extends LoadableState { + const factory _LoadableState({final bool isLoading, final TState? data}) = + _$LoadableStateImpl; + const _LoadableState._() : super._(); + + @override + bool get isLoading; + @override + TState? get data; + @override + @JsonKey(ignore: true) + _$$LoadableStateImplCopyWith> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/state/widgets/components/background_loading_indicator.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_background_loading.dart similarity index 80% rename from lib/state/widgets/components/background_loading_indicator.dart rename to lib/state/app/infrastructure/loadableState/view/loadable_state_background_loading.dart index a7c4048..1c10bb0 100644 --- a/lib/state/widgets/components/background_loading_indicator.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_background_loading.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -class BackgroundLoadingIndicator extends StatelessWidget { +class LoadableStateBackgroundLoading extends StatelessWidget { final bool visible; - const BackgroundLoadingIndicator({required this.visible, super.key}); + const LoadableStateBackgroundLoading({required this.visible, super.key}); final Duration animationDuration = const Duration(milliseconds: 200); diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart new file mode 100644 index 0000000..c3d3c75 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart @@ -0,0 +1,42 @@ +import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../loadable_state.dart'; +import 'loadable_state_background_loading.dart'; +import 'loadable_state_error_bar.dart'; +import 'loadable_state_primary_loading.dart'; + +// TODO might be a simpler way +class LoadableStateConsumer, TWrappedState extends LoadableState, TState> extends StatelessWidget { + final Widget Function(TState state) child; + const LoadableStateConsumer({required this.child, super.key}); + + static Duration animationDuration = const Duration(milliseconds: 200); + + @override + Widget build(BuildContext context) { + var state = context.read().state as LoadableState; + + return Column( + children: [ + LoadableStateErrorBar(visible: state.showError()), + Expanded( + child: Stack( + children: [ + LoadableStatePrimaryLoading(visible: state.showPrimaryLoading()), + LoadableStateBackgroundLoading(visible: state.showBackgroundLoading()), + + AnimatedOpacity( + opacity: state.showContent() ? 1.0 : 0.0, + duration: animationDuration, + curve: Curves.easeInOut, + child: state.showContent() ? child(state.data) : const SizedBox.shrink() + ), + ], + ), + ) + ], + ); + } +} diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart new file mode 100644 index 0000000..4e21c72 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../utilityWidgets/bloc_providing_builder.dart'; +import '../bloc/loadable_state_bloc.dart'; +import '../bloc/loadable_state_state.dart'; + +class LoadableStateErrorBar extends StatelessWidget { + final bool visible; + const LoadableStateErrorBar({required this.visible, super.key}); + + final Duration animationDuration = const Duration(milliseconds: 200); + + @override + Widget build(BuildContext context) => BlocProvidingBuilder( + create: (context) => LoadableStateBloc(), + child: (context, state) => AnimatedSize( + duration: animationDuration, + child: AnimatedSwitcher( + duration: animationDuration, + transitionBuilder: (Widget child, Animation animation) => SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: Visibility( + key: Key(visible.hashCode.toString()), + visible: visible, + replacement: const SizedBox(width: double.infinity), + child: Builder( + builder: (context) { + var controller = context.watch(); + var status = controller.connectivityStatusKnown() && !controller.isConnected() + ? (icon: Icons.wifi_off_outlined, text: 'Offline', color: Colors.grey.shade600) + : (icon: Icons.wifi_find_outlined, text: 'Verbindung fehlgeschlagen', color: Theme.of(context).primaryColor); + + return Container( + height: 20, + decoration: BoxDecoration( + color: status.color, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(status.icon, size: 14), + const SizedBox(width: 10), + Text(status.text, style: const TextStyle(fontSize: 12)) + ], + ), + ); + }, + ) + ) + ), + ), + ); +} diff --git a/lib/state/widgets/components/primary_loading_indicator.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_primary_loading.dart similarity index 74% rename from lib/state/widgets/components/primary_loading_indicator.dart rename to lib/state/app/infrastructure/loadableState/view/loadable_state_primary_loading.dart index 4f21fc1..a01ba28 100644 --- a/lib/state/widgets/components/primary_loading_indicator.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_primary_loading.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -class PrimaryLoadingIndicator extends StatelessWidget { +class LoadableStatePrimaryLoading extends StatelessWidget { final bool visible; - const PrimaryLoadingIndicator({required this.visible, super.key}); + const LoadableStatePrimaryLoading({required this.visible, super.key}); final Duration animationDuration = const Duration(milliseconds: 200); diff --git a/lib/state/app/infrastructure/repository/repository.dart b/lib/state/app/infrastructure/repository/repository.dart new file mode 100644 index 0000000..4a6caae --- /dev/null +++ b/lib/state/app/infrastructure/repository/repository.dart @@ -0,0 +1,13 @@ +import '../dataLoader/data_loader.dart'; + +abstract class Repository { + final List dataLoaders; + + Repository(this.dataLoaders); + + Future load() async { + dataLoaders.forEach((element) { + element.fetch(); + }); + } +} diff --git a/lib/state/app/infrastructure/utilityWidgets/bloc_providing_builder.dart b/lib/state/app/infrastructure/utilityWidgets/bloc_providing_builder.dart new file mode 100644 index 0000000..4c98579 --- /dev/null +++ b/lib/state/app/infrastructure/utilityWidgets/bloc_providing_builder.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class BlocProvidingBuilder, TState> extends StatelessWidget { + final TBloc Function(BuildContext context) create; + final Widget Function(BuildContext context, TState state) child; + const BlocProvidingBuilder({required this.create, required this.child, super.key}); + + @override + Widget build(BuildContext context) => BlocProvider(create: create, child: BlocBuilder(builder: child)); +} diff --git a/lib/state/app/modules/gradeAverages/screens/grade_averages_list_view.dart b/lib/state/app/modules/gradeAverages/view/grade_averages_list_view.dart similarity index 100% rename from lib/state/app/modules/gradeAverages/screens/grade_averages_list_view.dart rename to lib/state/app/modules/gradeAverages/view/grade_averages_list_view.dart diff --git a/lib/state/app/modules/gradeAverages/screens/grade_averages_view.dart b/lib/state/app/modules/gradeAverages/view/grade_averages_view.dart similarity index 97% rename from lib/state/app/modules/gradeAverages/screens/grade_averages_view.dart rename to lib/state/app/modules/gradeAverages/view/grade_averages_view.dart index ccda360..c0734ef 100644 --- a/lib/state/app/modules/gradeAverages/screens/grade_averages_view.dart +++ b/lib/state/app/modules/gradeAverages/view/grade_averages_view.dart @@ -7,8 +7,8 @@ import '../bloc/grade_averages_event.dart'; import '../bloc/grade_averages_state.dart'; import 'grade_averages_list_view.dart'; -class GradeAveragesScreen extends StatelessWidget { - const GradeAveragesScreen({super.key}); +class GradeAveragesView extends StatelessWidget { + const GradeAveragesView({super.key}); @override Widget build(BuildContext context) => BlocProvider( diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart new file mode 100644 index 0000000..0563ae3 --- /dev/null +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart @@ -0,0 +1,17 @@ +import '../../../infrastructure/loadableState/loadable_hydrated_bloc.dart'; +import '../../../infrastructure/repository/repository.dart'; +import '../repository/marianum_message_repository.dart'; +import 'marianum_message_state.dart'; + +class MarianumMessageBloc extends LoadableHydratedBloc { + MarianumMessageBloc(); + + + @override + MarianumMessageState fromStorage(Map json) => MarianumMessageState.fromJson(json); + @override + Map? toStorage(MarianumMessageState state) => state.toJson(); + + @override + Repository repository() => MarianumMessageRepository(emit); +} diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.dart new file mode 100644 index 0000000..4f2d7c9 --- /dev/null +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.dart @@ -0,0 +1,41 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'marianum_message_state.freezed.dart'; +part 'marianum_message_state.g.dart'; + + +@freezed +class MarianumMessageState with _$MarianumMessageState { + const factory MarianumMessageState({ + required MarianumMessageList messageList, + }) = _MarianumMessageState; + + factory MarianumMessageState.fromJson(Map json) => _$MarianumMessageStateFromJson(json); +} + +@freezed +class MarianumMessageList with _$MarianumMessageList { + const factory MarianumMessageList({ + required String base, + required List messages, + }) = _MarianumMessageList; + + factory MarianumMessageList.fromJson(Map json) => _$MarianumMessageListFromJson(json); +} + +@freezed +class MarianumMessage with _$MarianumMessage { + const factory MarianumMessage({ + required String name, + required String date, + required String url, + }) = _MarianumMessage; + + factory MarianumMessage.fromJson(Map json) => _$MarianumMessageFromJson(json); +} + + +enum GradeAveragesGradingSystem { + highSchool, + middleSchool, +} diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.freezed.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.freezed.dart new file mode 100644 index 0000000..35105e1 --- /dev/null +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.freezed.dart @@ -0,0 +1,507 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'marianum_message_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +MarianumMessageState _$MarianumMessageStateFromJson(Map json) { + return _MarianumMessageState.fromJson(json); +} + +/// @nodoc +mixin _$MarianumMessageState { + MarianumMessageList get messageList => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MarianumMessageStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MarianumMessageStateCopyWith<$Res> { + factory $MarianumMessageStateCopyWith(MarianumMessageState value, + $Res Function(MarianumMessageState) then) = + _$MarianumMessageStateCopyWithImpl<$Res, MarianumMessageState>; + @useResult + $Res call({MarianumMessageList messageList}); + + $MarianumMessageListCopyWith<$Res> get messageList; +} + +/// @nodoc +class _$MarianumMessageStateCopyWithImpl<$Res, + $Val extends MarianumMessageState> + implements $MarianumMessageStateCopyWith<$Res> { + _$MarianumMessageStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? messageList = null, + }) { + return _then(_value.copyWith( + messageList: null == messageList + ? _value.messageList + : messageList // ignore: cast_nullable_to_non_nullable + as MarianumMessageList, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $MarianumMessageListCopyWith<$Res> get messageList { + return $MarianumMessageListCopyWith<$Res>(_value.messageList, (value) { + return _then(_value.copyWith(messageList: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$MarianumMessageStateImplCopyWith<$Res> + implements $MarianumMessageStateCopyWith<$Res> { + factory _$$MarianumMessageStateImplCopyWith(_$MarianumMessageStateImpl value, + $Res Function(_$MarianumMessageStateImpl) then) = + __$$MarianumMessageStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({MarianumMessageList messageList}); + + @override + $MarianumMessageListCopyWith<$Res> get messageList; +} + +/// @nodoc +class __$$MarianumMessageStateImplCopyWithImpl<$Res> + extends _$MarianumMessageStateCopyWithImpl<$Res, _$MarianumMessageStateImpl> + implements _$$MarianumMessageStateImplCopyWith<$Res> { + __$$MarianumMessageStateImplCopyWithImpl(_$MarianumMessageStateImpl _value, + $Res Function(_$MarianumMessageStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? messageList = null, + }) { + return _then(_$MarianumMessageStateImpl( + messageList: null == messageList + ? _value.messageList + : messageList // ignore: cast_nullable_to_non_nullable + as MarianumMessageList, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MarianumMessageStateImpl implements _MarianumMessageState { + const _$MarianumMessageStateImpl({required this.messageList}); + + factory _$MarianumMessageStateImpl.fromJson(Map json) => + _$$MarianumMessageStateImplFromJson(json); + + @override + final MarianumMessageList messageList; + + @override + String toString() { + return 'MarianumMessageState(messageList: $messageList)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MarianumMessageStateImpl && + (identical(other.messageList, messageList) || + other.messageList == messageList)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, messageList); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MarianumMessageStateImplCopyWith<_$MarianumMessageStateImpl> + get copyWith => + __$$MarianumMessageStateImplCopyWithImpl<_$MarianumMessageStateImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$MarianumMessageStateImplToJson( + this, + ); + } +} + +abstract class _MarianumMessageState implements MarianumMessageState { + const factory _MarianumMessageState( + {required final MarianumMessageList messageList}) = + _$MarianumMessageStateImpl; + + factory _MarianumMessageState.fromJson(Map json) = + _$MarianumMessageStateImpl.fromJson; + + @override + MarianumMessageList get messageList; + @override + @JsonKey(ignore: true) + _$$MarianumMessageStateImplCopyWith<_$MarianumMessageStateImpl> + get copyWith => throw _privateConstructorUsedError; +} + +MarianumMessageList _$MarianumMessageListFromJson(Map json) { + return _MarianumMessageList.fromJson(json); +} + +/// @nodoc +mixin _$MarianumMessageList { + String get base => throw _privateConstructorUsedError; + List get messages => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MarianumMessageListCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MarianumMessageListCopyWith<$Res> { + factory $MarianumMessageListCopyWith( + MarianumMessageList value, $Res Function(MarianumMessageList) then) = + _$MarianumMessageListCopyWithImpl<$Res, MarianumMessageList>; + @useResult + $Res call({String base, List messages}); +} + +/// @nodoc +class _$MarianumMessageListCopyWithImpl<$Res, $Val extends MarianumMessageList> + implements $MarianumMessageListCopyWith<$Res> { + _$MarianumMessageListCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? base = null, + Object? messages = null, + }) { + return _then(_value.copyWith( + base: null == base + ? _value.base + : base // ignore: cast_nullable_to_non_nullable + as String, + messages: null == messages + ? _value.messages + : messages // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MarianumMessageListImplCopyWith<$Res> + implements $MarianumMessageListCopyWith<$Res> { + factory _$$MarianumMessageListImplCopyWith(_$MarianumMessageListImpl value, + $Res Function(_$MarianumMessageListImpl) then) = + __$$MarianumMessageListImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String base, List messages}); +} + +/// @nodoc +class __$$MarianumMessageListImplCopyWithImpl<$Res> + extends _$MarianumMessageListCopyWithImpl<$Res, _$MarianumMessageListImpl> + implements _$$MarianumMessageListImplCopyWith<$Res> { + __$$MarianumMessageListImplCopyWithImpl(_$MarianumMessageListImpl _value, + $Res Function(_$MarianumMessageListImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? base = null, + Object? messages = null, + }) { + return _then(_$MarianumMessageListImpl( + base: null == base + ? _value.base + : base // ignore: cast_nullable_to_non_nullable + as String, + messages: null == messages + ? _value._messages + : messages // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MarianumMessageListImpl implements _MarianumMessageList { + const _$MarianumMessageListImpl( + {required this.base, required final List messages}) + : _messages = messages; + + factory _$MarianumMessageListImpl.fromJson(Map json) => + _$$MarianumMessageListImplFromJson(json); + + @override + final String base; + final List _messages; + @override + List get messages { + if (_messages is EqualUnmodifiableListView) return _messages; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_messages); + } + + @override + String toString() { + return 'MarianumMessageList(base: $base, messages: $messages)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MarianumMessageListImpl && + (identical(other.base, base) || other.base == base) && + const DeepCollectionEquality().equals(other._messages, _messages)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, base, const DeepCollectionEquality().hash(_messages)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MarianumMessageListImplCopyWith<_$MarianumMessageListImpl> get copyWith => + __$$MarianumMessageListImplCopyWithImpl<_$MarianumMessageListImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$MarianumMessageListImplToJson( + this, + ); + } +} + +abstract class _MarianumMessageList implements MarianumMessageList { + const factory _MarianumMessageList( + {required final String base, + required final List messages}) = + _$MarianumMessageListImpl; + + factory _MarianumMessageList.fromJson(Map json) = + _$MarianumMessageListImpl.fromJson; + + @override + String get base; + @override + List get messages; + @override + @JsonKey(ignore: true) + _$$MarianumMessageListImplCopyWith<_$MarianumMessageListImpl> get copyWith => + throw _privateConstructorUsedError; +} + +MarianumMessage _$MarianumMessageFromJson(Map json) { + return _MarianumMessage.fromJson(json); +} + +/// @nodoc +mixin _$MarianumMessage { + String get name => throw _privateConstructorUsedError; + String get date => throw _privateConstructorUsedError; + String get url => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MarianumMessageCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MarianumMessageCopyWith<$Res> { + factory $MarianumMessageCopyWith( + MarianumMessage value, $Res Function(MarianumMessage) then) = + _$MarianumMessageCopyWithImpl<$Res, MarianumMessage>; + @useResult + $Res call({String name, String date, String url}); +} + +/// @nodoc +class _$MarianumMessageCopyWithImpl<$Res, $Val extends MarianumMessage> + implements $MarianumMessageCopyWith<$Res> { + _$MarianumMessageCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? date = null, + Object? url = null, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as String, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MarianumMessageImplCopyWith<$Res> + implements $MarianumMessageCopyWith<$Res> { + factory _$$MarianumMessageImplCopyWith(_$MarianumMessageImpl value, + $Res Function(_$MarianumMessageImpl) then) = + __$$MarianumMessageImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String name, String date, String url}); +} + +/// @nodoc +class __$$MarianumMessageImplCopyWithImpl<$Res> + extends _$MarianumMessageCopyWithImpl<$Res, _$MarianumMessageImpl> + implements _$$MarianumMessageImplCopyWith<$Res> { + __$$MarianumMessageImplCopyWithImpl( + _$MarianumMessageImpl _value, $Res Function(_$MarianumMessageImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? date = null, + Object? url = null, + }) { + return _then(_$MarianumMessageImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as String, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MarianumMessageImpl implements _MarianumMessage { + const _$MarianumMessageImpl( + {required this.name, required this.date, required this.url}); + + factory _$MarianumMessageImpl.fromJson(Map json) => + _$$MarianumMessageImplFromJson(json); + + @override + final String name; + @override + final String date; + @override + final String url; + + @override + String toString() { + return 'MarianumMessage(name: $name, date: $date, url: $url)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MarianumMessageImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.date, date) || other.date == date) && + (identical(other.url, url) || other.url == url)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, name, date, url); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MarianumMessageImplCopyWith<_$MarianumMessageImpl> get copyWith => + __$$MarianumMessageImplCopyWithImpl<_$MarianumMessageImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$MarianumMessageImplToJson( + this, + ); + } +} + +abstract class _MarianumMessage implements MarianumMessage { + const factory _MarianumMessage( + {required final String name, + required final String date, + required final String url}) = _$MarianumMessageImpl; + + factory _MarianumMessage.fromJson(Map json) = + _$MarianumMessageImpl.fromJson; + + @override + String get name; + @override + String get date; + @override + String get url; + @override + @JsonKey(ignore: true) + _$$MarianumMessageImplCopyWith<_$MarianumMessageImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.g.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.g.dart new file mode 100644 index 0000000..80dc1a2 --- /dev/null +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_state.g.dart @@ -0,0 +1,52 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'marianum_message_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$MarianumMessageStateImpl _$$MarianumMessageStateImplFromJson( + Map json) => + _$MarianumMessageStateImpl( + messageList: MarianumMessageList.fromJson( + json['messageList'] as Map), + ); + +Map _$$MarianumMessageStateImplToJson( + _$MarianumMessageStateImpl instance) => + { + 'messageList': instance.messageList, + }; + +_$MarianumMessageListImpl _$$MarianumMessageListImplFromJson( + Map json) => + _$MarianumMessageListImpl( + base: json['base'] as String, + messages: (json['messages'] as List) + .map((e) => MarianumMessage.fromJson(e as Map)) + .toList(), + ); + +Map _$$MarianumMessageListImplToJson( + _$MarianumMessageListImpl instance) => + { + 'base': instance.base, + 'messages': instance.messages, + }; + +_$MarianumMessageImpl _$$MarianumMessageImplFromJson( + Map json) => + _$MarianumMessageImpl( + name: json['name'] as String, + date: json['date'] as String, + url: json['url'] as String, + ); + +Map _$$MarianumMessageImplToJson( + _$MarianumMessageImpl instance) => + { + 'name': instance.name, + 'date': instance.date, + 'url': instance.url, + }; diff --git a/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart new file mode 100644 index 0000000..694b304 --- /dev/null +++ b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart @@ -0,0 +1,6 @@ +import '../../../infrastructure/dataLoader/data_loader.dart'; + +class MarianumMessageGetMessages extends DataLoader { + @override + Future fetch() => Future(() => 'Test'); +} diff --git a/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart b/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart new file mode 100644 index 0000000..9679f44 --- /dev/null +++ b/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart @@ -0,0 +1,9 @@ +import '../../../infrastructure/loadableState/loadable_state.dart'; +import '../../../infrastructure/repository/repository.dart'; +import '../dataProvider/marianum_message_get_messages.dart'; + +class MarianumMessageRepository extends Repository { + MarianumMessageRepository(void Function(LoadableState content) emit) : super([ + MarianumMessageGetMessages(), + ]); +} diff --git a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart new file mode 100644 index 0000000..fa323c7 --- /dev/null +++ b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart @@ -0,0 +1,49 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; + +import '../../../../../api/mhsl/message/getMessages/getMessagesResponse.dart'; +import '../../../../../view/pages/more/message/messageView.dart'; +import '../../../infrastructure/loadableState/loadable_state.dart'; +import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart'; +import '../../../infrastructure/utilityWidgets/bloc_providing_builder.dart'; +import '../bloc/marianum_message_bloc.dart'; +import '../bloc/marianum_message_state.dart'; + +class MarianumMessageListView extends StatelessWidget { + const MarianumMessageListView({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Marianum Message'), + ), + body: BlocProvidingBuilder>( + create: (context) => MarianumMessageBloc(), + child: (context, state) { + // if(value.primaryLoading()) return const LoadingSpinner(); + log(state.toString()); + return LoadableStateConsumer, MarianumMessageState>( + child: (state) => ListView.builder( + itemCount: state.messageList.messages.length, + itemBuilder: (context, index) { + var message = state.messageList.messages.toList()[index]; + return ListTile( + leading: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [Icon(Icons.newspaper)], + ), + title: Text(message.name, overflow: TextOverflow.ellipsis), + subtitle: Text('vom ${message.date}'), + trailing: const Icon(Icons.arrow_right), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: state.messageList.base, message: message as GetMessagesResponseObject))); + }, + ); + } + ), + ); + } + ), + ); +} diff --git a/lib/state/widgets/components/error_bar.dart b/lib/state/widgets/components/error_bar.dart deleted file mode 100644 index 0d1c8bf..0000000 --- a/lib/state/widgets/components/error_bar.dart +++ /dev/null @@ -1,58 +0,0 @@ -// import 'package:flutter/material.dart'; -// -// import '../../app/base/infrastructure/errorBar/error_bar_controller.dart'; -// import '../../infrastructure/state_extensions.dart'; -// import '../controller_provider.dart'; -// -// class ErrorBar extends StatelessWidget { -// final bool visible; -// const ErrorBar({required this.visible, super.key}); -// -// final Duration animationDuration = const Duration(milliseconds: 200); -// -// @override -// Widget build(BuildContext context) => ControllerProvider( -// create: (context) => ErrorBarController(), -// child: (context) => AnimatedSize( -// duration: animationDuration, -// child: AnimatedSwitcher( -// duration: animationDuration, -// transitionBuilder: (Widget child, Animation animation) => SlideTransition( -// position: Tween( -// begin: const Offset(0.0, -1.0), -// end: Offset.zero, -// ).animate(animation), -// child: child, -// ), -// child: Visibility( -// key: Key(visible.hashCode.toString()), -// visible: visible, -// replacement: const SizedBox(width: double.infinity), -// child: Builder( -// builder: (context) { -// var controller = context.watchController(); -// 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); -// -// return Container( -// height: 20, -// decoration: BoxDecoration( -// color: status.color, -// ), -// child: Row( -// mainAxisAlignment: MainAxisAlignment.center, -// children: [ -// Icon(status.icon, size: 14), -// const SizedBox(width: 10), -// Text(status.text, style: const TextStyle(fontSize: 12)) -// ], -// ), -// ); -// }, -// ) -// ) -// ), -// ), -// ); -// } diff --git a/lib/state/widgets/controller_consumer.dart b/lib/state/widgets/controller_consumer.dart deleted file mode 100644 index c814d94..0000000 --- a/lib/state/widgets/controller_consumer.dart +++ /dev/null @@ -1,12 +0,0 @@ -// import 'package:flutter/material.dart'; -// import 'package:flutter_bloc/flutter_bloc.dart'; -// -// import '../infrastructure/controller.dart'; -// -// class ControllerConsumer, TState> extends StatelessWidget { -// final Widget Function(BuildContext context, TState state) child; -// const ControllerConsumer({required this.child, super.key}); -// -// @override -// Widget build(BuildContext context) => BlocBuilder(builder: child); -// } diff --git a/lib/state/widgets/controller_provider.dart b/lib/state/widgets/controller_provider.dart deleted file mode 100644 index e232ac3..0000000 --- a/lib/state/widgets/controller_provider.dart +++ /dev/null @@ -1,21 +0,0 @@ -// import 'package:flutter/material.dart'; -// import 'package:flutter_bloc/flutter_bloc.dart'; -// import 'package:provider/single_child_widget.dart'; -// -// import '../infrastructure/controller.dart'; -// -// -// class ControllerProvider extends SingleChildStatelessWidget { -// final TState Function(BuildContext context) create; -// final bool lazy; -// final Widget Function(BuildContext context) child; -// ControllerProvider({required this.create, this.lazy = true, required this.child, super.key}) -// : super(child: Builder(builder: child)); -// -// @override -// Widget buildWithChild(BuildContext context, Widget? child) => BlocProvider( -// create: create, -// lazy: lazy, -// child: child, -// ); -// } diff --git a/lib/state/widgets/controllers_provider.dart b/lib/state/widgets/controllers_provider.dart deleted file mode 100644 index 1b5417b..0000000 --- a/lib/state/widgets/controllers_provider.dart +++ /dev/null @@ -1,17 +0,0 @@ -// -// import 'package:flutter/material.dart'; -// import 'package:flutter_bloc/flutter_bloc.dart'; -// -// import 'controller_provider.dart'; -// -// class ControllersProvider extends StatelessWidget { -// final List controllers; -// final Widget Function(BuildContext context) child; -// const ControllersProvider({required this.controllers, required this.child, super.key}); -// -// @override -// Widget build(BuildContext context) => MultiBlocProvider( -// providers: controllers, -// child: Builder(builder: child) -// ); -// } diff --git a/lib/state/widgets/loadable_controller_consumer.dart b/lib/state/widgets/loadable_controller_consumer.dart deleted file mode 100644 index 5fa8b48..0000000 --- a/lib/state/widgets/loadable_controller_consumer.dart +++ /dev/null @@ -1,42 +0,0 @@ -// import 'package:flutter/material.dart'; -// import 'package:flutter_bloc/flutter_bloc.dart'; -// -// import '../infrastructure/controller.dart'; -// import '../infrastructure/loadable_state.dart'; -// import '../infrastructure/state_extensions.dart'; -// import 'components/background_loading_indicator.dart'; -// import 'components/error_bar.dart'; -// import 'components/primary_loading_indicator.dart'; -// -// class LoadableControllerConsumer, TState extends LoadableState> extends StatelessWidget { -// final Widget child; -// const LoadableControllerConsumer({required this.child, super.key}); -// -// final Duration animationDuration = const Duration(milliseconds: 200); -// -// @override -// Widget build(BuildContext context) { -// var state = context.readController().state; -// -// return Column( -// children: [ -// // ErrorBar(visible: state.errorBarVisible()), -// // Expanded( -// // child: Stack( -// // children: [ -// // PrimaryLoadingIndicator(visible: !state.hasStateData()), -// // BackgroundLoadingIndicator(visible: state.isBackgroundLoading() && !state.errorBarVisible()), -// // -// // AnimatedOpacity( -// // opacity: state.hasStateData() ? 1.0 : 0.0, -// // duration: animationDuration, -// // curve: Curves.easeInOut, -// // child: state.hasStateData() ? child : const SizedBox.shrink() -// // ), -// // ], -// // ), -// // ) -// ], -// ); -// } -// } diff --git a/lib/state/widgets/sub_selected_controller_consumer.dart b/lib/state/widgets/sub_selected_controller_consumer.dart deleted file mode 100644 index fdbe68f..0000000 --- a/lib/state/widgets/sub_selected_controller_consumer.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SubSelectedControllerConsumer, TFullState, TFilteredState> extends StatelessWidget { - final Widget Function(BuildContext context, TFilteredState state) child; - final TFilteredState Function(TFullState state) subSelect; - const SubSelectedControllerConsumer({required this.subSelect, required this.child, super.key}); - - @override - Widget build(BuildContext context) => BlocSelector(selector: subSelect, builder: child); -} diff --git a/lib/view/pages/more/test.dart b/lib/view/pages/more/test.dart index 36ec438..5f68993 100644 --- a/lib/view/pages/more/test.dart +++ b/lib/view/pages/more/test.dart @@ -4,7 +4,7 @@ // import '../../../state/infrastructure/loadable_state.dart'; // import '../../../state/infrastructure/state_extensions.dart'; // import '../../../state/widgets/controller_consumer.dart'; -// import '../../../state/widgets/loadable_controller_consumer.dart'; +// import '../../../state/widgets/loadable_state_consumer.dart'; // import '../../../state/widgets/sub_selected_controller_consumer.dart'; // import '../../../state/widgets/controller_provider.dart'; // diff --git a/lib/view/pages/overhang.dart b/lib/view/pages/overhang.dart index 184953b..599c8f6 100644 --- a/lib/view/pages/overhang.dart +++ b/lib/view/pages/overhang.dart @@ -6,15 +6,14 @@ 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/screens/grade_averages_view.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 '../../widget/centeredLeading.dart'; import '../../widget/infoDialog.dart'; import '../settings/settings.dart'; import 'more/feedback/feedbackDialog.dart'; -import 'more/gradeAverages/gradeAverage.dart'; import 'more/holidays/holidays.dart'; -import 'more/message/message.dart'; import 'more/roomplan/roomplan.dart'; import 'more/share/selectShareTypeDialog.dart'; @@ -31,9 +30,9 @@ class Overhang extends StatelessWidget { ), body: ListView( children: [ - const ListItemNavigator(icon: Icons.newspaper, text: 'Marianum Message', target: Message()), + 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: GradeAverage()), + const ListItemNavigator(icon: Icons.calculate, text: 'Notendurschnittsrechner', target: GradeAveragesView()), const ListItemNavigator(icon: Icons.calendar_month, text: 'Schulferien', target: Holidays()), const Divider(), ListTile( @@ -77,7 +76,7 @@ class Overhang extends StatelessWidget { ), ListTile( leading: const Icon(Icons.science_outlined), - onTap: () => pushScreen(context, withNavBar: false, screen: const GradeAveragesScreen()), + // onTap: () => pushScreen(context, withNavBar: false, screen: const GradeAveragesScreen()), ) ], ), diff --git a/lib/view/settings/devToolsSettingsDialog.dart b/lib/view/settings/devToolsSettingsDialog.dart index c62cdde..e1fb660 100644 --- a/lib/view/settings/devToolsSettingsDialog.dart +++ b/lib/view/settings/devToolsSettingsDialog.dart @@ -1,6 +1,7 @@ import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:provider/provider.dart'; import '../../storage/base/settingsProvider.dart'; @@ -109,6 +110,26 @@ class _DevToolsSettingsDialogState extends State { }, trailing: const Icon(Icons.arrow_right), ), + ListTile( + leading: const CenteredLeading(Icon(Icons.data_object)), + title: const Text('BLOC State cache'), + subtitle: FutureBuilder( + future: const CacheView().totalSize(), + builder: (context, snapshot) => Text("etwa ${snapshot.hasError ? "?" : snapshot.hasData ? filesize(snapshot.data) : "..."}\nLange tippen um zu löschen"), + ), + onTap: () { + // Navigator.push(context, MaterialPageRoute(builder: (context) => const CacheView())); + }, + onLongPress: () { + ConfirmDialog( + title: 'BLOC-Cache löschen', + content: 'Alle cache Einträge werden gelöscht. Der Cache wird bei Nutzung der App automatisch erneut aufgebaut', + confirmButton: 'Unwiederruflich löschen', + onConfirm: () => HydratedBloc.storage.clear(), + ).asDialog(context); + }, + trailing: const Icon(Icons.arrow_right), + ), ], ); } From b171fef348eb79a406f7181cb53c6cc69f7adad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Tue, 7 May 2024 22:15:56 +0200 Subject: [PATCH 09/19] repository and data provider concept --- .../dataLoader/data_loader.dart | 3 +- .../loadableState/loadable_hydrated_bloc.dart | 25 ------- .../loadable_state_background_loading.dart | 6 +- .../view/loadable_state_consumer.dart | 18 ++--- .../view/loadable_state_error_bar.dart | 4 +- .../view/loadable_state_primary_loading.dart | 6 +- .../infrastructure/repository/repository.dart | 14 +--- ...roviding_builder.dart => bloc_module.dart} | 4 +- .../loadable_hydrated_bloc.dart | 31 +++++++++ .../loadable_hydrated_bloc_event.dart | 7 ++ .../bloc/marianum_message_bloc.dart | 29 ++++++-- .../marianum_message_get_messages.dart | 9 ++- .../marianum_message_repository.dart | 8 +-- .../view/marianum_message_list_view.dart | 69 ++++++++++--------- 14 files changed, 129 insertions(+), 104 deletions(-) delete mode 100644 lib/state/app/infrastructure/loadableState/loadable_hydrated_bloc.dart rename lib/state/app/infrastructure/utilityWidgets/{bloc_providing_builder.dart => bloc_module.dart} (64%) create mode 100644 lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart create mode 100644 lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart diff --git a/lib/state/app/infrastructure/dataLoader/data_loader.dart b/lib/state/app/infrastructure/dataLoader/data_loader.dart index 6f84cb1..47306ba 100644 --- a/lib/state/app/infrastructure/dataLoader/data_loader.dart +++ b/lib/state/app/infrastructure/dataLoader/data_loader.dart @@ -1,4 +1,3 @@ abstract class DataLoader { - - Future fetch(); + Future fetch(); } diff --git a/lib/state/app/infrastructure/loadableState/loadable_hydrated_bloc.dart b/lib/state/app/infrastructure/loadableState/loadable_hydrated_bloc.dart deleted file mode 100644 index 5694249..0000000 --- a/lib/state/app/infrastructure/loadableState/loadable_hydrated_bloc.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:hydrated_bloc/hydrated_bloc.dart'; - -import '../repository/repository.dart'; -import 'loadable_state.dart'; - -sealed class LoadableHydratedBlocEvent {} -class DataRecieved extends LoadableHydratedBlocEvent {} - -abstract class LoadableHydratedBloc extends HydratedBloc> { - LoadableHydratedBloc() : super(const LoadableState()) { - repository().load(); - - } - - Repository repository(); - - - @override - fromJson(Map json) => LoadableState(isLoading: true, data: fromStorage(json)); - @override - Map? toJson(state) => state.data.toJson(); - - TState fromStorage(Map json); - Map? toStorage(TState state); -} diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_background_loading.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_background_loading.dart index 1c10bb0..5147e45 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_background_loading.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_background_loading.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; +import 'loadable_state_consumer.dart'; + class LoadableStateBackgroundLoading extends StatelessWidget { final bool visible; const LoadableStateBackgroundLoading({required this.visible, super.key}); - final Duration animationDuration = const Duration(milliseconds: 200); - @override Widget build(BuildContext context) => AnimatedSwitcher( - duration: animationDuration, + duration: LoadableStateConsumer.animationDuration, transitionBuilder: (Widget child, Animation animation) => SlideTransition( position: Tween( begin: const Offset(0.0, -1.0), diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart index c3d3c75..513a13e 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart @@ -2,36 +2,36 @@ import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../../utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; import '../loadable_state.dart'; import 'loadable_state_background_loading.dart'; import 'loadable_state_error_bar.dart'; import 'loadable_state_primary_loading.dart'; -// TODO might be a simpler way -class LoadableStateConsumer, TWrappedState extends LoadableState, TState> extends StatelessWidget { - final Widget Function(TState state) child; +class LoadableStateConsumer, LoadableState>, TState> extends StatelessWidget { + final Widget Function(TState state, bool loading) child; const LoadableStateConsumer({required this.child, super.key}); static Duration animationDuration = const Duration(milliseconds: 200); @override Widget build(BuildContext context) { - var state = context.read().state as LoadableState; + var loadableState = context.watch().state; return Column( children: [ - LoadableStateErrorBar(visible: state.showError()), + LoadableStateErrorBar(visible: loadableState.showError()), Expanded( child: Stack( children: [ - LoadableStatePrimaryLoading(visible: state.showPrimaryLoading()), - LoadableStateBackgroundLoading(visible: state.showBackgroundLoading()), + LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()), + LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()), AnimatedOpacity( - opacity: state.showContent() ? 1.0 : 0.0, + opacity: loadableState.showContent() ? 1.0 : 0.0, duration: animationDuration, curve: Curves.easeInOut, - child: state.showContent() ? child(state.data) : const SizedBox.shrink() + child: loadableState.showContent() ? child(loadableState.data, loadableState.isLoading) : const SizedBox.shrink() ), ], ), diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart index 4e21c72..b5c1f55 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../utilityWidgets/bloc_providing_builder.dart'; +import '../../utilityWidgets/bloc_module.dart'; import '../bloc/loadable_state_bloc.dart'; import '../bloc/loadable_state_state.dart'; @@ -12,7 +12,7 @@ class LoadableStateErrorBar extends StatelessWidget { final Duration animationDuration = const Duration(milliseconds: 200); @override - Widget build(BuildContext context) => BlocProvidingBuilder( + Widget build(BuildContext context) => BlocModule( create: (context) => LoadableStateBloc(), child: (context, state) => AnimatedSize( duration: animationDuration, diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_primary_loading.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_primary_loading.dart index a01ba28..053aaba 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_primary_loading.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_primary_loading.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; +import 'loadable_state_consumer.dart'; + class LoadableStatePrimaryLoading extends StatelessWidget { final bool visible; const LoadableStatePrimaryLoading({required this.visible, super.key}); - final Duration animationDuration = const Duration(milliseconds: 200); - @override Widget build(BuildContext context) => AnimatedOpacity( opacity: visible ? 1.0 : 0.0, - duration: animationDuration, + duration: LoadableStateConsumer.animationDuration, curve: Curves.easeInOut, child: const Center(child: CircularProgressIndicator()), ); diff --git a/lib/state/app/infrastructure/repository/repository.dart b/lib/state/app/infrastructure/repository/repository.dart index 4a6caae..58a4e85 100644 --- a/lib/state/app/infrastructure/repository/repository.dart +++ b/lib/state/app/infrastructure/repository/repository.dart @@ -1,13 +1 @@ -import '../dataLoader/data_loader.dart'; - -abstract class Repository { - final List dataLoaders; - - Repository(this.dataLoaders); - - Future load() async { - dataLoaders.forEach((element) { - element.fetch(); - }); - } -} +abstract class Repository {} diff --git a/lib/state/app/infrastructure/utilityWidgets/bloc_providing_builder.dart b/lib/state/app/infrastructure/utilityWidgets/bloc_module.dart similarity index 64% rename from lib/state/app/infrastructure/utilityWidgets/bloc_providing_builder.dart rename to lib/state/app/infrastructure/utilityWidgets/bloc_module.dart index 4c98579..3a61b9c 100644 --- a/lib/state/app/infrastructure/utilityWidgets/bloc_providing_builder.dart +++ b/lib/state/app/infrastructure/utilityWidgets/bloc_module.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class BlocProvidingBuilder, TState> extends StatelessWidget { +class BlocModule, TState> extends StatelessWidget { final TBloc Function(BuildContext context) create; final Widget Function(BuildContext context, TState state) child; - const BlocProvidingBuilder({required this.create, required this.child, super.key}); + const BlocModule({required this.create, required this.child, super.key}); @override Widget build(BuildContext context) => BlocProvider(create: create, child: BlocBuilder(builder: child)); diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart new file mode 100644 index 0000000..7f6d8a8 --- /dev/null +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart @@ -0,0 +1,31 @@ +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import '../../repository/repository.dart'; +import 'loadable_hydrated_bloc_event.dart'; +import '../../loadableState/loadable_state.dart'; + +abstract class LoadableHydratedBloc, TState, TRepository extends Repository> extends HydratedBloc, LoadableState> { + late TRepository _repository; + LoadableHydratedBloc() : super(const LoadableState()) { + on>((event, emit) => emit(LoadableState(isLoading: event.loading, data: event.state(innerState ?? fromNothing())))); + on>((event, emit) => emit(const LoadableState())); + + _repository = repository(); + loadState(); + } + + TState? get innerState => state.data; + TRepository get repo => _repository; + + @override + fromJson(Map json) => LoadableState(isLoading: true, data: fromStorage(json)); + @override + Map? toJson(LoadableState state) => state.data == null ? {} : state.data.toJson(); + + Future loadState(); + TRepository repository(); + + TState fromNothing(); + TState fromStorage(Map json); + Map? toStorage(TState state); +} diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart new file mode 100644 index 0000000..a4b3580 --- /dev/null +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart @@ -0,0 +1,7 @@ +class LoadableHydratedBlocEvent {} +class Emit extends LoadableHydratedBlocEvent { + final TState Function(TState state) state; + final bool loading; + Emit(this.state, {this.loading = false}); +} +class ClearState extends LoadableHydratedBlocEvent {} \ No newline at end of file diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart index 0563ae3..b960e9c 100644 --- a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart @@ -1,17 +1,32 @@ -import '../../../infrastructure/loadableState/loadable_hydrated_bloc.dart'; -import '../../../infrastructure/repository/repository.dart'; +import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart'; +import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; import '../repository/marianum_message_repository.dart'; import 'marianum_message_state.dart'; -class MarianumMessageBloc extends LoadableHydratedBloc { - MarianumMessageBloc(); +sealed class MarianumMessageEvent extends LoadableHydratedBlocEvent {} +class MessageEvent extends MarianumMessageEvent {} +class MarianumMessageBloc extends LoadableHydratedBloc { + MarianumMessageBloc() { + on((event, emit) { + add(Emit((state) => state.copyWith.messageList(messages: []))); + }); + } + + @override + Future loadState() async { + var messages = await repo.getMessages(); + add(Emit((state) => state.copyWith(messageList: messages))); + } + + @override + MarianumMessageRepository repository() => MarianumMessageRepository(); + + @override + MarianumMessageState fromNothing() => const MarianumMessageState(messageList: MarianumMessageList(base: "", messages: [])); @override MarianumMessageState fromStorage(Map json) => MarianumMessageState.fromJson(json); @override Map? toStorage(MarianumMessageState state) => state.toJson(); - - @override - Repository repository() => MarianumMessageRepository(emit); } diff --git a/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart index 694b304..4e0f188 100644 --- a/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart +++ b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart @@ -1,6 +1,11 @@ import '../../../infrastructure/dataLoader/data_loader.dart'; +import '../bloc/marianum_message_state.dart'; + +class MarianumMessageGetMessages extends DataLoader { -class MarianumMessageGetMessages extends DataLoader { @override - Future fetch() => Future(() => 'Test'); + Future fetch() async { + await Future.delayed(const Duration(seconds: 3)); + return const MarianumMessageList(base: '', messages: [MarianumMessage(date: '', name: 'RepoTest', url: '')]); + } } diff --git a/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart b/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart index 9679f44..033f821 100644 --- a/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart +++ b/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart @@ -1,9 +1,7 @@ -import '../../../infrastructure/loadableState/loadable_state.dart'; import '../../../infrastructure/repository/repository.dart'; +import '../bloc/marianum_message_state.dart'; import '../dataProvider/marianum_message_get_messages.dart'; -class MarianumMessageRepository extends Repository { - MarianumMessageRepository(void Function(LoadableState content) emit) : super([ - MarianumMessageGetMessages(), - ]); +class MarianumMessageRepository extends Repository { + Future getMessages() => MarianumMessageGetMessages().fetch(); } diff --git a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart index fa323c7..1fddef3 100644 --- a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart +++ b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart @@ -1,12 +1,14 @@ import 'dart:developer'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import '../../../../../api/mhsl/message/getMessages/getMessagesResponse.dart'; import '../../../../../view/pages/more/message/messageView.dart'; import '../../../infrastructure/loadableState/loadable_state.dart'; import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart'; -import '../../../infrastructure/utilityWidgets/bloc_providing_builder.dart'; +import '../../../infrastructure/utilityWidgets/bloc_module.dart'; +import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; import '../bloc/marianum_message_bloc.dart'; import '../bloc/marianum_message_state.dart'; @@ -14,36 +16,41 @@ class MarianumMessageListView extends StatelessWidget { const MarianumMessageListView({super.key}); @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Marianum Message'), - ), - body: BlocProvidingBuilder>( - create: (context) => MarianumMessageBloc(), - child: (context, state) { - // if(value.primaryLoading()) return const LoadingSpinner(); - log(state.toString()); - return LoadableStateConsumer, MarianumMessageState>( - child: (state) => ListView.builder( - itemCount: state.messageList.messages.length, - itemBuilder: (context, index) { - var message = state.messageList.messages.toList()[index]; - return ListTile( - leading: const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [Icon(Icons.newspaper)], - ), - title: Text(message.name, overflow: TextOverflow.ellipsis), - subtitle: Text('vom ${message.date}'), - trailing: const Icon(Icons.arrow_right), - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: state.messageList.base, message: message as GetMessagesResponseObject))); - }, - ); - } + Widget build(BuildContext context) => BlocModule>( + create: (context) => MarianumMessageBloc(), + child: (context, state) { + // if(value.primaryLoading()) return const LoadingSpinner(); + log(state.toString()); + return Scaffold( + appBar: AppBar( + title: const Text('Marianum Message'), + actions: [ + IconButton(onPressed: () => context.read().add(MessageEvent()), icon: Icon(Icons.abc)), + IconButton(onPressed: () => context.read().add(Emit((state) => MarianumMessageState(messageList: MarianumMessageList(base: "", messages: [MarianumMessage(url: "", name: "Teeest", date: "now")])))), icon: Icon(Icons.add)), + IconButton(onPressed: () => context.read().add(ClearState()), icon: Icon(Icons.add)) + ], + ), + body: LoadableStateConsumer( + child: (state, loading) => ListView.builder( + itemCount: state.messageList.messages.length, + itemBuilder: (context, index) { + var message = state.messageList.messages.toList()[index]; + return ListTile( + leading: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [Icon(Icons.newspaper)], + ), + title: Text("${message.name}${loading ? "loading" : "notloading"}", overflow: TextOverflow.ellipsis), + subtitle: Text('vom ${message.date}'), + trailing: const Icon(Icons.arrow_right), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: state.messageList.base, message: message as GetMessagesResponseObject))); + }, + ); + } ), - ); - } - ), + ), + ); + } ); } From 9fa711e460bdc80635c75539b628f1c30461baea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 11 May 2024 14:20:00 +0200 Subject: [PATCH 10/19] loadable error screen, reload actions, autoreload --- lib/app.dart | 68 +++---- .../bloc/loadable_state_bloc.dart | 36 +++- .../bloc/loadable_state_event.dart | 7 + .../loadableState/loadable_state.dart | 15 +- .../loadableState/loadable_state.freezed.dart | 53 +++++- .../loadableState/loading_error.dart | 12 ++ .../loadableState/loading_error.freezed.dart | 171 ++++++++++++++++++ .../view/loadable_state_consumer.dart | 63 +++++-- .../view/loadable_state_error_bar.dart | 85 +++++---- .../view/loadable_state_error_screen.dart | 42 +++++ .../utilityWidgets/bloc_module.dart | 17 +- .../loadable_hydrated_bloc.dart | 38 +++- .../loadable_hydrated_bloc_event.dart | 9 +- lib/state/app/modules/app_modules.dart | 41 ++++- .../bloc/marianum_message_bloc.dart | 2 +- .../marianum_message_get_messages.dart | 1 + .../view/marianum_message_list_view.dart | 11 +- lib/view/pages/overhang.dart | 16 +- lib/widget/ListItem.dart | 29 --- 19 files changed, 533 insertions(+), 183 deletions(-) create mode 100644 lib/state/app/infrastructure/loadableState/bloc/loadable_state_event.dart create mode 100644 lib/state/app/infrastructure/loadableState/loading_error.dart create mode 100644 lib/state/app/infrastructure/loadableState/loading_error.freezed.dart create mode 100644 lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart delete mode 100644 lib/widget/ListItem.dart diff --git a/lib/app.dart b/lib/app.dart index 7802cfd..5ba4596 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,6 +5,7 @@ import 'dart:developer'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; +import 'state/app/modules/app_modules.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:provider/provider.dart'; import 'package:badges/badges.dart' as badges; @@ -20,10 +21,7 @@ import 'notification/notificationController.dart'; import 'notification/notificationTasks.dart'; import 'notification/notifyUpdater.dart'; import 'storage/base/settingsProvider.dart'; -import 'view/pages/files/files.dart'; import 'view/pages/overhang.dart'; -import 'view/pages/talk/chatList.dart'; -import 'view/pages/timetable/timetable.dart'; class App extends StatefulWidget { const App({super.key}); @@ -101,50 +99,30 @@ class _AppState extends State with WidgetsBindingObserver { screenTransitionAnimation: const ScreenTransitionAnimation(curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200)), tabs: [ - PersistentTabConfig( - screen: const Breaker(breaker: BreakerArea.timetable, child: Timetable()), - item: ItemConfig( - activeForegroundColor: Theme.of(context).primaryColor, - inactiveForegroundColor: Theme.of(context).colorScheme.secondary, - icon: const Icon(Icons.calendar_month), - title: 'Vertretung' - ), - ), - PersistentTabConfig( - screen: const Breaker(breaker: BreakerArea.talk, child: ChatList()), - item: ItemConfig( - activeForegroundColor: Theme.of(context).primaryColor, - inactiveForegroundColor: Theme.of(context).colorScheme.secondary, - icon: Consumer( - builder: (context, value, child) { - if(value.primaryLoading()) return const Icon(Icons.chat); - var messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b); - return badges.Badge( - showBadge: messages > 0, - position: badges.BadgePosition.topEnd(top: -3, end: -3), - stackFit: StackFit.loose, - badgeStyle: badges.BadgeStyle( - padding: const EdgeInsets.all(3), - badgeColor: Theme.of(context).primaryColor, - elevation: 1, - ), - badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)), - child: const Icon(Icons.chat), - ); - }, - ), - title: 'Talk', - ), - ), - PersistentTabConfig( - screen: Breaker(breaker: BreakerArea.files, child: Files()), - item: ItemConfig( - activeForegroundColor: Theme.of(context).primaryColor, - inactiveForegroundColor: Theme.of(context).colorScheme.secondary, - icon: const Icon(Icons.folder), - title: 'Dateien' + AppModule.getModule(Modules.timetable).toBottomTab(context), + AppModule.getModule(Modules.talk).toBottomTab( + context, + itemBuilder: (icon) => Consumer( + builder: (context, value, child) { + if(value.primaryLoading()) return Icon(icon); + var messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b); + return badges.Badge( + showBadge: messages > 0, + position: badges.BadgePosition.topEnd(top: -3, end: -3), + stackFit: StackFit.loose, + badgeStyle: badges.BadgeStyle( + padding: const EdgeInsets.all(3), + badgeColor: Theme.of(context).primaryColor, + elevation: 1, + ), + badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)), + child: Icon(icon), + ); + }, ), ), + AppModule.getModule(Modules.files).toBottomTab(context), + PersistentTabConfig( screen: const Breaker(breaker: BreakerArea.more, child: Overhang()), item: ItemConfig( diff --git a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart index a2cb062..2ca0717 100644 --- a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart +++ b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart @@ -2,21 +2,47 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; +import '../loading_error.dart'; +import 'loadable_state_event.dart'; import 'loadable_state_state.dart'; -class LoadableStateBloc extends Bloc { +class LoadableStateBloc extends Bloc { late StreamSubscription> _updateStream; + LoadingError? loadingError; LoadableStateBloc() : super(const LoadableStateState(connections: null)) { - emitState(List v) => emit(state.copyWith(connections: v)); + on((event, emit) { + emit(event.state); + if(connectivityStatusKnown() && isConnected() && loadingError != null) { + if(!loadingError!.enableRetry) return; + loadingError!.retry!(); + } + }); - Connectivity().checkConnectivity().then(emitState); - _updateStream = Connectivity().onConnectivityChanged.listen(emitState); + emitConnectivity(List result) => add(ConnectivityChanged(LoadableStateState(connections: result))); + + Connectivity().checkConnectivity().then(emitConnectivity); + _updateStream = Connectivity().onConnectivityChanged.listen(emitConnectivity); } bool connectivityStatusKnown() => state.connections != null; - bool isConnected({bool? def}) => !(state.connections?.contains(ConnectivityResult.none) ?? def!); + bool isConnected() => !(state.connections?.contains(ConnectivityResult.none) ?? true); + bool allowRetry() => loadingError?.retry != null; + bool showErrorMessage() => isConnected() && loadingError != null; + + IconData connectionIcon() => connectivityStatusKnown() + ? isConnected() + ? Icons.nearby_error + : Icons.signal_wifi_connected_no_internet_4 + : Icons.device_unknown; + + String connectionText() => connectivityStatusKnown() + ? isConnected() + ? 'Verbindung fehlgeschlagen' + : 'Offline' + : 'Unbekannte Fehlerursache'; @override Future close() { diff --git a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_event.dart b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_event.dart new file mode 100644 index 0000000..80f3791 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_event.dart @@ -0,0 +1,7 @@ +import 'loadable_state_state.dart'; + +sealed class LoadableStateEvent {} +final class ConnectivityChanged extends LoadableStateEvent { + final LoadableStateState state; + ConnectivityChanged(this.state); +} diff --git a/lib/state/app/infrastructure/loadableState/loadable_state.dart b/lib/state/app/infrastructure/loadableState/loadable_state.dart index 6c25efc..98c87c1 100644 --- a/lib/state/app/infrastructure/loadableState/loadable_state.dart +++ b/lib/state/app/infrastructure/loadableState/loadable_state.dart @@ -1,5 +1,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; +import 'loading_error.dart'; + part 'loadable_state.freezed.dart'; @freezed @@ -9,10 +11,15 @@ class LoadableState with _$LoadableState { const factory LoadableState({ @Default(true) bool isLoading, @Default(null) TState? data, + @Default(null) LoadingError? error, }) = _LoadableState; - bool showPrimaryLoading() => isLoading && data == null; - bool showBackgroundLoading() => isLoading && data != null; - bool showError() => !isLoading && data == null; - bool showContent() => data != null; + bool _hasError() => error != null; + bool _hasData() => data != null; + + bool showPrimaryLoading() => isLoading && !_hasData(); + bool showBackgroundLoading() => isLoading && _hasData(); + bool showErrorBar() => _hasError() && _hasData(); + bool showError() => _hasError() && !_hasData(); + bool showContent() => _hasData(); } diff --git a/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart b/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart index 3bfb969..3b805ac 100644 --- a/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart +++ b/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart @@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$LoadableState { bool get isLoading => throw _privateConstructorUsedError; TState? get data => throw _privateConstructorUsedError; + LoadingError? get error => throw _privateConstructorUsedError; @JsonKey(ignore: true) $LoadableStateCopyWith> get copyWith => @@ -30,7 +31,9 @@ abstract class $LoadableStateCopyWith { $Res Function(LoadableState) then) = _$LoadableStateCopyWithImpl>; @useResult - $Res call({bool isLoading, TState? data}); + $Res call({bool isLoading, TState? data, LoadingError? error}); + + $LoadingErrorCopyWith<$Res>? get error; } /// @nodoc @@ -49,6 +52,7 @@ class _$LoadableStateCopyWithImpl? get error { + if (_value.error == null) { + return null; + } + + return $LoadingErrorCopyWith<$Res>(_value.error!, (value) { + return _then(_value.copyWith(error: value) as $Val); + }); + } } /// @nodoc @@ -71,7 +91,10 @@ abstract class _$$LoadableStateImplCopyWith __$$LoadableStateImplCopyWithImpl; @override @useResult - $Res call({bool isLoading, TState? data}); + $Res call({bool isLoading, TState? data, LoadingError? error}); + + @override + $LoadingErrorCopyWith<$Res>? get error; } /// @nodoc @@ -88,6 +111,7 @@ class __$$LoadableStateImplCopyWithImpl $Res call({ Object? isLoading = null, Object? data = freezed, + Object? error = freezed, }) { return _then(_$LoadableStateImpl( isLoading: null == isLoading @@ -98,6 +122,10 @@ class __$$LoadableStateImplCopyWithImpl ? _value.data : data // ignore: cast_nullable_to_non_nullable as TState?, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as LoadingError?, )); } } @@ -105,7 +133,8 @@ class __$$LoadableStateImplCopyWithImpl /// @nodoc class _$LoadableStateImpl extends _LoadableState { - const _$LoadableStateImpl({this.isLoading = true, this.data = null}) + const _$LoadableStateImpl( + {this.isLoading = true, this.data = null, this.error = null}) : super._(); @override @@ -114,10 +143,13 @@ class _$LoadableStateImpl extends _LoadableState { @override @JsonKey() final TState? data; + @override + @JsonKey() + final LoadingError? error; @override String toString() { - return 'LoadableState<$TState>(isLoading: $isLoading, data: $data)'; + return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, error: $error)'; } @override @@ -127,12 +159,13 @@ class _$LoadableStateImpl extends _LoadableState { other is _$LoadableStateImpl && (identical(other.isLoading, isLoading) || other.isLoading == isLoading) && - const DeepCollectionEquality().equals(other.data, data)); + const DeepCollectionEquality().equals(other.data, data) && + (identical(other.error, error) || other.error == error)); } @override int get hashCode => Object.hash( - runtimeType, isLoading, const DeepCollectionEquality().hash(data)); + runtimeType, isLoading, const DeepCollectionEquality().hash(data), error); @JsonKey(ignore: true) @override @@ -143,8 +176,10 @@ class _$LoadableStateImpl extends _LoadableState { } abstract class _LoadableState extends LoadableState { - const factory _LoadableState({final bool isLoading, final TState? data}) = - _$LoadableStateImpl; + const factory _LoadableState( + {final bool isLoading, + final TState? data, + final LoadingError? error}) = _$LoadableStateImpl; const _LoadableState._() : super._(); @override @@ -152,6 +187,8 @@ abstract class _LoadableState extends LoadableState { @override TState? get data; @override + LoadingError? get error; + @override @JsonKey(ignore: true) _$$LoadableStateImplCopyWith> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/state/app/infrastructure/loadableState/loading_error.dart b/lib/state/app/infrastructure/loadableState/loading_error.dart new file mode 100644 index 0000000..909f6d7 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/loading_error.dart @@ -0,0 +1,12 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'loading_error.freezed.dart'; + +@freezed +class LoadingError with _$LoadingError { + const factory LoadingError({ + required String message, + @Default(false) bool enableRetry, + @Default(null) void Function()? retry, + }) = _LoadingError; +} diff --git a/lib/state/app/infrastructure/loadableState/loading_error.freezed.dart b/lib/state/app/infrastructure/loadableState/loading_error.freezed.dart new file mode 100644 index 0000000..2570c98 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/loading_error.freezed.dart @@ -0,0 +1,171 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'loading_error.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$LoadingError { + String get message => throw _privateConstructorUsedError; + bool get enableRetry => throw _privateConstructorUsedError; + void Function()? get retry => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $LoadingErrorCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoadingErrorCopyWith<$Res> { + factory $LoadingErrorCopyWith( + LoadingError value, $Res Function(LoadingError) then) = + _$LoadingErrorCopyWithImpl<$Res, LoadingError>; + @useResult + $Res call({String message, bool enableRetry, void Function()? retry}); +} + +/// @nodoc +class _$LoadingErrorCopyWithImpl<$Res, $Val extends LoadingError> + implements $LoadingErrorCopyWith<$Res> { + _$LoadingErrorCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + Object? enableRetry = null, + Object? retry = freezed, + }) { + return _then(_value.copyWith( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + enableRetry: null == enableRetry + ? _value.enableRetry + : enableRetry // ignore: cast_nullable_to_non_nullable + as bool, + retry: freezed == retry + ? _value.retry + : retry // ignore: cast_nullable_to_non_nullable + as void Function()?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LoadingErrorImplCopyWith<$Res> + implements $LoadingErrorCopyWith<$Res> { + factory _$$LoadingErrorImplCopyWith( + _$LoadingErrorImpl value, $Res Function(_$LoadingErrorImpl) then) = + __$$LoadingErrorImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String message, bool enableRetry, void Function()? retry}); +} + +/// @nodoc +class __$$LoadingErrorImplCopyWithImpl<$Res> + extends _$LoadingErrorCopyWithImpl<$Res, _$LoadingErrorImpl> + implements _$$LoadingErrorImplCopyWith<$Res> { + __$$LoadingErrorImplCopyWithImpl( + _$LoadingErrorImpl _value, $Res Function(_$LoadingErrorImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + Object? enableRetry = null, + Object? retry = freezed, + }) { + return _then(_$LoadingErrorImpl( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + enableRetry: null == enableRetry + ? _value.enableRetry + : enableRetry // ignore: cast_nullable_to_non_nullable + as bool, + retry: freezed == retry + ? _value.retry + : retry // ignore: cast_nullable_to_non_nullable + as void Function()?, + )); + } +} + +/// @nodoc + +class _$LoadingErrorImpl implements _LoadingError { + const _$LoadingErrorImpl( + {required this.message, this.enableRetry = false, this.retry = null}); + + @override + final String message; + @override + @JsonKey() + final bool enableRetry; + @override + @JsonKey() + final void Function()? retry; + + @override + String toString() { + return 'LoadingError(message: $message, enableRetry: $enableRetry, retry: $retry)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoadingErrorImpl && + (identical(other.message, message) || other.message == message) && + (identical(other.enableRetry, enableRetry) || + other.enableRetry == enableRetry) && + (identical(other.retry, retry) || other.retry == retry)); + } + + @override + int get hashCode => Object.hash(runtimeType, message, enableRetry, retry); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LoadingErrorImplCopyWith<_$LoadingErrorImpl> get copyWith => + __$$LoadingErrorImplCopyWithImpl<_$LoadingErrorImpl>(this, _$identity); +} + +abstract class _LoadingError implements LoadingError { + const factory _LoadingError( + {required final String message, + final bool enableRetry, + final void Function()? retry}) = _$LoadingErrorImpl; + + @override + String get message; + @override + bool get enableRetry; + @override + void Function()? get retry; + @override + @JsonKey(ignore: true) + _$$LoadingErrorImplCopyWith<_$LoadingErrorImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart index 513a13e..c895b7c 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart @@ -2,10 +2,14 @@ import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../../utilityWidgets/bloc_module.dart'; import '../../utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; +import '../bloc/loadable_state_bloc.dart'; +import '../bloc/loadable_state_state.dart'; import '../loadable_state.dart'; import 'loadable_state_background_loading.dart'; import 'loadable_state_error_bar.dart'; +import 'loadable_state_error_screen.dart'; import 'loadable_state_primary_loading.dart'; class LoadableStateConsumer, LoadableState>, TState> extends StatelessWidget { @@ -17,26 +21,51 @@ class LoadableStateConsumer().state; + var childWidget = RefreshIndicator( + onRefresh: () { + loadableState.error != null && loadableState.error!.retry != null + ? loadableState.error!.retry!() + : null; + return Future.value(); + }, + child: SizedBox( + height: MediaQuery.of(context).size.height, + child: loadableState.showContent() + ? child(loadableState.data, loadableState.isLoading) + : const SizedBox.shrink(), + ), + // SingleChildScrollView( // TODO not all childs are reloadable + // physics: const AlwaysScrollableScrollPhysics(), + // child: + // ), + ); - return Column( - children: [ - LoadableStateErrorBar(visible: loadableState.showError()), - Expanded( - child: Stack( - children: [ - LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()), - LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()), + return BlocModule( + create: (context) => LoadableStateBloc(), + child: (context, bloc, state) { + bloc.loadingError = loadableState.error; + return Column( + children: [ + LoadableStateErrorBar(visible: loadableState.showErrorBar()), + Expanded( + child: Stack( + children: [ + LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()), + LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()), + LoadableStateErrorScreen(visible: loadableState.showError()), - AnimatedOpacity( - opacity: loadableState.showContent() ? 1.0 : 0.0, - duration: animationDuration, - curve: Curves.easeInOut, - child: loadableState.showContent() ? child(loadableState.data, loadableState.isLoading) : const SizedBox.shrink() + AnimatedOpacity( + opacity: loadableState.showContent() ? 1.0 : 0.0, + duration: animationDuration, + curve: Curves.easeInOut, + child: childWidget, + ), + ], ), - ], - ), - ) - ], + ) + ], + ); + } ); } } diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart index b5c1f55..ee3ee67 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../utilityWidgets/bloc_module.dart'; import '../bloc/loadable_state_bloc.dart'; -import '../bloc/loadable_state_state.dart'; class LoadableStateErrorBar extends StatelessWidget { final bool visible; @@ -12,48 +10,49 @@ class LoadableStateErrorBar extends StatelessWidget { final Duration animationDuration = const Duration(milliseconds: 200); @override - Widget build(BuildContext context) => BlocModule( - create: (context) => LoadableStateBloc(), - child: (context, state) => AnimatedSize( - duration: animationDuration, - child: AnimatedSwitcher( - duration: animationDuration, - transitionBuilder: (Widget child, Animation animation) => SlideTransition( - position: Tween( - begin: const Offset(0.0, -1.0), - end: Offset.zero, - ).animate(animation), - child: child, - ), - child: Visibility( - key: Key(visible.hashCode.toString()), - visible: visible, - replacement: const SizedBox(width: double.infinity), - child: Builder( - builder: (context) { - var controller = context.watch(); - var status = controller.connectivityStatusKnown() && !controller.isConnected() - ? (icon: Icons.wifi_off_outlined, text: 'Offline', color: Colors.grey.shade600) - : (icon: Icons.wifi_find_outlined, text: 'Verbindung fehlgeschlagen', color: Theme.of(context).primaryColor); + Widget build(BuildContext context) => AnimatedSize( + duration: animationDuration, + child: AnimatedSwitcher( + duration: animationDuration, + transitionBuilder: (Widget child, Animation animation) => SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: Visibility( + key: Key(visible.hashCode.toString()), + visible: visible, + replacement: const SizedBox(width: double.infinity), + child: Builder( + builder: (context) { + var bloc = context.watch(); + var status = ( + icon: bloc.connectionIcon(), + text: bloc.connectionText(), + color: bloc.connectivityStatusKnown() && !bloc.isConnected() + ? Colors.grey.shade600 + : Theme.of(context).primaryColor + ); - return Container( - height: 20, - decoration: BoxDecoration( - color: status.color, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(status.icon, size: 14), - const SizedBox(width: 10), - Text(status.text, style: const TextStyle(fontSize: 12)) - ], - ), - ); - }, - ) - ) - ), + return Container( + height: 20, + decoration: BoxDecoration( + color: status.color, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(status.icon, size: 14), + const SizedBox(width: 10), + Text(status.text, style: const TextStyle(fontSize: 12)) + ], + ), + ); + }, + ) + ) ), ); } diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart new file mode 100644 index 0000000..0737d28 --- /dev/null +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../bloc/loadable_state_bloc.dart'; +import 'loadable_state_consumer.dart'; + +class LoadableStateErrorScreen extends StatelessWidget { + final bool visible; + const LoadableStateErrorScreen({required this.visible, super.key}); + + + @override + Widget build(BuildContext context) { + final bloc = context.watch(); + return AnimatedOpacity( + opacity: visible ? 1.0 : 0.0, + duration: LoadableStateConsumer.animationDuration, + curve: Curves.easeInOut, + child: !visible ? null : Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(bloc.connectionIcon(), size: 40), + const SizedBox(height: 10), + Text(bloc.connectionText(), style: const TextStyle(fontSize: 20)), + + if(bloc.allowRetry()) ...[ + const SizedBox(height: 10), + TextButton(onPressed: () => bloc.loadingError!.retry!(), child: const Text('Erneut versuschen')), + ], + + if(bloc.showErrorMessage()) ...[ + const SizedBox(height: 40), + Text(bloc.loadingError!.message, style: TextStyle(color: Theme.of(context).hintColor, fontSize: 12)) + ], + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/state/app/infrastructure/utilityWidgets/bloc_module.dart b/lib/state/app/infrastructure/utilityWidgets/bloc_module.dart index 3a61b9c..35297b7 100644 --- a/lib/state/app/infrastructure/utilityWidgets/bloc_module.dart +++ b/lib/state/app/infrastructure/utilityWidgets/bloc_module.dart @@ -3,9 +3,20 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class BlocModule, TState> extends StatelessWidget { final TBloc Function(BuildContext context) create; - final Widget Function(BuildContext context, TState state) child; - const BlocModule({required this.create, required this.child, super.key}); + final Widget Function(BuildContext context, TBloc bloc, TState state) child; + final bool autoRebuild; + const BlocModule({required this.create, required this.child, this.autoRebuild = false, super.key}); + + Widget rebuildChild(BuildContext context) => child(context, context.watch(), context.watch().state); + Widget staticChild(BuildContext context) => child(context, context.read(), context.read().state); @override - Widget build(BuildContext context) => BlocProvider(create: create, child: BlocBuilder(builder: child)); + Widget build(BuildContext context) => BlocProvider( + create: create, + child: Builder( + builder: (context) => autoRebuild + ? rebuildChild(context) + : staticChild(context) + ) + ); } diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart index 7f6d8a8..7751907 100644 --- a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart @@ -1,28 +1,58 @@ +import 'dart:developer'; + import 'package:hydrated_bloc/hydrated_bloc.dart'; +import '../../loadableState/loading_error.dart'; import '../../repository/repository.dart'; import 'loadable_hydrated_bloc_event.dart'; import '../../loadableState/loadable_state.dart'; -abstract class LoadableHydratedBloc, TState, TRepository extends Repository> extends HydratedBloc, LoadableState> { +abstract class LoadableHydratedBloc< + TEvent extends LoadableHydratedBlocEvent, + TState, + TRepository extends Repository +> extends HydratedBloc< + LoadableHydratedBlocEvent, + LoadableState +> { late TRepository _repository; LoadableHydratedBloc() : super(const LoadableState()) { on>((event, emit) => emit(LoadableState(isLoading: event.loading, data: event.state(innerState ?? fromNothing())))); + on>((event, emit) => emit(LoadableState(isLoading: true, data: innerState))); on>((event, emit) => emit(const LoadableState())); - + on>((event, emit) => emit(LoadableState(isLoading: false, data: innerState, error: event.error))); + _repository = repository(); - loadState(); + fetch(); } TState? get innerState => state.data; TRepository get repo => _repository; + void fetch() { + gatherData().catchError( + (e) => add( + Error( + LoadingError( + message: e.toString(), + enableRetry: true, + retry: () { + log('Fetch retry on ${TState.toString()}'); + add(Reload()); + fetch(); + } + ) + ) + ) + ); + } + @override fromJson(Map json) => LoadableState(isLoading: true, data: fromStorage(json)); @override Map? toJson(LoadableState state) => state.data == null ? {} : state.data.toJson(); - Future loadState(); + Future gatherData(); TRepository repository(); TState fromNothing(); diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart index a4b3580..c38a1ba 100644 --- a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart @@ -1,7 +1,14 @@ +import '../../loadableState/loading_error.dart'; + class LoadableHydratedBlocEvent {} class Emit extends LoadableHydratedBlocEvent { final TState Function(TState state) state; final bool loading; Emit(this.state, {this.loading = false}); } -class ClearState extends LoadableHydratedBlocEvent {} \ No newline at end of file +class ClearState extends LoadableHydratedBlocEvent {} +class Error extends LoadableHydratedBlocEvent { + final LoadingError error; + Error(this.error); +} +class Reload extends LoadableHydratedBlocEvent {} diff --git a/lib/state/app/modules/app_modules.dart b/lib/state/app/modules/app_modules.dart index 28849dc..83b93b5 100644 --- a/lib/state/app/modules/app_modules.dart +++ b/lib/state/app/modules/app_modules.dart @@ -1,8 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; +import '../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart'; +import '../../../model/breakers/Breaker.dart'; import '../../../view/pages/files/files.dart'; +import '../../../view/pages/more/holidays/holidays.dart'; +import '../../../view/pages/more/roomplan/roomplan.dart'; import '../../../view/pages/talk/chatList.dart'; import '../../../view/pages/timetable/timetable.dart'; +import '../../../widget/centeredLeading.dart'; +import 'gradeAverages/view/grade_averages_view.dart'; +import 'marianumMessage/view/marianum_message_list_view.dart'; class AppModule { String name; @@ -11,14 +19,37 @@ class AppModule { AppModule(this.name, this.icon, this.create); - static Map modules() => { - Module.timetable: AppModule('Vertretung', Icons.calendar_month, Timetable.new), - Module.talk: AppModule('Talk', Icons.chat, ChatList.new), - Module.files: AppModule('Files', Icons.folder, Files.new), + static Map modules() => { + Modules.timetable: AppModule('Vertretung', Icons.calendar_month, Timetable.new), + Modules.talk: AppModule('Talk', Icons.chat, ChatList.new), + Modules.files: AppModule('Files', Icons.folder, Files.new), + Modules.marianumMessage: AppModule('Marianum Message', Icons.newspaper, MarianumMessageListView.new), + Modules.roomPlan: AppModule('Raumplan', Icons.location_pin, Roomplan.new), + Modules.gradeAveragesCalculator: AppModule('Notendurschnittsrechner', Icons.calculate, GradeAveragesView.new), + Modules.holidays: AppModule('Schulferien', Icons.holiday_village, Holidays.new), }; + + static AppModule getModule(Modules module) => modules()[module]!; + + Widget toListTile(BuildContext context) => ListTile( + leading: CenteredLeading(Icon(icon)), + title: Text(name), + onTap: () => pushScreen(context, withNavBar: false, screen: create()), + trailing: const Icon(Icons.arrow_right), + ); + + PersistentTabConfig toBottomTab(BuildContext context, {Widget Function(IconData icon)? itemBuilder}) => PersistentTabConfig( + screen: Breaker(breaker: BreakerArea.global, child: create()), + item: ItemConfig( + activeForegroundColor: Theme.of(context).primaryColor, + inactiveForegroundColor: Theme.of(context).colorScheme.secondary, + icon: itemBuilder == null ? Icon(icon) : itemBuilder(icon), + title: name + ), + ); } -enum Module { +enum Modules { timetable, talk, files, diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart index b960e9c..c2193f5 100644 --- a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart @@ -14,7 +14,7 @@ class MarianumMessageBloc extends LoadableHydratedBloc loadState() async { + Future gatherData() async { var messages = await repo.getMessages(); add(Emit((state) => state.copyWith(messageList: messages))); } diff --git a/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart index 4e0f188..7957329 100644 --- a/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart +++ b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart @@ -6,6 +6,7 @@ class MarianumMessageGetMessages extends DataLoader { @override Future fetch() async { await Future.delayed(const Duration(seconds: 3)); + throw UnimplementedError("Test"); return const MarianumMessageList(base: '', messages: [MarianumMessage(date: '', name: 'RepoTest', url: '')]); } } diff --git a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart index 1fddef3..aaf6c68 100644 --- a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart +++ b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -18,14 +16,10 @@ class MarianumMessageListView extends StatelessWidget { @override Widget build(BuildContext context) => BlocModule>( create: (context) => MarianumMessageBloc(), - child: (context, state) { - // if(value.primaryLoading()) return const LoadingSpinner(); - log(state.toString()); - return Scaffold( + child: (context, bloc, state) => Scaffold( appBar: AppBar( title: const Text('Marianum Message'), actions: [ - IconButton(onPressed: () => context.read().add(MessageEvent()), icon: Icon(Icons.abc)), IconButton(onPressed: () => context.read().add(Emit((state) => MarianumMessageState(messageList: MarianumMessageList(base: "", messages: [MarianumMessage(url: "", name: "Teeest", date: "now")])))), icon: Icon(Icons.add)), IconButton(onPressed: () => context.read().add(ClearState()), icon: Icon(Icons.add)) ], @@ -50,7 +44,6 @@ class MarianumMessageListView extends StatelessWidget { } ), ), - ); - } + ) ); } diff --git a/lib/view/pages/overhang.dart b/lib/view/pages/overhang.dart index 599c8f6..dcbe81c 100644 --- a/lib/view/pages/overhang.dart +++ b/lib/view/pages/overhang.dart @@ -6,15 +6,11 @@ import 'package:in_app_review/in_app_review.dart'; import '../../extensions/renderNotNull.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; -import '../../state/app/modules/gradeAverages/view/grade_averages_view.dart'; -import '../../state/app/modules/marianumMessage/view/marianum_message_list_view.dart'; -import '../../widget/ListItem.dart'; +import '../../state/app/modules/app_modules.dart'; import '../../widget/centeredLeading.dart'; import '../../widget/infoDialog.dart'; import '../settings/settings.dart'; import 'more/feedback/feedbackDialog.dart'; -import 'more/holidays/holidays.dart'; -import 'more/roomplan/roomplan.dart'; import 'more/share/selectShareTypeDialog.dart'; class Overhang extends StatelessWidget { @@ -30,11 +26,13 @@ class Overhang extends StatelessWidget { ), body: ListView( children: [ - const ListItemNavigator(icon: Icons.newspaper, text: 'Marianum Message', target: MarianumMessageListView()), - const ListItemNavigator(icon: Icons.room, text: 'Raumplan', target: Roomplan()), - const ListItemNavigator(icon: Icons.calculate, text: 'Notendurschnittsrechner', target: GradeAveragesView()), - const ListItemNavigator(icon: Icons.calendar_month, text: 'Schulferien', target: Holidays()), + AppModule.getModule(Modules.marianumMessage).toListTile(context), + AppModule.getModule(Modules.roomPlan).toListTile(context), + AppModule.getModule(Modules.gradeAveragesCalculator).toListTile(context), + AppModule.getModule(Modules.holidays).toListTile(context), + const Divider(), + ListTile( leading: const Icon(Icons.share_outlined), title: const Text('Teile die App'), diff --git a/lib/widget/ListItem.dart b/lib/widget/ListItem.dart deleted file mode 100644 index 09c81c9..0000000 --- a/lib/widget/ListItem.dart +++ /dev/null @@ -1,29 +0,0 @@ - -import 'package:flutter/material.dart'; -import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; - -class ListItemNavigator extends StatelessWidget { - const ListItemNavigator({super.key, required this.icon, required this.text, required this.target, this.onLongPress, this.arrow = true}); - - final IconData icon; - final String text; - final bool arrow; - - final Widget target; - - final GestureLongPressCallback? onLongPress; - - @override - Widget build(BuildContext context) { - onTabAction() => pushScreen(context, withNavBar: false, screen: target); //Navigator.push(context, MaterialPageRoute(builder: (context) => target)); - onLongPressAction() => onLongPress; - - return ListTile( - leading: Icon(icon), - title: Text(text), - trailing: arrow ? const Icon(Icons.arrow_right) : null, - onTap: onTabAction, - onLongPress: onLongPressAction, - ); - } -} From 181682a42474c0200fd1e74b91447f2bf55744ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 11 May 2024 17:52:53 +0200 Subject: [PATCH 11/19] moved reload actions out of error context --- .../bloc/loadable_state_bloc.dart | 13 ++--- .../loadableState/loadable_state.dart | 1 + .../loadableState/loadable_state.freezed.dart | 41 +++++++++++-- .../loadableState/loading_error.dart | 3 +- .../loadableState/loading_error.freezed.dart | 57 +++++++------------ .../view/loadable_state_consumer.dart | 33 ++++++----- .../view/loadable_state_error_screen.dart | 6 +- .../loadable_hydrated_bloc.dart | 37 ++++++------ .../loadable_hydrated_bloc_event.dart | 2 +- .../bloc/marianum_message_bloc.dart | 2 +- .../marianum_message_get_messages.dart | 2 +- .../view/marianum_message_list_view.dart | 4 +- lib/view/pages/overhang.dart | 4 +- lib/widget/conditional_wrapper.dart | 17 ++++++ 14 files changed, 129 insertions(+), 93 deletions(-) create mode 100644 lib/widget/conditional_wrapper.dart diff --git a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart index 2ca0717..1738a81 100644 --- a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart +++ b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart @@ -4,20 +4,19 @@ 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 { late StreamSubscription> _updateStream; - LoadingError? loadingError; + void Function()? reFetch; LoadableStateBloc() : super(const LoadableStateState(connections: null)) { on((event, emit) { emit(event.state); - if(connectivityStatusKnown() && isConnected() && loadingError != null) { - if(!loadingError!.enableRetry) return; - loadingError!.retry!(); + if(connectivityStatusKnown() && isConnected()) { + if(reFetch == null) return; + reFetch!(); } }); @@ -29,8 +28,8 @@ class LoadableStateBloc extends Bloc { bool connectivityStatusKnown() => state.connections != null; bool isConnected() => !(state.connections?.contains(ConnectivityResult.none) ?? true); - bool allowRetry() => loadingError?.retry != null; - bool showErrorMessage() => isConnected() && loadingError != null; + bool allowRetry() => reFetch != null; + bool showErrorMessage() => isConnected() && reFetch != null; IconData connectionIcon() => connectivityStatusKnown() ? isConnected() diff --git a/lib/state/app/infrastructure/loadableState/loadable_state.dart b/lib/state/app/infrastructure/loadableState/loadable_state.dart index 98c87c1..4a6c22a 100644 --- a/lib/state/app/infrastructure/loadableState/loadable_state.dart +++ b/lib/state/app/infrastructure/loadableState/loadable_state.dart @@ -11,6 +11,7 @@ class LoadableState with _$LoadableState { const factory LoadableState({ @Default(true) bool isLoading, @Default(null) TState? data, + @Default(null) void Function()? reFetch, @Default(null) LoadingError? error, }) = _LoadableState; diff --git a/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart b/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart index 3b805ac..03ac3a3 100644 --- a/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart +++ b/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart @@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$LoadableState { bool get isLoading => throw _privateConstructorUsedError; TState? get data => throw _privateConstructorUsedError; + void Function()? get reFetch => throw _privateConstructorUsedError; LoadingError? get error => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -31,7 +32,11 @@ abstract class $LoadableStateCopyWith { $Res Function(LoadableState) then) = _$LoadableStateCopyWithImpl>; @useResult - $Res call({bool isLoading, TState? data, LoadingError? error}); + $Res call( + {bool isLoading, + TState? data, + void Function()? reFetch, + LoadingError? error}); $LoadingErrorCopyWith<$Res>? get error; } @@ -52,6 +57,7 @@ class _$LoadableStateCopyWithImpl __$$LoadableStateImplCopyWithImpl; @override @useResult - $Res call({bool isLoading, TState? data, LoadingError? error}); + $Res call( + {bool isLoading, + TState? data, + void Function()? reFetch, + LoadingError? error}); @override $LoadingErrorCopyWith<$Res>? get error; @@ -111,6 +125,7 @@ class __$$LoadableStateImplCopyWithImpl $Res call({ Object? isLoading = null, Object? data = freezed, + Object? reFetch = freezed, Object? error = freezed, }) { return _then(_$LoadableStateImpl( @@ -122,6 +137,10 @@ class __$$LoadableStateImplCopyWithImpl ? _value.data : data // ignore: cast_nullable_to_non_nullable as TState?, + reFetch: freezed == reFetch + ? _value.reFetch + : reFetch // ignore: cast_nullable_to_non_nullable + as void Function()?, error: freezed == error ? _value.error : error // ignore: cast_nullable_to_non_nullable @@ -134,7 +153,10 @@ class __$$LoadableStateImplCopyWithImpl class _$LoadableStateImpl extends _LoadableState { const _$LoadableStateImpl( - {this.isLoading = true, this.data = null, this.error = null}) + {this.isLoading = true, + this.data = null, + this.reFetch = null, + this.error = null}) : super._(); @override @@ -145,11 +167,14 @@ class _$LoadableStateImpl extends _LoadableState { final TState? data; @override @JsonKey() + final void Function()? reFetch; + @override + @JsonKey() final LoadingError? error; @override String toString() { - return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, error: $error)'; + return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, reFetch: $reFetch, error: $error)'; } @override @@ -160,12 +185,13 @@ class _$LoadableStateImpl extends _LoadableState { (identical(other.isLoading, isLoading) || other.isLoading == isLoading) && const DeepCollectionEquality().equals(other.data, data) && + (identical(other.reFetch, reFetch) || other.reFetch == reFetch) && (identical(other.error, error) || other.error == error)); } @override - int get hashCode => Object.hash( - runtimeType, isLoading, const DeepCollectionEquality().hash(data), error); + int get hashCode => Object.hash(runtimeType, isLoading, + const DeepCollectionEquality().hash(data), reFetch, error); @JsonKey(ignore: true) @override @@ -179,6 +205,7 @@ abstract class _LoadableState extends LoadableState { const factory _LoadableState( {final bool isLoading, final TState? data, + final void Function()? reFetch, final LoadingError? error}) = _$LoadableStateImpl; const _LoadableState._() : super._(); @@ -187,6 +214,8 @@ abstract class _LoadableState extends LoadableState { @override TState? get data; @override + void Function()? get reFetch; + @override LoadingError? get error; @override @JsonKey(ignore: true) diff --git a/lib/state/app/infrastructure/loadableState/loading_error.dart b/lib/state/app/infrastructure/loadableState/loading_error.dart index 909f6d7..b68ac7d 100644 --- a/lib/state/app/infrastructure/loadableState/loading_error.dart +++ b/lib/state/app/infrastructure/loadableState/loading_error.dart @@ -6,7 +6,6 @@ part 'loading_error.freezed.dart'; class LoadingError with _$LoadingError { const factory LoadingError({ required String message, - @Default(false) bool enableRetry, - @Default(null) void Function()? retry, + @Default(false) bool allowRetry, }) = _LoadingError; } diff --git a/lib/state/app/infrastructure/loadableState/loading_error.freezed.dart b/lib/state/app/infrastructure/loadableState/loading_error.freezed.dart index 2570c98..6515a46 100644 --- a/lib/state/app/infrastructure/loadableState/loading_error.freezed.dart +++ b/lib/state/app/infrastructure/loadableState/loading_error.freezed.dart @@ -17,8 +17,7 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$LoadingError { String get message => throw _privateConstructorUsedError; - bool get enableRetry => throw _privateConstructorUsedError; - void Function()? get retry => throw _privateConstructorUsedError; + bool get allowRetry => throw _privateConstructorUsedError; @JsonKey(ignore: true) $LoadingErrorCopyWith get copyWith => @@ -31,7 +30,7 @@ abstract class $LoadingErrorCopyWith<$Res> { LoadingError value, $Res Function(LoadingError) then) = _$LoadingErrorCopyWithImpl<$Res, LoadingError>; @useResult - $Res call({String message, bool enableRetry, void Function()? retry}); + $Res call({String message, bool allowRetry}); } /// @nodoc @@ -48,22 +47,17 @@ class _$LoadingErrorCopyWithImpl<$Res, $Val extends LoadingError> @override $Res call({ Object? message = null, - Object? enableRetry = null, - Object? retry = freezed, + Object? allowRetry = null, }) { 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 + allowRetry: null == allowRetry + ? _value.allowRetry + : allowRetry // 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); } } @@ -76,7 +70,7 @@ abstract class _$$LoadingErrorImplCopyWith<$Res> __$$LoadingErrorImplCopyWithImpl<$Res>; @override @useResult - $Res call({String message, bool enableRetry, void Function()? retry}); + $Res call({String message, bool allowRetry}); } /// @nodoc @@ -91,22 +85,17 @@ class __$$LoadingErrorImplCopyWithImpl<$Res> @override $Res call({ Object? message = null, - Object? enableRetry = null, - Object? retry = freezed, + Object? allowRetry = null, }) { 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 + allowRetry: null == allowRetry + ? _value.allowRetry + : allowRetry // ignore: cast_nullable_to_non_nullable as bool, - retry: freezed == retry - ? _value.retry - : retry // ignore: cast_nullable_to_non_nullable - as void Function()?, )); } } @@ -114,21 +103,17 @@ class __$$LoadingErrorImplCopyWithImpl<$Res> /// @nodoc class _$LoadingErrorImpl implements _LoadingError { - const _$LoadingErrorImpl( - {required this.message, this.enableRetry = false, this.retry = null}); + const _$LoadingErrorImpl({required this.message, this.allowRetry = false}); @override final String message; @override @JsonKey() - final bool enableRetry; - @override - @JsonKey() - final void Function()? retry; + final bool allowRetry; @override String toString() { - return 'LoadingError(message: $message, enableRetry: $enableRetry, retry: $retry)'; + return 'LoadingError(message: $message, allowRetry: $allowRetry)'; } @override @@ -137,13 +122,12 @@ class _$LoadingErrorImpl implements _LoadingError { (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)); + (identical(other.allowRetry, allowRetry) || + other.allowRetry == allowRetry)); } @override - int get hashCode => Object.hash(runtimeType, message, enableRetry, retry); + int get hashCode => Object.hash(runtimeType, message, allowRetry); @JsonKey(ignore: true) @override @@ -155,15 +139,12 @@ class _$LoadingErrorImpl implements _LoadingError { abstract class _LoadingError implements LoadingError { const factory _LoadingError( {required final String message, - final bool enableRetry, - final void Function()? retry}) = _$LoadingErrorImpl; + final bool allowRetry}) = _$LoadingErrorImpl; @override String get message; @override - bool get enableRetry; - @override - void Function()? get retry; + bool get allowRetry; @override @JsonKey(ignore: true) _$$LoadingErrorImplCopyWith<_$LoadingErrorImpl> get copyWith => diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart index c895b7c..3313846 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart @@ -2,6 +2,7 @@ import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../../../../../widget/conditional_wrapper.dart'; import '../../utilityWidgets/bloc_module.dart'; import '../../utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; import '../bloc/loadable_state_bloc.dart'; @@ -14,36 +15,42 @@ import 'loadable_state_primary_loading.dart'; class LoadableStateConsumer, LoadableState>, TState> extends StatelessWidget { final Widget Function(TState state, bool loading) child; - const LoadableStateConsumer({required this.child, super.key}); + final bool wrapWithScrollView; + const LoadableStateConsumer({required this.child, this.wrapWithScrollView = false, super.key}); static Duration animationDuration = const Duration(milliseconds: 200); @override Widget build(BuildContext context) { var loadableState = context.watch().state; - var childWidget = RefreshIndicator( - onRefresh: () { - loadableState.error != null && loadableState.error!.retry != null - ? loadableState.error!.retry!() - : null; - return Future.value(); - }, + var childWidget = ConditionalWrapper( + condition: loadableState.reFetch != null, + wrapper: (child) => RefreshIndicator( + onRefresh: () { + if(loadableState.reFetch != null) loadableState.reFetch!(); + return Future.value(); + }, + child: ConditionalWrapper( + condition: wrapWithScrollView, + wrapper: (child) => SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: child + ), + child: child, + ) + ), child: SizedBox( height: MediaQuery.of(context).size.height, child: loadableState.showContent() ? child(loadableState.data, loadableState.isLoading) : const SizedBox.shrink(), ), - // SingleChildScrollView( // TODO not all childs are reloadable - // physics: const AlwaysScrollableScrollPhysics(), - // child: - // ), ); return BlocModule( create: (context) => LoadableStateBloc(), child: (context, bloc, state) { - bloc.loadingError = loadableState.error; + bloc.reFetch = loadableState.reFetch; return Column( children: [ LoadableStateErrorBar(visible: loadableState.showErrorBar()), diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart index 0737d28..3ed7d4c 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart @@ -27,16 +27,16 @@ class LoadableStateErrorScreen extends StatelessWidget { if(bloc.allowRetry()) ...[ const SizedBox(height: 10), - TextButton(onPressed: () => bloc.loadingError!.retry!(), child: const Text('Erneut versuschen')), + TextButton(onPressed: () => bloc.reFetch!(), 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)) + Text("bloc.loadingError!.message", style: TextStyle(color: Theme.of(context).hintColor, fontSize: 12)) ], ], ), ), ); } -} \ No newline at end of file +} diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart index 7751907..4a55906 100644 --- a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart @@ -17,10 +17,10 @@ abstract class LoadableHydratedBloc< > { late TRepository _repository; LoadableHydratedBloc() : super(const LoadableState()) { - on>((event, emit) => emit(LoadableState(isLoading: event.loading, data: event.state(innerState ?? fromNothing())))); - on>((event, emit) => emit(LoadableState(isLoading: true, data: innerState))); + on>((event, emit) => emit(LoadableState(isLoading: event.loading, data: event.state(innerState ?? fromNothing()), reFetch: retry))); + on>((event, emit) => emit(LoadableState(isLoading: true, data: innerState))); on>((event, emit) => emit(const LoadableState())); - on>((event, emit) => emit(LoadableState(isLoading: false, data: innerState, error: event.error))); + on>((event, emit) => emit(LoadableState(isLoading: false, data: innerState, reFetch: retry, error: event.error))); _repository = repository(); fetch(); @@ -29,22 +29,25 @@ abstract class LoadableHydratedBloc< TState? get innerState => state.data; TRepository get repo => _repository; + void retry() { + log('Fetch retry triggered for ${TState.toString()}'); + add(RefetchStarted()); + fetch(); + } + void fetch() { + log('Fetching data for ${TState.toString()}'); gatherData().catchError( - (e) => add( - Error( - LoadingError( - message: e.toString(), - enableRetry: true, - retry: () { - log('Fetch retry on ${TState.toString()}'); - add(Reload()); - fetch(); - } - ) - ) - ) - ); + (e) { + log('Error while fetching ${TState.toString()}: ${e.toString()}'); + add(Error(LoadingError( + message: e.toString(), + allowRetry: true, + ))); + } + ).then((value) { + log('Fetch for ${TState.toString()} completed!'); + }); } @override diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart index c38a1ba..06462de 100644 --- a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart @@ -11,4 +11,4 @@ class Error extends LoadableHydratedBlocEvent { final LoadingError error; Error(this.error); } -class Reload extends LoadableHydratedBlocEvent {} +class RefetchStarted extends LoadableHydratedBlocEvent {} diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart index c2193f5..375dae0 100644 --- a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart @@ -23,7 +23,7 @@ class MarianumMessageBloc extends LoadableHydratedBloc MarianumMessageRepository(); @override - MarianumMessageState fromNothing() => const MarianumMessageState(messageList: MarianumMessageList(base: "", messages: [])); + MarianumMessageState fromNothing() => const MarianumMessageState(messageList: MarianumMessageList(base: '', messages: [])); @override MarianumMessageState fromStorage(Map json) => MarianumMessageState.fromJson(json); diff --git a/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart index 7957329..1a7d8f6 100644 --- a/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart +++ b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart @@ -6,7 +6,7 @@ class MarianumMessageGetMessages extends DataLoader { @override Future fetch() async { await Future.delayed(const Duration(seconds: 3)); - throw UnimplementedError("Test"); + // throw UnimplementedError("Test"); return const MarianumMessageList(base: '', messages: [MarianumMessage(date: '', name: 'RepoTest', url: '')]); } } diff --git a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart index aaf6c68..a56bc36 100644 --- a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart +++ b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart @@ -20,8 +20,8 @@ class MarianumMessageListView extends StatelessWidget { appBar: AppBar( title: const Text('Marianum Message'), actions: [ - IconButton(onPressed: () => context.read().add(Emit((state) => MarianumMessageState(messageList: MarianumMessageList(base: "", messages: [MarianumMessage(url: "", name: "Teeest", date: "now")])))), icon: Icon(Icons.add)), - IconButton(onPressed: () => context.read().add(ClearState()), icon: Icon(Icons.add)) + IconButton(onPressed: () => context.read().add(Emit((state) => const MarianumMessageState(messageList: MarianumMessageList(base: '', messages: [MarianumMessage(url: '', name: 'Teeest', date: 'now')])))), icon: const Icon(Icons.add)), + IconButton(onPressed: () => context.read().add(ClearState()), icon: const Icon(Icons.add)) ], ), body: LoadableStateConsumer( diff --git a/lib/view/pages/overhang.dart b/lib/view/pages/overhang.dart index dcbe81c..08a5f0f 100644 --- a/lib/view/pages/overhang.dart +++ b/lib/view/pages/overhang.dart @@ -72,8 +72,8 @@ class Overhang extends StatelessWidget { trailing: const Icon(Icons.arrow_right), onTap: () => pushScreen(context, withNavBar: false, screen: const FeedbackDialog()), ), - ListTile( - leading: const Icon(Icons.science_outlined), + const ListTile( + leading: Icon(Icons.science_outlined), // onTap: () => pushScreen(context, withNavBar: false, screen: const GradeAveragesScreen()), ) ], diff --git a/lib/widget/conditional_wrapper.dart b/lib/widget/conditional_wrapper.dart new file mode 100644 index 0000000..5526cf3 --- /dev/null +++ b/lib/widget/conditional_wrapper.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class ConditionalWrapper extends StatelessWidget { + final bool condition; + final Widget child; + final Widget Function(Widget child) wrapper; + + const ConditionalWrapper({ + required this.condition, + required this.child, + required this.wrapper, + super.key, + }); + + @override + Widget build(BuildContext context) => condition ? wrapper(child) : child; +} From e57a1a915e7d3f8889f591d05b43c51d51fdb86b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 11 May 2024 19:29:12 +0200 Subject: [PATCH 12/19] implemented marianum message dataloader --- .../mhsl/message/getMessages/getMessages.dart | 3 -- .../dataLoader/data_loader.dart | 35 ++++++++++++++++++- .../dataLoader/mhsl_data_loader.dart | 9 +++++ .../marianum_message_get_messages.dart | 14 ++++---- .../marianum_message_repository.dart | 2 +- .../view/marianum_message_list_view.dart | 8 +---- pubspec.yaml | 1 + 7 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart diff --git a/lib/api/mhsl/message/getMessages/getMessages.dart b/lib/api/mhsl/message/getMessages/getMessages.dart index 32a5734..34bfc6b 100644 --- a/lib/api/mhsl/message/getMessages/getMessages.dart +++ b/lib/api/mhsl/message/getMessages/getMessages.dart @@ -7,11 +7,8 @@ import 'getMessagesResponse.dart'; class GetMessages extends MhslApi { GetMessages() : super('message/messages.json'); - - @override GetMessagesResponse assemble(String raw) => GetMessagesResponse.fromJson(jsonDecode(raw)); - @override Future request(Uri uri) => http.get(uri); } diff --git a/lib/state/app/infrastructure/dataLoader/data_loader.dart b/lib/state/app/infrastructure/dataLoader/data_loader.dart index 47306ba..c6b826b 100644 --- a/lib/state/app/infrastructure/dataLoader/data_loader.dart +++ b/lib/state/app/infrastructure/dataLoader/data_loader.dart @@ -1,3 +1,36 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:dio/dio.dart'; + abstract class DataLoader { - Future fetch(); + final Dio dio; + DataLoader(this.dio) { + dio.options.connectTimeout = const Duration(seconds: 10).inMilliseconds; + dio.options.sendTimeout = const Duration(seconds: 30).inMilliseconds; + dio.options.receiveTimeout = const Duration(seconds: 30).inMilliseconds; + } + + Future run() async { + var response = await fetch(); + try { + return assemble(DataLoaderResult( + json: jsonDecode(response.data!), + headers: response.headers.map.map((key, value) => MapEntry(key, value.join(';'))), + )); + } catch(trace, e) { + log(trace.toString()); + throw(e); + } + } + + Future> fetch(); + TResult assemble(DataLoaderResult data); +} + +class DataLoaderResult { + final Map json; + final Map headers; + + DataLoaderResult({required this.json, required this.headers}); } diff --git a/lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart b/lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart new file mode 100644 index 0000000..060e37d --- /dev/null +++ b/lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart @@ -0,0 +1,9 @@ +import 'package:dio/dio.dart'; + +import 'data_loader.dart'; + +abstract class MhslDataLoader extends DataLoader { + MhslDataLoader() : super(Dio(BaseOptions( + baseUrl: 'https://mhsl.eu/marianum/marianummobile/' + ))); +} diff --git a/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart index 1a7d8f6..cb7f9db 100644 --- a/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart +++ b/lib/state/app/modules/marianumMessage/dataProvider/marianum_message_get_messages.dart @@ -1,12 +1,12 @@ +import 'package:dio/dio.dart'; + import '../../../infrastructure/dataLoader/data_loader.dart'; +import '../../../infrastructure/dataLoader/mhsl_data_loader.dart'; import '../bloc/marianum_message_state.dart'; -class MarianumMessageGetMessages extends DataLoader { - +class MarianumMessageGetMessages extends MhslDataLoader { @override - Future fetch() async { - await Future.delayed(const Duration(seconds: 3)); - // throw UnimplementedError("Test"); - return const MarianumMessageList(base: '', messages: [MarianumMessage(date: '', name: 'RepoTest', url: '')]); - } + Future> fetch() async => dio.get('/message/messages.json'); + @override + MarianumMessageList assemble(DataLoaderResult data) => MarianumMessageList.fromJson(data.json); } diff --git a/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart b/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart index 033f821..3148b92 100644 --- a/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart +++ b/lib/state/app/modules/marianumMessage/repository/marianum_message_repository.dart @@ -3,5 +3,5 @@ import '../bloc/marianum_message_state.dart'; import '../dataProvider/marianum_message_get_messages.dart'; class MarianumMessageRepository extends Repository { - Future getMessages() => MarianumMessageGetMessages().fetch(); + Future getMessages() => MarianumMessageGetMessages().run(); } diff --git a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart index a56bc36..bab6a1d 100644 --- a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart +++ b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart @@ -1,12 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import '../../../../../api/mhsl/message/getMessages/getMessagesResponse.dart'; import '../../../../../view/pages/more/message/messageView.dart'; import '../../../infrastructure/loadableState/loadable_state.dart'; import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart'; import '../../../infrastructure/utilityWidgets/bloc_module.dart'; -import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; import '../bloc/marianum_message_bloc.dart'; import '../bloc/marianum_message_state.dart'; @@ -19,10 +17,6 @@ class MarianumMessageListView extends StatelessWidget { child: (context, bloc, state) => Scaffold( appBar: AppBar( title: const Text('Marianum Message'), - actions: [ - IconButton(onPressed: () => context.read().add(Emit((state) => const MarianumMessageState(messageList: MarianumMessageList(base: '', messages: [MarianumMessage(url: '', name: 'Teeest', date: 'now')])))), icon: const Icon(Icons.add)), - IconButton(onPressed: () => context.read().add(ClearState()), icon: const Icon(Icons.add)) - ], ), body: LoadableStateConsumer( child: (state, loading) => ListView.builder( @@ -34,7 +28,7 @@ class MarianumMessageListView extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.newspaper)], ), - title: Text("${message.name}${loading ? "loading" : "notloading"}", overflow: TextOverflow.ellipsis), + title: Text(message.name, overflow: TextOverflow.ellipsis), subtitle: Text('vom ${message.date}'), trailing: const Icon(Icons.arrow_right), onTap: () { diff --git a/pubspec.yaml b/pubspec.yaml index 61599a4..c74885e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -103,6 +103,7 @@ dependencies: freezed_annotation: ^2.4.1 connectivity_plus: ^6.0.3 hydrated_bloc: ^9.1.5 + dio: ^4.0.6 dev_dependencies: flutter_test: From 2056be23cd977d60cc8b348477df7b706ec698ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 12 May 2024 00:31:23 +0200 Subject: [PATCH 13/19] added minimum duration of loading animation --- lib/state/app/infrastructure/dataLoader/data_loader.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/state/app/infrastructure/dataLoader/data_loader.dart b/lib/state/app/infrastructure/dataLoader/data_loader.dart index c6b826b..cb4a3fe 100644 --- a/lib/state/app/infrastructure/dataLoader/data_loader.dart +++ b/lib/state/app/infrastructure/dataLoader/data_loader.dart @@ -12,7 +12,13 @@ abstract class DataLoader { } Future run() async { - var response = await fetch(); + var fetcher = fetch(); + await Future.wait([ + fetcher, + Future.delayed(const Duration(milliseconds: 500)) // TODO tune or remove + ]); + + var response = await fetcher; try { return assemble(DataLoaderResult( json: jsonDecode(response.data!), From 3281b134e06fe9696d0b7d66c2b5a429a6b100c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 12 May 2024 00:36:24 +0200 Subject: [PATCH 14/19] moved message pdf view --- .../view/marianum_message_list_view.dart | 5 +- .../view/marianum_message_view.dart} | 6 +- lib/view/pages/more/message/message.dart | 59 ------------------- 3 files changed, 5 insertions(+), 65 deletions(-) rename lib/{view/pages/more/message/messageView.dart => state/app/modules/marianumMessage/view/marianum_message_view.dart} (91%) delete mode 100644 lib/view/pages/more/message/message.dart diff --git a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart index bab6a1d..1957886 100644 --- a/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart +++ b/lib/state/app/modules/marianumMessage/view/marianum_message_list_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import '../../../../../api/mhsl/message/getMessages/getMessagesResponse.dart'; -import '../../../../../view/pages/more/message/messageView.dart'; +import 'marianum_message_view.dart'; import '../../../infrastructure/loadableState/loadable_state.dart'; import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart'; import '../../../infrastructure/utilityWidgets/bloc_module.dart'; @@ -32,7 +31,7 @@ class MarianumMessageListView extends StatelessWidget { subtitle: Text('vom ${message.date}'), trailing: const Icon(Icons.arrow_right), onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: state.messageList.base, message: message as GetMessagesResponseObject))); + Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: state.messageList.base, message: message))); }, ); } diff --git a/lib/view/pages/more/message/messageView.dart b/lib/state/app/modules/marianumMessage/view/marianum_message_view.dart similarity index 91% rename from lib/view/pages/more/message/messageView.dart rename to lib/state/app/modules/marianumMessage/view/marianum_message_view.dart index 791a2ed..ae8fd36 100644 --- a/lib/view/pages/more/message/messageView.dart +++ b/lib/state/app/modules/marianumMessage/view/marianum_message_view.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../../api/mhsl/message/getMessages/getMessagesResponse.dart'; -import '../../../../widget/confirmDialog.dart'; +import '../bloc/marianum_message_state.dart'; +import '../../../../../widget/confirmDialog.dart'; class MessageView extends StatefulWidget { final String basePath; - final GetMessagesResponseObject message; + final MarianumMessage message; const MessageView({super.key, required this.basePath, required this.message}); @override diff --git a/lib/view/pages/more/message/message.dart b/lib/view/pages/more/message/message.dart deleted file mode 100644 index d9be17f..0000000 --- a/lib/view/pages/more/message/message.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../../../../model/message/messageProps.dart'; -import '../../../../widget/loadingSpinner.dart'; -import 'messageView.dart'; - - -class Message extends StatefulWidget { - const Message({super.key}); - - @override - State createState() => _MessageState(); -} - -class _MessageState extends State { - @override - void initState() { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - Provider.of(context, listen: false).run(); - }); - super.initState(); - } - - @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Marianum Message'), - ), - body: Consumer(builder: (context, value, child) { - if(value.primaryLoading()) return const LoadingSpinner(); - - return RefreshIndicator( - child: ListView.builder( - itemCount: value.getMessagesResponse.messages.length, - itemBuilder: (context, index) { - var message = value.getMessagesResponse.messages.toList()[index]; - return ListTile( - leading: const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [Icon(Icons.newspaper)], - ), - title: Text(message.name, overflow: TextOverflow.ellipsis), - subtitle: Text('vom ${message.date}'), - trailing: const Icon(Icons.arrow_right), - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: value.getMessagesResponse.base, message: message))); - }, - ); - } - ), - onRefresh: () { - Provider.of(context, listen: false).run(renew: true); - return Future.delayed(const Duration(seconds: 3)); - }, - ); - }), - ); -} From ebbb70dc964f2778b9b0faf6362b09eb3430d4cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 12 May 2024 02:39:35 +0200 Subject: [PATCH 15/19] added timestamp to bloc cache, showing age in offline mode --- .../app/base/account/account_controller.dart | 0 .../dataLoader/mhsl_data_loader.dart | 2 +- .../bloc/loadable_state_bloc.dart | 6 +- .../loadableState/loadable_state.dart | 1 + .../loadableState/loadable_state.freezed.dart | 26 ++- .../view/loadable_state_consumer.dart | 4 +- .../view/loadable_state_error_bar.dart | 41 +++-- .../view/loadable_state_error_screen.dart | 19 ++- .../loadable_hydrated_bloc.dart | 41 ++++- .../loadable_save_context.dart | 24 +++ .../loadable_save_context.freezed.dart | 155 ++++++++++++++++++ .../loadable_save_context.g.dart | 19 +++ lib/view/pages/overhang.dart | 4 - 13 files changed, 302 insertions(+), 40 deletions(-) delete mode 100644 lib/state/app/base/account/account_controller.dart create mode 100644 lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.dart create mode 100644 lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.freezed.dart create mode 100644 lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.g.dart diff --git a/lib/state/app/base/account/account_controller.dart b/lib/state/app/base/account/account_controller.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart b/lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart index 060e37d..0825587 100644 --- a/lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart +++ b/lib/state/app/infrastructure/dataLoader/mhsl_data_loader.dart @@ -4,6 +4,6 @@ import 'data_loader.dart'; abstract class MhslDataLoader extends DataLoader { MhslDataLoader() : super(Dio(BaseOptions( - baseUrl: 'https://mhsl.eu/marianum/marianummobile/' + baseUrl: 'https://mhsl.eu/marianum/marianummobile/' ))); } diff --git a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart index 1738a81..89998bb 100644 --- a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart +++ b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; import 'loadable_state_event.dart'; import 'loadable_state_state.dart'; @@ -29,7 +30,6 @@ class LoadableStateBloc extends Bloc { bool connectivityStatusKnown() => state.connections != null; bool isConnected() => !(state.connections?.contains(ConnectivityResult.none) ?? true); bool allowRetry() => reFetch != null; - bool showErrorMessage() => isConnected() && reFetch != null; IconData connectionIcon() => connectivityStatusKnown() ? isConnected() @@ -37,10 +37,10 @@ class LoadableStateBloc extends Bloc { : Icons.signal_wifi_connected_no_internet_4 : Icons.device_unknown; - String connectionText() => connectivityStatusKnown() + String connectionText({int? lastUpdated}) => connectivityStatusKnown() ? isConnected() ? 'Verbindung fehlgeschlagen' - : 'Offline' + : 'Offline${lastUpdated == null ? '' : ' - Stand von ${Jiffy.parseFromMillisecondsSinceEpoch(lastUpdated).fromNow()}'}' : 'Unbekannte Fehlerursache'; @override diff --git a/lib/state/app/infrastructure/loadableState/loadable_state.dart b/lib/state/app/infrastructure/loadableState/loadable_state.dart index 4a6c22a..ac1626a 100644 --- a/lib/state/app/infrastructure/loadableState/loadable_state.dart +++ b/lib/state/app/infrastructure/loadableState/loadable_state.dart @@ -11,6 +11,7 @@ class LoadableState with _$LoadableState { const factory LoadableState({ @Default(true) bool isLoading, @Default(null) TState? data, + @Default(null) int? lastFetch, @Default(null) void Function()? reFetch, @Default(null) LoadingError? error, }) = _LoadableState; diff --git a/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart b/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart index 03ac3a3..e66b4a8 100644 --- a/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart +++ b/lib/state/app/infrastructure/loadableState/loadable_state.freezed.dart @@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$LoadableState { bool get isLoading => throw _privateConstructorUsedError; TState? get data => throw _privateConstructorUsedError; + int? get lastFetch => throw _privateConstructorUsedError; void Function()? get reFetch => throw _privateConstructorUsedError; LoadingError? get error => throw _privateConstructorUsedError; @@ -35,6 +36,7 @@ abstract class $LoadableStateCopyWith { $Res call( {bool isLoading, TState? data, + int? lastFetch, void Function()? reFetch, LoadingError? error}); @@ -57,6 +59,7 @@ class _$LoadableStateCopyWithImpl $Res call( {bool isLoading, TState? data, + int? lastFetch, void Function()? reFetch, LoadingError? error}); @@ -125,6 +133,7 @@ class __$$LoadableStateImplCopyWithImpl $Res call({ Object? isLoading = null, Object? data = freezed, + Object? lastFetch = freezed, Object? reFetch = freezed, Object? error = freezed, }) { @@ -137,6 +146,10 @@ class __$$LoadableStateImplCopyWithImpl ? _value.data : data // ignore: cast_nullable_to_non_nullable as TState?, + lastFetch: freezed == lastFetch + ? _value.lastFetch + : lastFetch // ignore: cast_nullable_to_non_nullable + as int?, reFetch: freezed == reFetch ? _value.reFetch : reFetch // ignore: cast_nullable_to_non_nullable @@ -155,6 +168,7 @@ class _$LoadableStateImpl extends _LoadableState { const _$LoadableStateImpl( {this.isLoading = true, this.data = null, + this.lastFetch = null, this.reFetch = null, this.error = null}) : super._(); @@ -167,6 +181,9 @@ class _$LoadableStateImpl extends _LoadableState { final TState? data; @override @JsonKey() + final int? lastFetch; + @override + @JsonKey() final void Function()? reFetch; @override @JsonKey() @@ -174,7 +191,7 @@ class _$LoadableStateImpl extends _LoadableState { @override String toString() { - return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, reFetch: $reFetch, error: $error)'; + return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error)'; } @override @@ -185,13 +202,15 @@ class _$LoadableStateImpl extends _LoadableState { (identical(other.isLoading, isLoading) || other.isLoading == isLoading) && const DeepCollectionEquality().equals(other.data, data) && + (identical(other.lastFetch, lastFetch) || + other.lastFetch == lastFetch) && (identical(other.reFetch, reFetch) || other.reFetch == reFetch) && (identical(other.error, error) || other.error == error)); } @override int get hashCode => Object.hash(runtimeType, isLoading, - const DeepCollectionEquality().hash(data), reFetch, error); + const DeepCollectionEquality().hash(data), lastFetch, reFetch, error); @JsonKey(ignore: true) @override @@ -205,6 +224,7 @@ abstract class _LoadableState extends LoadableState { const factory _LoadableState( {final bool isLoading, final TState? data, + final int? lastFetch, final void Function()? reFetch, final LoadingError? error}) = _$LoadableStateImpl; const _LoadableState._() : super._(); @@ -214,6 +234,8 @@ abstract class _LoadableState extends LoadableState { @override TState? get data; @override + int? get lastFetch; + @override void Function()? get reFetch; @override LoadingError? get error; diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart index 3313846..7d9244e 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_consumer.dart @@ -53,13 +53,13 @@ class LoadableStateConsumer(); var status = ( - icon: bloc.connectionIcon(), - text: bloc.connectionText(), - color: bloc.connectivityStatusKnown() && !bloc.isConnected() + icon: bloc.connectionIcon(), + text: bloc.connectionText(lastUpdated: lastUpdated), + 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 InkWell( + onTap: () { + if(!bloc.isConnected()) return; + InfoDialog.show(context, 'Exception: ${message.toString()}'); + }, + child: Container( + height: 20, + decoration: BoxDecoration( + color: status.color, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(status.icon, size: 14), + const SizedBox(width: 10), + Text(status.text, style: const TextStyle(fontSize: 12)) + ], + ), ), ); }, diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart index 3ed7d4c..cb68e76 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_screen.dart @@ -6,7 +6,8 @@ import 'loadable_state_consumer.dart'; class LoadableStateErrorScreen extends StatelessWidget { final bool visible; - const LoadableStateErrorScreen({required this.visible, super.key}); + final String? message; + const LoadableStateErrorScreen({required this.visible, this.message, super.key}); @override @@ -28,11 +29,19 @@ class LoadableStateErrorScreen extends StatelessWidget { if(bloc.allowRetry()) ...[ const SizedBox(height: 10), TextButton(onPressed: () => bloc.reFetch!(), 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)) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + message ?? 'Task failed successfully :)', + style: TextStyle( + color: Theme.of(context).hintColor, + fontSize: 12 + ), + maxLines: 10, + overflow: TextOverflow.ellipsis, + ), + ), ], ], ), diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart index 4a55906..116bad2 100644 --- a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart @@ -6,6 +6,7 @@ import '../../loadableState/loading_error.dart'; import '../../repository/repository.dart'; import 'loadable_hydrated_bloc_event.dart'; import '../../loadableState/loadable_state.dart'; +import 'loadable_save_context.dart'; abstract class LoadableHydratedBloc< TEvent extends LoadableHydratedBlocEvent, @@ -17,10 +18,29 @@ abstract class LoadableHydratedBloc< > { late TRepository _repository; LoadableHydratedBloc() : super(const LoadableState()) { - on>((event, emit) => emit(LoadableState(isLoading: event.loading, data: event.state(innerState ?? fromNothing()), reFetch: retry))); - on>((event, emit) => emit(LoadableState(isLoading: true, data: innerState))); + + on>((event, emit) => emit(LoadableState( + isLoading: event.loading, + data: event.state(innerState ?? fromNothing()), + lastFetch: DateTime.now().millisecondsSinceEpoch, + reFetch: retry + ))); + + on>((event, emit) => emit(LoadableState( + isLoading: true, + data: innerState, + lastFetch: state.lastFetch + ))); + on>((event, emit) => emit(const LoadableState())); - on>((event, emit) => emit(LoadableState(isLoading: false, data: innerState, reFetch: retry, error: event.error))); + + on>((event, emit) => emit(LoadableState( + isLoading: false, + data: innerState, + lastFetch: state.lastFetch, + reFetch: retry, + error: event.error + ))); _repository = repository(); fetch(); @@ -41,19 +61,26 @@ abstract class LoadableHydratedBloc< (e) { log('Error while fetching ${TState.toString()}: ${e.toString()}'); add(Error(LoadingError( - message: e.toString(), + message: e.message, allowRetry: true, ))); - } + }, ).then((value) { log('Fetch for ${TState.toString()} completed!'); }); } @override - fromJson(Map json) => LoadableState(isLoading: true, data: fromStorage(json)); + fromJson(Map json) { + var rawData = LoadableSaveContext.unwrap(json); + return LoadableState(isLoading: true, lastFetch: rawData.meta.timestamp, data: fromStorage(rawData.data)); + } + @override - Map? toJson(LoadableState state) => state.data == null ? {} : state.data.toJson(); + Map? toJson(LoadableState state) => LoadableSaveContext.wrap( + toStorage(state.data), + state.lastFetch ?? DateTime.now().millisecondsSinceEpoch + ); Future gatherData(); TRepository repository(); diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.dart new file mode 100644 index 0000000..e4d68d5 --- /dev/null +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.dart @@ -0,0 +1,24 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'loadable_save_context.freezed.dart'; +part 'loadable_save_context.g.dart'; + +@freezed +class LoadableSaveContext with _$LoadableSaveContext { + const LoadableSaveContext._(); + const factory LoadableSaveContext({ + required int timestamp, + }) = _LoadableSaveContext; + + factory LoadableSaveContext.fromJson(Map json) => _$LoadableSaveContextFromJson(json); + + static String dataKey = 'data'; + static String metaKey = 'meta'; + + static Map wrap(Map? data, int lastFetch) => + {dataKey: data, metaKey: LoadableSaveContext(timestamp: lastFetch).toJson()}; + + static ({Map data, LoadableSaveContext meta}) unwrap(Map data) => + (data: data[dataKey] as Map, meta: LoadableSaveContext.fromJson(data[metaKey])); +} + diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.freezed.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.freezed.dart new file mode 100644 index 0000000..649526e --- /dev/null +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.freezed.dart @@ -0,0 +1,155 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'loadable_save_context.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +LoadableSaveContext _$LoadableSaveContextFromJson(Map json) { + return _LoadableSaveContext.fromJson(json); +} + +/// @nodoc +mixin _$LoadableSaveContext { + int get timestamp => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $LoadableSaveContextCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoadableSaveContextCopyWith<$Res> { + factory $LoadableSaveContextCopyWith( + LoadableSaveContext value, $Res Function(LoadableSaveContext) then) = + _$LoadableSaveContextCopyWithImpl<$Res, LoadableSaveContext>; + @useResult + $Res call({int timestamp}); +} + +/// @nodoc +class _$LoadableSaveContextCopyWithImpl<$Res, $Val extends LoadableSaveContext> + implements $LoadableSaveContextCopyWith<$Res> { + _$LoadableSaveContextCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? timestamp = null, + }) { + return _then(_value.copyWith( + timestamp: null == timestamp + ? _value.timestamp + : timestamp // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LoadableSaveContextImplCopyWith<$Res> + implements $LoadableSaveContextCopyWith<$Res> { + factory _$$LoadableSaveContextImplCopyWith(_$LoadableSaveContextImpl value, + $Res Function(_$LoadableSaveContextImpl) then) = + __$$LoadableSaveContextImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int timestamp}); +} + +/// @nodoc +class __$$LoadableSaveContextImplCopyWithImpl<$Res> + extends _$LoadableSaveContextCopyWithImpl<$Res, _$LoadableSaveContextImpl> + implements _$$LoadableSaveContextImplCopyWith<$Res> { + __$$LoadableSaveContextImplCopyWithImpl(_$LoadableSaveContextImpl _value, + $Res Function(_$LoadableSaveContextImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? timestamp = null, + }) { + return _then(_$LoadableSaveContextImpl( + timestamp: null == timestamp + ? _value.timestamp + : timestamp // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$LoadableSaveContextImpl extends _LoadableSaveContext { + const _$LoadableSaveContextImpl({required this.timestamp}) : super._(); + + factory _$LoadableSaveContextImpl.fromJson(Map json) => + _$$LoadableSaveContextImplFromJson(json); + + @override + final int timestamp; + + @override + String toString() { + return 'LoadableSaveContext(timestamp: $timestamp)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoadableSaveContextImpl && + (identical(other.timestamp, timestamp) || + other.timestamp == timestamp)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, timestamp); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LoadableSaveContextImplCopyWith<_$LoadableSaveContextImpl> get copyWith => + __$$LoadableSaveContextImplCopyWithImpl<_$LoadableSaveContextImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$LoadableSaveContextImplToJson( + this, + ); + } +} + +abstract class _LoadableSaveContext extends LoadableSaveContext { + const factory _LoadableSaveContext({required final int timestamp}) = + _$LoadableSaveContextImpl; + const _LoadableSaveContext._() : super._(); + + factory _LoadableSaveContext.fromJson(Map json) = + _$LoadableSaveContextImpl.fromJson; + + @override + int get timestamp; + @override + @JsonKey(ignore: true) + _$$LoadableSaveContextImplCopyWith<_$LoadableSaveContextImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.g.dart b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.g.dart new file mode 100644 index 0000000..c536225 --- /dev/null +++ b/lib/state/app/infrastructure/utilityWidgets/loadableHydratedBloc/loadable_save_context.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'loadable_save_context.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$LoadableSaveContextImpl _$$LoadableSaveContextImplFromJson( + Map json) => + _$LoadableSaveContextImpl( + timestamp: json['timestamp'] as int, + ); + +Map _$$LoadableSaveContextImplToJson( + _$LoadableSaveContextImpl instance) => + { + 'timestamp': instance.timestamp, + }; diff --git a/lib/view/pages/overhang.dart b/lib/view/pages/overhang.dart index 08a5f0f..6269307 100644 --- a/lib/view/pages/overhang.dart +++ b/lib/view/pages/overhang.dart @@ -72,10 +72,6 @@ class Overhang extends StatelessWidget { trailing: const Icon(Icons.arrow_right), onTap: () => pushScreen(context, withNavBar: false, screen: const FeedbackDialog()), ), - const ListTile( - leading: Icon(Icons.science_outlined), - // onTap: () => pushScreen(context, withNavBar: false, screen: const GradeAveragesScreen()), - ) ], ), ); From 69fc98ad452593fc1eafea6db5406c8e3666766a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 12 May 2024 14:27:16 +0200 Subject: [PATCH 16/19] automatic updating of last timestamp for bloc cache --- lib/app.dart | 1 + .../bloc/loadable_state_bloc.dart | 4 ++ .../view/loadable_state_error_bar.dart | 59 +++++++++++++------ .../bloc/marianum_message_bloc.dart | 6 -- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 5ba4596..912d495 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -96,6 +96,7 @@ class _AppState extends State with WidgetsBindingObserver { controller: App.bottomNavigator, navBarOverlap: const NavBarOverlap.none(), backgroundColor: Theme.of(context).colorScheme.primary, + handleAndroidBackButtonPress: false, screenTransitionAnimation: const ScreenTransitionAnimation(curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200)), tabs: [ diff --git a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart index 89998bb..b246417 100644 --- a/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart +++ b/lib/state/app/infrastructure/loadableState/bloc/loadable_state_bloc.dart @@ -37,6 +37,10 @@ class LoadableStateBloc extends Bloc { : Icons.signal_wifi_connected_no_internet_4 : Icons.device_unknown; + Color connectionColor(BuildContext context) => connectivityStatusKnown() && !isConnected() + ? Colors.grey.shade600 + : Theme.of(context).primaryColor; + String connectionText({int? lastUpdated}) => connectivityStatusKnown() ? isConnected() ? 'Verbindung fehlgeschlagen' diff --git a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart index 2752714..bc5353c 100644 --- a/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart +++ b/lib/state/app/infrastructure/loadableState/view/loadable_state_error_bar.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -31,14 +33,6 @@ class LoadableStateErrorBar extends StatelessWidget { child: Builder( builder: (context) { var bloc = context.watch(); - var status = ( - icon: bloc.connectionIcon(), - text: bloc.connectionText(lastUpdated: lastUpdated), - color: bloc.connectivityStatusKnown() && !bloc.isConnected() - ? Colors.grey.shade600 - : Theme.of(context).primaryColor - ); - return InkWell( onTap: () { if(!bloc.isConnected()) return; @@ -47,16 +41,9 @@ class LoadableStateErrorBar extends StatelessWidget { child: 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)) - ], + color: bloc.connectionColor(context), ), + child: LoadableStateErrorBarText(lastUpdated: lastUpdated), ), ); }, @@ -65,3 +52,41 @@ class LoadableStateErrorBar extends StatelessWidget { ), ); } + +class LoadableStateErrorBarText extends StatefulWidget { + final int? lastUpdated; + const LoadableStateErrorBarText({required this.lastUpdated, super.key}); + + @override + State createState() => _LoadableStateErrorBarTextState(); +} + +class _LoadableStateErrorBarTextState extends State { + late Timer _rebuildTimer; + @override + void initState() { + _rebuildTimer = Timer.periodic(const Duration(seconds: 10), (timer) => setState(() {})); + super.initState(); + } + + @override + Widget build(BuildContext context) { + var bloc = context.watch(); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(bloc.connectionIcon(), size: 14), + const SizedBox(width: 10), + Text(bloc.connectionText(lastUpdated: widget.lastUpdated), style: const TextStyle(fontSize: 12)) + ], + ); + } + + @override + void dispose() { + _rebuildTimer.cancel(); + super.dispose(); + } +} + diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart index 375dae0..6a8e52d 100644 --- a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart @@ -7,12 +7,6 @@ sealed class MarianumMessageEvent extends LoadableHydratedBlocEvent { - MarianumMessageBloc() { - on((event, emit) { - add(Emit((state) => state.copyWith.messageList(messages: []))); - }); - } - @override Future gatherData() async { var messages = await repo.getMessages(); From 8968e53e5c7393d83f58de9bf63412dede1ee7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 12 May 2024 14:38:06 +0200 Subject: [PATCH 17/19] removed test page --- lib/view/pages/more/test.dart | 48 ----------------------------------- 1 file changed, 48 deletions(-) delete mode 100644 lib/view/pages/more/test.dart diff --git a/lib/view/pages/more/test.dart b/lib/view/pages/more/test.dart deleted file mode 100644 index 5f68993..0000000 --- a/lib/view/pages/more/test.dart +++ /dev/null @@ -1,48 +0,0 @@ -// import 'package:flutter/material.dart'; -// import '../../../state/app/application/marianumMessage/marianum_message_controller.dart'; -// import '../../../state/app/application/marianumMessage/marianum_message_state.dart'; -// import '../../../state/infrastructure/loadable_state.dart'; -// import '../../../state/infrastructure/state_extensions.dart'; -// import '../../../state/widgets/controller_consumer.dart'; -// import '../../../state/widgets/loadable_state_consumer.dart'; -// import '../../../state/widgets/sub_selected_controller_consumer.dart'; -// import '../../../state/widgets/controller_provider.dart'; -// -// class Test extends StatelessWidget { -// const Test({super.key}); -// -// @override -// Widget build(BuildContext context) => ControllerProvider( -// create: (context) => MarianumMessageController(), -// child: (context) => Scaffold( -// appBar: AppBar(title: const Text('TEST')), -// body: ControllerConsumer( -// child: (context, state) => Column( -// children: [ -// TextButton( -// onPressed: () => context.readController().someaction(), -// child: Text(context.watchController().state.toString()) -// ), -// TextButton( -// onPressed: () => context.readController().backgroundLoading(), -// child: Text(context.watchController().state.toString()) -// ), -// TextButton( -// onPressed: () => context.readController().done(), -// child: Text(context.watchController().state.toString()) -// ), -// TextButton( -// onPressed: () => context.readController().error(), -// child: Text(context.watchController().state.toString()) -// ), -// ControllerConsumer(child: (context, state) => Text(state.base.toString())), -// SubSelectedControllerConsumer( -// subSelect: (state) => state.messages, -// child: (context, state) => Text(state.toString()), -// ) -// ], -// ), -// ), -// ), -// ); -// } From 8ff993bf197caff9f20b5ee75116fec7c49ac40a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 12 May 2024 14:44:12 +0200 Subject: [PATCH 18/19] removed old marianum message structure --- .../mhsl/message/getMessages/getMessages.dart | 14 ------ .../message/getMessages/getMessagesCache.dart | 17 ------- .../getMessages/getMessagesResponse.dart | 28 ----------- .../getMessages/getMessagesResponse.g.dart | 49 ------------------- lib/main.dart | 2 - lib/model/message/messageProps.dart | 25 ---------- 6 files changed, 135 deletions(-) delete mode 100644 lib/api/mhsl/message/getMessages/getMessages.dart delete mode 100644 lib/api/mhsl/message/getMessages/getMessagesCache.dart delete mode 100644 lib/api/mhsl/message/getMessages/getMessagesResponse.dart delete mode 100644 lib/api/mhsl/message/getMessages/getMessagesResponse.g.dart delete mode 100644 lib/model/message/messageProps.dart diff --git a/lib/api/mhsl/message/getMessages/getMessages.dart b/lib/api/mhsl/message/getMessages/getMessages.dart deleted file mode 100644 index 34bfc6b..0000000 --- a/lib/api/mhsl/message/getMessages/getMessages.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'dart:convert'; - -import 'package:http/http.dart' as http; - -import '../../mhslApi.dart'; -import 'getMessagesResponse.dart'; - -class GetMessages extends MhslApi { - GetMessages() : super('message/messages.json'); - @override - GetMessagesResponse assemble(String raw) => GetMessagesResponse.fromJson(jsonDecode(raw)); - @override - Future request(Uri uri) => http.get(uri); -} diff --git a/lib/api/mhsl/message/getMessages/getMessagesCache.dart b/lib/api/mhsl/message/getMessages/getMessagesCache.dart deleted file mode 100644 index fcb9afe..0000000 --- a/lib/api/mhsl/message/getMessages/getMessagesCache.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:convert'; - -import '../../../requestCache.dart'; -import 'getMessages.dart'; -import 'getMessagesResponse.dart'; - -class GetMessagesCache extends RequestCache { - GetMessagesCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) { - start('message'); - } - - @override - GetMessagesResponse onLocalData(String json) => GetMessagesResponse.fromJson(jsonDecode(json)); - - @override - Future onLoad() => GetMessages().run(); -} diff --git a/lib/api/mhsl/message/getMessages/getMessagesResponse.dart b/lib/api/mhsl/message/getMessages/getMessagesResponse.dart deleted file mode 100644 index 107c492..0000000 --- a/lib/api/mhsl/message/getMessages/getMessagesResponse.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -import '../../../apiResponse.dart'; - -part 'getMessagesResponse.g.dart'; - -@JsonSerializable(explicitToJson: true) -class GetMessagesResponse extends ApiResponse { - String base; - Set messages; - - GetMessagesResponse(this.base, this.messages); - - factory GetMessagesResponse.fromJson(Map json) => _$GetMessagesResponseFromJson(json); - Map toJson() => _$GetMessagesResponseToJson(this); -} - -@JsonSerializable(explicitToJson: true) -class GetMessagesResponseObject { - String name; - String date; - String url; - - GetMessagesResponseObject(this.name, this.date, this.url); - - factory GetMessagesResponseObject.fromJson(Map json) => _$GetMessagesResponseObjectFromJson(json); - Map toJson() => _$GetMessagesResponseObjectToJson(this); -} diff --git a/lib/api/mhsl/message/getMessages/getMessagesResponse.g.dart b/lib/api/mhsl/message/getMessages/getMessagesResponse.g.dart deleted file mode 100644 index 393b020..0000000 --- a/lib/api/mhsl/message/getMessages/getMessagesResponse.g.dart +++ /dev/null @@ -1,49 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'getMessagesResponse.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -GetMessagesResponse _$GetMessagesResponseFromJson(Map json) => - GetMessagesResponse( - json['base'] as String, - (json['messages'] as List) - .map((e) => - GetMessagesResponseObject.fromJson(e as Map)) - .toSet(), - )..headers = (json['headers'] as Map?)?.map( - (k, e) => MapEntry(k, e as String), - ); - -Map _$GetMessagesResponseToJson(GetMessagesResponse instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('headers', instance.headers); - val['base'] = instance.base; - val['messages'] = instance.messages.map((e) => e.toJson()).toList(); - return val; -} - -GetMessagesResponseObject _$GetMessagesResponseObjectFromJson( - Map json) => - GetMessagesResponseObject( - json['name'] as String, - json['date'] as String, - json['url'] as String, - ); - -Map _$GetMessagesResponseObjectToJson( - GetMessagesResponseObject instance) => - { - 'name': instance.name, - 'date': instance.date, - 'url': instance.url, - }; diff --git a/lib/main.dart b/lib/main.dart index 53189a2..b4bebb9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,7 +25,6 @@ import 'model/chatList/chatListProps.dart'; import 'model/chatList/chatProps.dart'; import 'model/files/filesProps.dart'; import 'model/holidays/holidaysProps.dart'; -import 'model/message/messageProps.dart'; import 'model/timetable/timetableProps.dart'; import 'storage/base/settingsProvider.dart'; import 'theming/darkAppTheme.dart'; @@ -71,7 +70,6 @@ Future main() async { ChangeNotifierProvider(create: (context) => ChatProps()), ChangeNotifierProvider(create: (context) => FilesProps()), - ChangeNotifierProvider(create: (context) => MessageProps()), ChangeNotifierProvider(create: (context) => HolidaysProps()), ], child: const Main(), diff --git a/lib/model/message/messageProps.dart b/lib/model/message/messageProps.dart deleted file mode 100644 index c2e2a37..0000000 --- a/lib/model/message/messageProps.dart +++ /dev/null @@ -1,25 +0,0 @@ - -import '../../api/apiResponse.dart'; -import '../../api/mhsl/message/getMessages/getMessagesCache.dart'; -import '../../api/mhsl/message/getMessages/getMessagesResponse.dart'; -import '../dataHolder.dart'; - -class MessageProps extends DataHolder { - GetMessagesResponse? _getMessagesResponse; - GetMessagesResponse get getMessagesResponse => _getMessagesResponse!; - - @override - List properties() => [_getMessagesResponse]; - - @override - void run({renew}) { - GetMessagesCache( - renew: renew, - onUpdate: (GetMessagesResponse data) => { - _getMessagesResponse = data, - notifyListeners(), - } - ); - } - -} From a57f42d4ed39153140d0273a33e72a800322052d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 12 May 2024 15:07:57 +0200 Subject: [PATCH 19/19] resolved pr comments --- .../app/modules/gradeAverages/view/grade_averages_view.dart | 4 ++-- .../modules/marianumMessage/bloc/marianum_message_bloc.dart | 4 +--- .../modules/marianumMessage/bloc/marianum_message_event.dart | 5 +++++ lib/view/pages/talk/components/chatMessage.dart | 2 +- lib/view/settings/devToolsSettingsDialog.dart | 5 +---- 5 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 lib/state/app/modules/marianumMessage/bloc/marianum_message_event.dart diff --git a/lib/state/app/modules/gradeAverages/view/grade_averages_view.dart b/lib/state/app/modules/gradeAverages/view/grade_averages_view.dart index c0734ef..7c55e68 100644 --- a/lib/state/app/modules/gradeAverages/view/grade_averages_view.dart +++ b/lib/state/app/modules/gradeAverages/view/grade_averages_view.dart @@ -62,7 +62,7 @@ class GradeAveragesView extends StatelessWidget { builder: (context) => ConfirmDialog( title: 'Notensystem wechseln', content: - 'Beim wechsel des Notensystems werden alle Einträge zurückgesetzt.', + 'Beim Wechsel des Notensystems werden alle Einträge zurückgesetzt.', confirmButton: 'Fortfahren', onConfirm: () => bloc.add(GradingSystemChanged(isMiddleSchool)), ), @@ -84,7 +84,7 @@ class GradeAveragesView extends StatelessWidget { const SizedBox(height: 10), const Divider(), const SizedBox(height: 10), - Text(bloc.isMiddleSchool() ? 'Wähle unten die Anzahl deiner jewiligen Noten aus' : 'Wähle unten die Anzahl deiner jeweiligen Punkte aus'), + Text(bloc.isMiddleSchool() ? 'Wähle unten die Anzahl deiner jeweiligen Noten aus' : 'Wähle unten die Anzahl deiner jeweiligen Punkte aus'), const SizedBox(height: 10), const Expanded( child: GradeAveragesListView() diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart index 6a8e52d..7876351 100644 --- a/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart @@ -1,11 +1,9 @@ import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart'; import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; import '../repository/marianum_message_repository.dart'; +import 'marianum_message_event.dart'; import 'marianum_message_state.dart'; -sealed class MarianumMessageEvent extends LoadableHydratedBlocEvent {} -class MessageEvent extends MarianumMessageEvent {} - class MarianumMessageBloc extends LoadableHydratedBloc { @override Future gatherData() async { diff --git a/lib/state/app/modules/marianumMessage/bloc/marianum_message_event.dart b/lib/state/app/modules/marianumMessage/bloc/marianum_message_event.dart new file mode 100644 index 0000000..43cbf2a --- /dev/null +++ b/lib/state/app/modules/marianumMessage/bloc/marianum_message_event.dart @@ -0,0 +1,5 @@ +import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; +import 'marianum_message_state.dart'; + +sealed class MarianumMessageEvent extends LoadableHydratedBlocEvent {} +class MessageEvent extends MarianumMessageEvent {} diff --git a/lib/view/pages/talk/components/chatMessage.dart b/lib/view/pages/talk/components/chatMessage.dart index 7767874..48bcaf9 100644 --- a/lib/view/pages/talk/components/chatMessage.dart +++ b/lib/view/pages/talk/components/chatMessage.dart @@ -52,7 +52,7 @@ class ChatMessage { fadeInDuration: Duration.zero, fadeOutDuration: Duration.zero, errorListener: (value) {}, - imageUrl: 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=100&sub_selected_controller_consumer.dart=-1&a=1', + imageUrl: 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=100&y=-1&a=1', ); } diff --git a/lib/view/settings/devToolsSettingsDialog.dart b/lib/view/settings/devToolsSettingsDialog.dart index e1fb660..463afa9 100644 --- a/lib/view/settings/devToolsSettingsDialog.dart +++ b/lib/view/settings/devToolsSettingsDialog.dart @@ -113,10 +113,7 @@ class _DevToolsSettingsDialogState extends State { ListTile( leading: const CenteredLeading(Icon(Icons.data_object)), title: const Text('BLOC State cache'), - subtitle: FutureBuilder( - future: const CacheView().totalSize(), - builder: (context, snapshot) => Text("etwa ${snapshot.hasError ? "?" : snapshot.hasData ? filesize(snapshot.data) : "..."}\nLange tippen um zu löschen"), - ), + subtitle: const Text('Lange tippen um zu löschen'), onTap: () { // Navigator.push(context, MaterialPageRoute(builder: (context) => const CacheView())); },