revamp on bloc approach

This commit is contained in:
Elias Müller 2024-05-05 15:48:26 +02:00
parent ee6af2bc07
commit f58a2ec8cd
28 changed files with 523 additions and 480 deletions

View File

@ -4,3 +4,4 @@ targets:
json_serializable: json_serializable:
options: options:
explicit_to_json: false explicit_to_json: false
generic_argument_factories: true

View File

@ -137,7 +137,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
), ),
), ),
PersistentTabConfig( PersistentTabConfig(
screen: const Breaker(breaker: BreakerArea.files, child: Files([])), screen: Breaker(breaker: BreakerArea.files, child: Files()),
item: ItemConfig( item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor, activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary, inactiveForegroundColor: Theme.of(context).colorScheme.secondary,

View File

@ -7,8 +7,10 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:loader_overlay/loader_overlay.dart'; import 'package:loader_overlay/loader_overlay.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@ -34,21 +36,26 @@ import 'widget/placeholderView.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
try { var initialisationTasks = [
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform)
log("Firebase token: ${await FirebaseMessaging.instance.getToken() ?? "Error: no Firebase token!"}"); .then((value) async => log("Firebase token: ${await FirebaseMessaging.instance.getToken() ?? "Error: no Firebase token!"}"))
} catch (e) { .onError((error, stackTrace) => log('Error initializing Firebase: $error')),
log('Error initializing Firebase app!');
}
var data = await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem'); PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem')
SecurityContext.defaultContext.setTrustedCertificatesBytes(data.buffer.asUint8List()); .then((certificate) => SecurityContext.defaultContext.setTrustedCertificatesBytes(certificate.buffer.asUint8List())),
Future(() async {
await HydratedStorage.build(storageDirectory: await getTemporaryDirectory()).then((storage) => HydratedBloc.storage = storage);
})
];
await Future.wait(initialisationTasks);
if(kReleaseMode) { if(kReleaseMode) {
ErrorWidget.builder = (error) => PlaceholderView( ErrorWidget.builder = (error) => PlaceholderView(
icon: Icons.phonelink_erase_rounded, icon: Icons.phonelink_erase_rounded,
text: error.toStringShort(), text: error.toStringShort(),
); );
} }
runApp( runApp(

View File

@ -1,10 +0,0 @@
import '../../../infrastructure/controller.dart';
import 'grade_averages_state.dart';
class GradeAveragesController extends Controller<GradeAveragesState> {
GradeAveragesController(super.initialState);
void setGradeType(GradingSchemes scheme) => emit(state.copyWith(gradingScheme: scheme));
double average() => state.grades.reduce((a, b) => a + b) / state.grades.length;
}

View File

@ -1,24 +0,0 @@
import '../../../infrastructure/controller.dart';
import '../../../infrastructure/loadable_state.dart';
import 'marianum_message_state.dart';
class MarianumMessageController extends Controller<LoadableState<MarianumMessageState>> {
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: [])));
}
}

View File

@ -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<String> test
}) = _MarianumMessageState;
}

View File

@ -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>(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<String> get test => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$MarianumMessageStateCopyWith<MarianumMessageState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $MarianumMessageStateCopyWith<$Res> {
factory $MarianumMessageStateCopyWith(MarianumMessageState value,
$Res Function(MarianumMessageState) then) =
_$MarianumMessageStateCopyWithImpl<$Res, MarianumMessageState>;
@useResult
$Res call({List<String> 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<String>,
) 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<String> 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<String>,
));
}
}
/// @nodoc
class _$MarianumMessageStateImpl implements _MarianumMessageState {
const _$MarianumMessageStateImpl({required final List<String> test})
: _test = test;
final List<String> _test;
@override
List<String> 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<String> test}) =
_$MarianumMessageStateImpl;
@override
List<String> get test;
@override
@JsonKey(ignore: true)
_$$MarianumMessageStateImplCopyWith<_$MarianumMessageStateImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@ -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<Module, AppModule> modules() => {
Module.timetable: AppModule('Vertretung', Icons.calendar_month, Timetable.new),
Module.talk: AppModule('Talk', Icons.chat, ChatList.new),
Module.files: AppModule('Files', Icons.folder, Files.new),
};
}
enum Module {
timetable,
talk,
files,
marianumMessage,
roomPlan,
gradeAveragesCalculator,
holidays,
}

View File

@ -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<GradeAveragesEvent, GradeAveragesState> {
GradeAveragesBloc() : super(const GradeAveragesState(gradingSystem: GradeAveragesGradingSystem.middleSchool, grades: [])) {
on<GradingSystemChanged>((event, emit) {
add(ResetAll());
emit(
state.copyWith(
gradingSystem: event.isMiddleSchool
? GradeAveragesGradingSystem.middleSchool
: GradeAveragesGradingSystem.highSchool
)
);
});
on<ResetAll>((event, emit) {
emit(state.copyWith(grades: []));
});
on<ResetGrade>((event, emit) {
emit(state.copyWith(grades: [...state.grades]..removeWhere((grade) => grade == event.grade)));
});
on<IncrementGrade>((event, emit) {
emit(state.copyWith(grades: [...state.grades, event.grade]));
});
on<DecrementGrade>((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<String, dynamic> json) => GradeAveragesState.fromJson(json);
@override
Map<String, dynamic>? toJson(GradeAveragesState state) => state.toJson();
}

View File

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

View File

@ -6,14 +6,14 @@ part 'grade_averages_state.g.dart';
@freezed @freezed
class GradeAveragesState with _$GradeAveragesState { class GradeAveragesState with _$GradeAveragesState {
const factory GradeAveragesState({ const factory GradeAveragesState({
required GradingSchemes gradingScheme, required GradeAveragesGradingSystem gradingSystem,
required List<int> grades, required List<int> grades,
}) = _GradeAveragesState; }) = _GradeAveragesState;
factory GradeAveragesState.fromJson(Map<String, dynamic> json) => _$GradeAveragesStateFromJson(json); factory GradeAveragesState.fromJson(Map<String, dynamic> json) => _$GradeAveragesStateFromJson(json);
} }
enum GradingSchemes { enum GradeAveragesGradingSystem {
middleSchool,
highSchool, highSchool,
middleSchool,
} }

View File

@ -20,7 +20,8 @@ GradeAveragesState _$GradeAveragesStateFromJson(Map<String, dynamic> json) {
/// @nodoc /// @nodoc
mixin _$GradeAveragesState { mixin _$GradeAveragesState {
GradingSchemes get gradingScheme => throw _privateConstructorUsedError; GradeAveragesGradingSystem get gradingSystem =>
throw _privateConstructorUsedError;
List<int> get grades => throw _privateConstructorUsedError; List<int> get grades => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -35,7 +36,7 @@ abstract class $GradeAveragesStateCopyWith<$Res> {
GradeAveragesState value, $Res Function(GradeAveragesState) then) = GradeAveragesState value, $Res Function(GradeAveragesState) then) =
_$GradeAveragesStateCopyWithImpl<$Res, GradeAveragesState>; _$GradeAveragesStateCopyWithImpl<$Res, GradeAveragesState>;
@useResult @useResult
$Res call({GradingSchemes gradingScheme, List<int> grades}); $Res call({GradeAveragesGradingSystem gradingSystem, List<int> grades});
} }
/// @nodoc /// @nodoc
@ -51,14 +52,14 @@ class _$GradeAveragesStateCopyWithImpl<$Res, $Val extends GradeAveragesState>
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? gradingScheme = null, Object? gradingSystem = null,
Object? grades = null, Object? grades = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
gradingScheme: null == gradingScheme gradingSystem: null == gradingSystem
? _value.gradingScheme ? _value.gradingSystem
: gradingScheme // ignore: cast_nullable_to_non_nullable : gradingSystem // ignore: cast_nullable_to_non_nullable
as GradingSchemes, as GradeAveragesGradingSystem,
grades: null == grades grades: null == grades
? _value.grades ? _value.grades
: grades // ignore: cast_nullable_to_non_nullable : grades // ignore: cast_nullable_to_non_nullable
@ -75,7 +76,7 @@ abstract class _$$GradeAveragesStateImplCopyWith<$Res>
__$$GradeAveragesStateImplCopyWithImpl<$Res>; __$$GradeAveragesStateImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({GradingSchemes gradingScheme, List<int> grades}); $Res call({GradeAveragesGradingSystem gradingSystem, List<int> grades});
} }
/// @nodoc /// @nodoc
@ -89,14 +90,14 @@ class __$$GradeAveragesStateImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? gradingScheme = null, Object? gradingSystem = null,
Object? grades = null, Object? grades = null,
}) { }) {
return _then(_$GradeAveragesStateImpl( return _then(_$GradeAveragesStateImpl(
gradingScheme: null == gradingScheme gradingSystem: null == gradingSystem
? _value.gradingScheme ? _value.gradingSystem
: gradingScheme // ignore: cast_nullable_to_non_nullable : gradingSystem // ignore: cast_nullable_to_non_nullable
as GradingSchemes, as GradeAveragesGradingSystem,
grades: null == grades grades: null == grades
? _value._grades ? _value._grades
: grades // ignore: cast_nullable_to_non_nullable : grades // ignore: cast_nullable_to_non_nullable
@ -109,14 +110,14 @@ class __$$GradeAveragesStateImplCopyWithImpl<$Res>
@JsonSerializable() @JsonSerializable()
class _$GradeAveragesStateImpl implements _GradeAveragesState { class _$GradeAveragesStateImpl implements _GradeAveragesState {
const _$GradeAveragesStateImpl( const _$GradeAveragesStateImpl(
{required this.gradingScheme, required final List<int> grades}) {required this.gradingSystem, required final List<int> grades})
: _grades = grades; : _grades = grades;
factory _$GradeAveragesStateImpl.fromJson(Map<String, dynamic> json) => factory _$GradeAveragesStateImpl.fromJson(Map<String, dynamic> json) =>
_$$GradeAveragesStateImplFromJson(json); _$$GradeAveragesStateImplFromJson(json);
@override @override
final GradingSchemes gradingScheme; final GradeAveragesGradingSystem gradingSystem;
final List<int> _grades; final List<int> _grades;
@override @override
List<int> get grades { List<int> get grades {
@ -127,7 +128,7 @@ class _$GradeAveragesStateImpl implements _GradeAveragesState {
@override @override
String toString() { String toString() {
return 'GradeAveragesState(gradingScheme: $gradingScheme, grades: $grades)'; return 'GradeAveragesState(gradingSystem: $gradingSystem, grades: $grades)';
} }
@override @override
@ -135,15 +136,15 @@ class _$GradeAveragesStateImpl implements _GradeAveragesState {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$GradeAveragesStateImpl && other is _$GradeAveragesStateImpl &&
(identical(other.gradingScheme, gradingScheme) || (identical(other.gradingSystem, gradingSystem) ||
other.gradingScheme == gradingScheme) && other.gradingSystem == gradingSystem) &&
const DeepCollectionEquality().equals(other._grades, _grades)); const DeepCollectionEquality().equals(other._grades, _grades));
} }
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, gradingScheme, const DeepCollectionEquality().hash(_grades)); runtimeType, gradingSystem, const DeepCollectionEquality().hash(_grades));
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@ -162,14 +163,14 @@ class _$GradeAveragesStateImpl implements _GradeAveragesState {
abstract class _GradeAveragesState implements GradeAveragesState { abstract class _GradeAveragesState implements GradeAveragesState {
const factory _GradeAveragesState( const factory _GradeAveragesState(
{required final GradingSchemes gradingScheme, {required final GradeAveragesGradingSystem gradingSystem,
required final List<int> grades}) = _$GradeAveragesStateImpl; required final List<int> grades}) = _$GradeAveragesStateImpl;
factory _GradeAveragesState.fromJson(Map<String, dynamic> json) = factory _GradeAveragesState.fromJson(Map<String, dynamic> json) =
_$GradeAveragesStateImpl.fromJson; _$GradeAveragesStateImpl.fromJson;
@override @override
GradingSchemes get gradingScheme; GradeAveragesGradingSystem get gradingSystem;
@override @override
List<int> get grades; List<int> get grades;
@override @override

View File

@ -9,19 +9,20 @@ part of 'grade_averages_state.dart';
_$GradeAveragesStateImpl _$$GradeAveragesStateImplFromJson( _$GradeAveragesStateImpl _$$GradeAveragesStateImplFromJson(
Map<String, dynamic> json) => Map<String, dynamic> json) =>
_$GradeAveragesStateImpl( _$GradeAveragesStateImpl(
gradingScheme: gradingSystem: $enumDecode(
$enumDecode(_$GradingSchemesEnumMap, json['gradingScheme']), _$GradeAveragesGradingSystemEnumMap, json['gradingSystem']),
grades: (json['grades'] as List<dynamic>).map((e) => e as int).toList(), grades: (json['grades'] as List<dynamic>).map((e) => e as int).toList(),
); );
Map<String, dynamic> _$$GradeAveragesStateImplToJson( Map<String, dynamic> _$$GradeAveragesStateImplToJson(
_$GradeAveragesStateImpl instance) => _$GradeAveragesStateImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'gradingScheme': _$GradingSchemesEnumMap[instance.gradingScheme]!, 'gradingSystem':
_$GradeAveragesGradingSystemEnumMap[instance.gradingSystem]!,
'grades': instance.grades, 'grades': instance.grades,
}; };
const _$GradingSchemesEnumMap = { const _$GradeAveragesGradingSystemEnumMap = {
GradingSchemes.middleSchool: 'middleSchool', GradeAveragesGradingSystem.highSchool: 'highSchool',
GradingSchemes.highSchool: 'highSchool', GradeAveragesGradingSystem.middleSchool: 'middleSchool',
}; };

View File

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

View File

@ -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<GradeAveragesBloc>(
create: (context) => GradeAveragesBloc(),
child: BlocBuilder<GradeAveragesBloc, GradeAveragesState>(
builder: (context, state) {
var bloc = context.watch<GradeAveragesBloc>();
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<bool>(
initialValue: bloc.isMiddleSchool(),
icon: const Icon(Icons.more_horiz),
itemBuilder: (context) => [true, false].map((isMiddleSchool) => PopupMenuItem<bool>(
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()
),
],
),
);
},
),
);
}

View File

@ -1,5 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class Controller<TContentType> extends Cubit<TContentType> {
Controller(super.initialState);
}

View File

@ -1,37 +0,0 @@
class LoadableState<TState> {
final LoadingState loadingState;
final TState? data;
const LoadableState({required this.loadingState, required this.data});
LoadableState<TState> loading() =>
LoadableState<TState>(loadingState: LoadingState.loading, data: null);
LoadableState<TState> cached(TState state) =>
LoadableState<TState>(loadingState: LoadingState.loading, data: state);
LoadableState<TState> done(TState state) =>
LoadableState<TState>(loadingState: LoadingState.none, data: state);
LoadableState<TState> error({TState? state}) =>
LoadableState<TState>(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,
}

View File

@ -1,7 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
extension StateExtensions on BuildContext {
TState readController<TState>() => read<TState>();
TState watchController<TState>() => watch<TState>();
}

View File

@ -1,58 +1,58 @@
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
//
import '../../app/base/infrastructure/errorBar/error_bar_controller.dart'; // import '../../app/base/infrastructure/errorBar/error_bar_controller.dart';
import '../../infrastructure/state_extensions.dart'; // import '../../infrastructure/state_extensions.dart';
import '../controller_provider.dart'; // import '../controller_provider.dart';
//
class ErrorBar extends StatelessWidget { // class ErrorBar extends StatelessWidget {
final bool visible; // final bool visible;
const ErrorBar({required this.visible, super.key}); // const ErrorBar({required this.visible, super.key});
//
final Duration animationDuration = const Duration(milliseconds: 200); // final Duration animationDuration = const Duration(milliseconds: 200);
//
@override // @override
Widget build(BuildContext context) => ControllerProvider<ErrorBarController>( // Widget build(BuildContext context) => ControllerProvider<ErrorBarController>(
create: (context) => ErrorBarController(), // create: (context) => ErrorBarController(),
child: (context) => AnimatedSize( // child: (context) => AnimatedSize(
duration: animationDuration, // duration: animationDuration,
child: AnimatedSwitcher( // child: AnimatedSwitcher(
duration: animationDuration, // duration: animationDuration,
transitionBuilder: (Widget child, Animation<double> animation) => SlideTransition( // transitionBuilder: (Widget child, Animation<double> animation) => SlideTransition(
position: Tween<Offset>( // position: Tween<Offset>(
begin: const Offset(0.0, -1.0), // begin: const Offset(0.0, -1.0),
end: Offset.zero, // end: Offset.zero,
).animate(animation), // ).animate(animation),
child: child, // child: child,
), // ),
child: Visibility( // child: Visibility(
key: Key(visible.hashCode.toString()), // key: Key(visible.hashCode.toString()),
visible: visible, // visible: visible,
replacement: const SizedBox(width: double.infinity), // replacement: const SizedBox(width: double.infinity),
child: Builder( // child: Builder(
builder: (context) { // builder: (context) {
var controller = context.watchController<ErrorBarController>(); // var controller = context.watchController<ErrorBarController>();
var status = controller.connectivityStatusKnown() && !controller.isConnected() // var status = controller.connectivityStatusKnown() && !controller.isConnected()
? (icon: Icons.wifi_off_outlined, text: 'Offline', color: Colors.grey.shade600) // ? (icon: Icons.wifi_off_outlined, text: 'Offline', color: Colors.grey.shade600)
: (icon: Icons.wifi_find_outlined, text: 'Verbindung fehlgeschlagen', color: Theme.of(context).primaryColor); // : (icon: Icons.wifi_find_outlined, text: 'Verbindung fehlgeschlagen', color: Theme.of(context).primaryColor);
//
return Container( // return Container(
height: 20, // height: 20,
decoration: BoxDecoration( // decoration: BoxDecoration(
color: status.color, // color: status.color,
), // ),
child: Row( // child: Row(
mainAxisAlignment: MainAxisAlignment.center, // mainAxisAlignment: MainAxisAlignment.center,
children: [ // children: [
Icon(status.icon, size: 14), // Icon(status.icon, size: 14),
const SizedBox(width: 10), // const SizedBox(width: 10),
Text(status.text, style: const TextStyle(fontSize: 12)) // Text(status.text, style: const TextStyle(fontSize: 12))
], // ],
), // ),
); // );
}, // },
) // )
) // )
), // ),
), // ),
); // );
} // }

View File

@ -1,12 +1,12 @@
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; // import 'package:flutter_bloc/flutter_bloc.dart';
//
import '../infrastructure/controller.dart'; // import '../infrastructure/controller.dart';
//
class ControllerConsumer<TController extends Controller<TState>, TState> extends StatelessWidget { // class ControllerConsumer<TController extends Controller<TState>, TState> extends StatelessWidget {
final Widget Function(BuildContext context, TState state) child; // final Widget Function(BuildContext context, TState state) child;
const ControllerConsumer({required this.child, super.key}); // const ControllerConsumer({required this.child, super.key});
//
@override // @override
Widget build(BuildContext context) => BlocBuilder<TController, TState>(builder: child); // Widget build(BuildContext context) => BlocBuilder<TController, TState>(builder: child);
} // }

View File

@ -1,21 +1,21 @@
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; // import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/single_child_widget.dart'; // import 'package:provider/single_child_widget.dart';
//
import '../infrastructure/controller.dart'; // import '../infrastructure/controller.dart';
//
//
class ControllerProvider<TState extends Controller> extends SingleChildStatelessWidget { // class ControllerProvider<TState extends Controller> extends SingleChildStatelessWidget {
final TState Function(BuildContext context) create; // final TState Function(BuildContext context) create;
final bool lazy; // final bool lazy;
final Widget Function(BuildContext context) child; // final Widget Function(BuildContext context) child;
ControllerProvider({required this.create, this.lazy = true, required this.child, super.key}) // ControllerProvider({required this.create, this.lazy = true, required this.child, super.key})
: super(child: Builder(builder: child)); // : super(child: Builder(builder: child));
//
@override // @override
Widget buildWithChild(BuildContext context, Widget? child) => BlocProvider( // Widget buildWithChild(BuildContext context, Widget? child) => BlocProvider(
create: create, // create: create,
lazy: lazy, // lazy: lazy,
child: child, // child: child,
); // );
} // }

View File

@ -1,17 +1,17 @@
//
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; // import 'package:flutter_bloc/flutter_bloc.dart';
//
import 'controller_provider.dart'; // import 'controller_provider.dart';
//
class ControllersProvider extends StatelessWidget { // class ControllersProvider extends StatelessWidget {
final List<ControllerProvider> controllers; // final List<ControllerProvider> controllers;
final Widget Function(BuildContext context) child; // final Widget Function(BuildContext context) child;
const ControllersProvider({required this.controllers, required this.child, super.key}); // const ControllersProvider({required this.controllers, required this.child, super.key});
//
@override // @override
Widget build(BuildContext context) => MultiBlocProvider( // Widget build(BuildContext context) => MultiBlocProvider(
providers: controllers, // providers: controllers,
child: Builder(builder: child) // child: Builder(builder: child)
); // );
} // }

View File

@ -1,40 +1,42 @@
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
// import 'package:flutter_bloc/flutter_bloc.dart';
import '../infrastructure/controller.dart'; //
import '../infrastructure/loadable_state.dart'; // import '../infrastructure/controller.dart';
import '../infrastructure/state_extensions.dart'; // import '../infrastructure/loadable_state.dart';
import 'components/background_loading_indicator.dart'; // import '../infrastructure/state_extensions.dart';
import 'components/error_bar.dart'; // import 'components/background_loading_indicator.dart';
import 'components/primary_loading_indicator.dart'; // import 'components/error_bar.dart';
// import 'components/primary_loading_indicator.dart';
class LoadableControllerConsumer<TController extends Controller<TState>, TState extends LoadableState> extends StatelessWidget { //
final Widget child; // class LoadableControllerConsumer<TController extends Controller<TState>, TState extends LoadableState> extends StatelessWidget {
const LoadableControllerConsumer({required this.child, super.key}); // final Widget child;
// const LoadableControllerConsumer({required this.child, super.key});
final Duration animationDuration = const Duration(milliseconds: 200); //
// final Duration animationDuration = const Duration(milliseconds: 200);
@override //
Widget build(BuildContext context) { // @override
var state = context.readController<TController>().state; // Widget build(BuildContext context) {
return Column( // var state = context.readController<TController>().state;
children: [ //
ErrorBar(visible: state.errorBarVisible()), // return Column(
Expanded( // children: [
child: Stack( // // ErrorBar(visible: state.errorBarVisible()),
children: [ // // Expanded(
PrimaryLoadingIndicator(visible: !state.hasStateData()), // // child: Stack(
BackgroundLoadingIndicator(visible: state.isBackgroundLoading() && !state.errorBarVisible()), // // children: [
// // PrimaryLoadingIndicator(visible: !state.hasStateData()),
AnimatedOpacity( // // BackgroundLoadingIndicator(visible: state.isBackgroundLoading() && !state.errorBarVisible()),
opacity: state.hasStateData() ? 1.0 : 0.0, // //
duration: animationDuration, // // AnimatedOpacity(
curve: Curves.easeInOut, // // opacity: state.hasStateData() ? 1.0 : 0.0,
child: state.hasStateData() ? child : const SizedBox.shrink() // // duration: animationDuration,
), // // curve: Curves.easeInOut,
], // // child: state.hasStateData() ? child : const SizedBox.shrink()
), // // ),
) // // ],
], // // ),
); // // )
} // ],
} // );
// }
// }

View File

@ -99,7 +99,7 @@ class _FileElementState extends State<FileElement> {
onTap: () { onTap: () {
if(widget.file.isDirectory) { if(widget.file.isDirectory) {
Navigator.of(context).push(MaterialPageRoute( 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 { } else {
if(EndpointData().getEndpointMode() == EndpointMode.stage) { if(EndpointData().getEndpointMode() == EndpointMode.stage) {

View File

@ -21,7 +21,7 @@ import 'filesUploadDialog.dart';
class Files extends StatefulWidget { class Files extends StatefulWidget {
final List<String> path; final List<String> path;
const Files(this.path, {super.key}); Files({List<String>? path, super.key}) : path = path ?? [];
@override @override
State<Files> createState() => _FilesState(); State<Files> createState() => _FilesState();

View File

@ -1,48 +1,48 @@
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
import '../../../state/app/application/marianumMessage/marianum_message_controller.dart'; // import '../../../state/app/application/marianumMessage/marianum_message_controller.dart';
import '../../../state/app/application/marianumMessage/marianum_message_state.dart'; // import '../../../state/app/application/marianumMessage/marianum_message_state.dart';
import '../../../state/infrastructure/loadable_state.dart'; // import '../../../state/infrastructure/loadable_state.dart';
import '../../../state/infrastructure/state_extensions.dart'; // import '../../../state/infrastructure/state_extensions.dart';
import '../../../state/widgets/controller_consumer.dart'; // import '../../../state/widgets/controller_consumer.dart';
import '../../../state/widgets/loadable_controller_consumer.dart'; // import '../../../state/widgets/loadable_controller_consumer.dart';
import '../../../state/widgets/sub_selected_controller_consumer.dart'; // import '../../../state/widgets/sub_selected_controller_consumer.dart';
import '../../../state/widgets/controller_provider.dart'; // import '../../../state/widgets/controller_provider.dart';
//
class Test extends StatelessWidget { // class Test extends StatelessWidget {
const Test({super.key}); // const Test({super.key});
//
@override // @override
Widget build(BuildContext context) => ControllerProvider<MarianumMessageController>( // Widget build(BuildContext context) => ControllerProvider<MarianumMessageController>(
create: (context) => MarianumMessageController(), // create: (context) => MarianumMessageController(),
child: (context) => Scaffold( // child: (context) => Scaffold(
appBar: AppBar(title: const Text('TEST')), // appBar: AppBar(title: const Text('TEST')),
body: LoadableControllerConsumer<MarianumMessageController, LoadableState<MarianumMessageState>>( // body: ControllerConsumer<MarianumMessageController, MarianumMessageState>(
child: Column( // child: (context, state) => Column(
children: [ // children: [
TextButton( // TextButton(
onPressed: () => context.readController<MarianumMessageController>().loading(), // onPressed: () => context.readController<MarianumMessageController>().someaction(),
child: Text(context.watchController<MarianumMessageController>().state.loadingState.toString()) // child: Text(context.watchController<MarianumMessageController>().state.toString())
), // ),
TextButton( // TextButton(
onPressed: () => context.readController<MarianumMessageController>().backgroundLoading(), // onPressed: () => context.readController<MarianumMessageController>().backgroundLoading(),
child: Text(context.watchController<MarianumMessageController>().state.loadingState.toString()) // child: Text(context.watchController<MarianumMessageController>().state.toString())
), // ),
TextButton( // TextButton(
onPressed: () => context.readController<MarianumMessageController>().done(), // onPressed: () => context.readController<MarianumMessageController>().done(),
child: Text(context.watchController<MarianumMessageController>().state.loadingState.toString()) // child: Text(context.watchController<MarianumMessageController>().state.toString())
), // ),
TextButton( // TextButton(
onPressed: () => context.readController<MarianumMessageController>().error(), // onPressed: () => context.readController<MarianumMessageController>().error(),
child: Text(context.watchController<MarianumMessageController>().state.loadingState.toString()) // child: Text(context.watchController<MarianumMessageController>().state.toString())
), // ),
ControllerConsumer<MarianumMessageController, LoadableState<MarianumMessageState>>(child: (context, state) => Text(state.data!.test.toString())), // ControllerConsumer<MarianumMessageController, MarianumMessageState>(child: (context, state) => Text(state.base.toString())),
SubSelectedControllerConsumer<MarianumMessageController, LoadableState<MarianumMessageState>, LoadingState>( // SubSelectedControllerConsumer<MarianumMessageController, MarianumMessageState, List>(
subSelect: (state) => state.loadingState, // subSelect: (state) => state.messages,
child: (context, state) => Text(state.toString()), // child: (context, state) => Text(state.toString()),
) // )
], // ],
), // ),
), // ),
), // ),
); // );
} // }

View File

@ -6,6 +6,7 @@ import 'package:in_app_review/in_app_review.dart';
import '../../extensions/renderNotNull.dart'; import '../../extensions/renderNotNull.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import '../../state/app/modules/gradeAverages/screens/grade_averages_view.dart';
import '../../widget/ListItem.dart'; import '../../widget/ListItem.dart';
import '../../widget/centeredLeading.dart'; import '../../widget/centeredLeading.dart';
import '../../widget/infoDialog.dart'; import '../../widget/infoDialog.dart';
@ -16,7 +17,6 @@ import 'more/holidays/holidays.dart';
import 'more/message/message.dart'; import 'more/message/message.dart';
import 'more/roomplan/roomplan.dart'; import 'more/roomplan/roomplan.dart';
import 'more/share/selectShareTypeDialog.dart'; import 'more/share/selectShareTypeDialog.dart';
import 'more/test.dart';
class Overhang extends StatelessWidget { class Overhang extends StatelessWidget {
const Overhang({super.key}); const Overhang({super.key});
@ -77,7 +77,7 @@ class Overhang extends StatelessWidget {
), ),
ListTile( ListTile(
leading: const Icon(Icons.science_outlined), leading: const Icon(Icons.science_outlined),
onTap: () => pushScreen(context, withNavBar: false, screen: const Test()), onTap: () => pushScreen(context, withNavBar: false, screen: const GradeAveragesScreen()),
) )
], ],
), ),

View File

@ -102,6 +102,7 @@ dependencies:
flutter_bloc: ^8.1.5 flutter_bloc: ^8.1.5
freezed_annotation: ^2.4.1 freezed_annotation: ^2.4.1
connectivity_plus: ^6.0.3 connectivity_plus: ^6.0.3
hydrated_bloc: ^9.1.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: