Merge pull request 'develop-bloc' (#68) from develop-bloc into develop

Reviewed-on: #68
Reviewed-by: Pupsi <larslukasneuhaus@gmx.de>
This commit is contained in:
Elias Müller 2024-05-12 13:12:22 +00:00
commit 43471fcf3d
56 changed files with 2615 additions and 299 deletions

View File

@ -9,6 +9,10 @@
# packages, and plugins designed to encourage good coding practices. # packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
analyzer:
errors:
invalid_annotation_target: ignore
linter: linter:
# The lint rules applied to this project can be customized in the # The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml` # section below to disable rules from the `package:flutter_lints/flutter.yaml`

7
build.yaml Normal file
View File

@ -0,0 +1,7 @@
targets:
$default:
builders:
json_serializable:
options:
explicit_to_json: false
generic_argument_factories: true

View File

@ -1,17 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../mhslApi.dart';
import 'getMessagesResponse.dart';
class GetMessages extends MhslApi<GetMessagesResponse> {
GetMessages() : super('message/messages.json');
@override
GetMessagesResponse assemble(String raw) => GetMessagesResponse.fromJson(jsonDecode(raw));
@override
Future<http.Response> request(Uri uri) => http.get(uri);
}

View File

@ -1,17 +0,0 @@
import 'dart:convert';
import '../../../requestCache.dart';
import 'getMessages.dart';
import 'getMessagesResponse.dart';
class GetMessagesCache extends RequestCache<GetMessagesResponse> {
GetMessagesCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) {
start('message');
}
@override
GetMessagesResponse onLocalData(String json) => GetMessagesResponse.fromJson(jsonDecode(json));
@override
Future<GetMessagesResponse> onLoad() => GetMessages().run();
}

View File

@ -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<GetMessagesResponseObject> messages;
GetMessagesResponse(this.base, this.messages);
factory GetMessagesResponse.fromJson(Map<String, dynamic> json) => _$GetMessagesResponseFromJson(json);
Map<String, dynamic> 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<String, dynamic> json) => _$GetMessagesResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetMessagesResponseObjectToJson(this);
}

View File

@ -1,49 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'getMessagesResponse.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetMessagesResponse _$GetMessagesResponseFromJson(Map<String, dynamic> json) =>
GetMessagesResponse(
json['base'] as String,
(json['messages'] as List<dynamic>)
.map((e) =>
GetMessagesResponseObject.fromJson(e as Map<String, dynamic>))
.toSet(),
)..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetMessagesResponseToJson(GetMessagesResponse instance) {
final val = <String, dynamic>{};
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<String, dynamic> json) =>
GetMessagesResponseObject(
json['name'] as String,
json['date'] as String,
json['url'] as String,
);
Map<String, dynamic> _$GetMessagesResponseObjectToJson(
GetMessagesResponseObject instance) =>
<String, dynamic>{
'name': instance.name,
'date': instance.date,
'url': instance.url,
};

View File

@ -5,6 +5,7 @@ import 'dart:developer';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'state/app/modules/app_modules.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
@ -21,10 +22,7 @@ import 'notification/notificationController.dart';
import 'notification/notificationTasks.dart'; import 'notification/notificationTasks.dart';
import 'notification/notifyUpdater.dart'; import 'notification/notifyUpdater.dart';
import 'storage/base/settingsProvider.dart'; import 'storage/base/settingsProvider.dart';
import 'view/pages/files/files.dart';
import 'view/pages/overhang.dart'; import 'view/pages/overhang.dart';
import 'view/pages/talk/chatList.dart';
import 'view/pages/timetable/timetable.dart';
class App extends StatefulWidget { class App extends StatefulWidget {
const App({super.key}); const App({super.key});
@ -99,26 +97,16 @@ class _AppState extends State<App> with WidgetsBindingObserver {
controller: Main.bottomNavigator, controller: Main.bottomNavigator,
navBarOverlap: const NavBarOverlap.none(), navBarOverlap: const NavBarOverlap.none(),
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,
handleAndroidBackButtonPress: false,
screenTransitionAnimation: const ScreenTransitionAnimation(curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200)), screenTransitionAnimation: const ScreenTransitionAnimation(curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200)),
tabs: [ tabs: [
PersistentTabConfig( AppModule.getModule(Modules.timetable).toBottomTab(context),
screen: const Breaker(breaker: BreakerArea.timetable, child: Timetable()), AppModule.getModule(Modules.talk).toBottomTab(
item: ItemConfig( context,
activeForegroundColor: Theme.of(context).primaryColor, itemBuilder: (icon) => Consumer<ChatListProps>(
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.calendar_month),
title: 'Vertretung'
),
),
PersistentTabConfig(
screen: const Breaker(breaker: BreakerArea.talk, child: ChatList()),
item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: Consumer<ChatListProps>(
builder: (context, value, child) { builder: (context, value, child) {
if(value.primaryLoading()) return const Icon(Icons.chat); if(value.primaryLoading()) return Icon(icon);
var messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b); var messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b);
return badges.Badge( return badges.Badge(
showBadge: messages > 0, showBadge: messages > 0,
@ -130,22 +118,13 @@ class _AppState extends State<App> with WidgetsBindingObserver {
elevation: 1, elevation: 1,
), ),
badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)), badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
child: const Icon(Icons.chat), child: Icon(icon),
); );
}, },
), ),
title: 'Talk',
),
),
PersistentTabConfig(
screen: const Breaker(breaker: BreakerArea.files, child: Files([])),
item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.folder),
title: 'Dateien'
),
), ),
AppModule.getModule(Modules.files).toBottomTab(context),
PersistentTabConfig( PersistentTabConfig(
screen: const Breaker(breaker: BreakerArea.more, child: Overhang()), screen: const Breaker(breaker: BreakerArea.more, child: Overhang()),
item: ItemConfig( item: ItemConfig(

View File

@ -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:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@ -24,7 +26,6 @@ import 'model/chatList/chatListProps.dart';
import 'model/chatList/chatProps.dart'; import 'model/chatList/chatProps.dart';
import 'model/files/filesProps.dart'; import 'model/files/filesProps.dart';
import 'model/holidays/holidaysProps.dart'; import 'model/holidays/holidaysProps.dart';
import 'model/message/messageProps.dart';
import 'model/timetable/timetableProps.dart'; import 'model/timetable/timetableProps.dart';
import 'storage/base/settingsProvider.dart'; import 'storage/base/settingsProvider.dart';
import 'theming/darkAppTheme.dart'; import 'theming/darkAppTheme.dart';
@ -35,15 +36,20 @@ 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(
@ -65,7 +71,6 @@ Future<void> main() async {
ChangeNotifierProvider(create: (context) => ChatProps()), ChangeNotifierProvider(create: (context) => ChatProps()),
ChangeNotifierProvider(create: (context) => FilesProps()), ChangeNotifierProvider(create: (context) => FilesProps()),
ChangeNotifierProvider(create: (context) => MessageProps()),
ChangeNotifierProvider(create: (context) => HolidaysProps()), ChangeNotifierProvider(create: (context) => HolidaysProps()),
], ],
child: const Main(), child: const Main(),

View File

@ -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<ApiResponse?> properties() => [_getMessagesResponse];
@override
void run({renew}) {
GetMessagesCache(
renew: renew,
onUpdate: (GetMessagesResponse data) => {
_getMessagesResponse = data,
notifyListeners(),
}
);
}
}

View File

@ -0,0 +1,42 @@
import 'dart:convert';
import 'dart:developer';
import 'package:dio/dio.dart';
abstract class DataLoader<TResult> {
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<TResult> run() async {
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!),
headers: response.headers.map.map((key, value) => MapEntry(key, value.join(';'))),
));
} catch(trace, e) {
log(trace.toString());
throw(e);
}
}
Future<Response<String>> fetch();
TResult assemble(DataLoaderResult data);
}
class DataLoaderResult {
final Map<String, dynamic> json;
final Map<String, String> headers;
DataLoaderResult({required this.json, required this.headers});
}

View File

@ -0,0 +1,9 @@
import 'package:dio/dio.dart';
import 'data_loader.dart';
abstract class MhslDataLoader<TResult> extends DataLoader<TResult> {
MhslDataLoader() : super(Dio(BaseOptions(
baseUrl: 'https://mhsl.eu/marianum/marianummobile/'
)));
}

View File

@ -0,0 +1,55 @@
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';
class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState> {
late StreamSubscription<List<ConnectivityResult>> _updateStream;
void Function()? reFetch;
LoadableStateBloc() : super(const LoadableStateState(connections: null)) {
on<ConnectivityChanged>((event, emit) {
emit(event.state);
if(connectivityStatusKnown() && isConnected()) {
if(reFetch == null) return;
reFetch!();
}
});
emitConnectivity(List<ConnectivityResult> result) => add(ConnectivityChanged(LoadableStateState(connections: result)));
Connectivity().checkConnectivity().then(emitConnectivity);
_updateStream = Connectivity().onConnectivityChanged.listen(emitConnectivity);
}
bool connectivityStatusKnown() => state.connections != null;
bool isConnected() => !(state.connections?.contains(ConnectivityResult.none) ?? true);
bool allowRetry() => reFetch != null;
IconData connectionIcon() => connectivityStatusKnown()
? isConnected()
? Icons.nearby_error
: 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'
: 'Offline${lastUpdated == null ? '' : ' - Stand von ${Jiffy.parseFromMillisecondsSinceEpoch(lastUpdated).fromNow()}'}'
: 'Unbekannte Fehlerursache';
@override
Future<void> close() {
_updateStream.cancel();
return super.close();
}
}

View File

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

View File

@ -0,0 +1,11 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'loadable_state_state.freezed.dart';
@freezed
class LoadableStateState with _$LoadableStateState {
const factory LoadableStateState({
required List<ConnectivityResult>? connections,
}) = _LoadableStateState;
}

View File

@ -0,0 +1,147 @@
// 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_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 _$LoadableStateState {
List<ConnectivityResult>? get connections =>
throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$LoadableStateStateCopyWith<LoadableStateState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LoadableStateStateCopyWith<$Res> {
factory $LoadableStateStateCopyWith(
LoadableStateState value, $Res Function(LoadableStateState) then) =
_$LoadableStateStateCopyWithImpl<$Res, LoadableStateState>;
@useResult
$Res call({List<ConnectivityResult>? connections});
}
/// @nodoc
class _$LoadableStateStateCopyWithImpl<$Res, $Val extends LoadableStateState>
implements $LoadableStateStateCopyWith<$Res> {
_$LoadableStateStateCopyWithImpl(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<ConnectivityResult>?,
) as $Val);
}
}
/// @nodoc
abstract class _$$LoadableStateStateImplCopyWith<$Res>
implements $LoadableStateStateCopyWith<$Res> {
factory _$$LoadableStateStateImplCopyWith(_$LoadableStateStateImpl value,
$Res Function(_$LoadableStateStateImpl) then) =
__$$LoadableStateStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({List<ConnectivityResult>? connections});
}
/// @nodoc
class __$$LoadableStateStateImplCopyWithImpl<$Res>
extends _$LoadableStateStateCopyWithImpl<$Res, _$LoadableStateStateImpl>
implements _$$LoadableStateStateImplCopyWith<$Res> {
__$$LoadableStateStateImplCopyWithImpl(_$LoadableStateStateImpl _value,
$Res Function(_$LoadableStateStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? connections = freezed,
}) {
return _then(_$LoadableStateStateImpl(
connections: freezed == connections
? _value._connections
: connections // ignore: cast_nullable_to_non_nullable
as List<ConnectivityResult>?,
));
}
}
/// @nodoc
class _$LoadableStateStateImpl implements _LoadableStateState {
const _$LoadableStateStateImpl(
{required final List<ConnectivityResult>? connections})
: _connections = connections;
final List<ConnectivityResult>? _connections;
@override
List<ConnectivityResult>? 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 'LoadableStateState(connections: $connections)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LoadableStateStateImpl &&
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')
_$$LoadableStateStateImplCopyWith<_$LoadableStateStateImpl> get copyWith =>
__$$LoadableStateStateImplCopyWithImpl<_$LoadableStateStateImpl>(
this, _$identity);
}
abstract class _LoadableStateState implements LoadableStateState {
const factory _LoadableStateState(
{required final List<ConnectivityResult>? connections}) =
_$LoadableStateStateImpl;
@override
List<ConnectivityResult>? get connections;
@override
@JsonKey(ignore: true)
_$$LoadableStateStateImplCopyWith<_$LoadableStateStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'loading_error.dart';
part 'loadable_state.freezed.dart';
@freezed
class LoadableState<TState> with _$LoadableState {
const 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;
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();
}

View File

@ -0,0 +1,246 @@
// 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>(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<TState> {
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;
@JsonKey(ignore: true)
$LoadableStateCopyWith<TState, LoadableState<TState>> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LoadableStateCopyWith<TState, $Res> {
factory $LoadableStateCopyWith(LoadableState<TState> value,
$Res Function(LoadableState<TState>) then) =
_$LoadableStateCopyWithImpl<TState, $Res, LoadableState<TState>>;
@useResult
$Res call(
{bool isLoading,
TState? data,
int? lastFetch,
void Function()? reFetch,
LoadingError? error});
$LoadingErrorCopyWith<$Res>? get error;
}
/// @nodoc
class _$LoadableStateCopyWithImpl<TState, $Res,
$Val extends LoadableState<TState>>
implements $LoadableStateCopyWith<TState, $Res> {
_$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,
Object? lastFetch = freezed,
Object? reFetch = freezed,
Object? error = 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?,
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
as void Function()?,
error: freezed == error
? _value.error
: error // ignore: cast_nullable_to_non_nullable
as LoadingError?,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$LoadingErrorCopyWith<$Res>? get error {
if (_value.error == null) {
return null;
}
return $LoadingErrorCopyWith<$Res>(_value.error!, (value) {
return _then(_value.copyWith(error: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$LoadableStateImplCopyWith<TState, $Res>
implements $LoadableStateCopyWith<TState, $Res> {
factory _$$LoadableStateImplCopyWith(_$LoadableStateImpl<TState> value,
$Res Function(_$LoadableStateImpl<TState>) then) =
__$$LoadableStateImplCopyWithImpl<TState, $Res>;
@override
@useResult
$Res call(
{bool isLoading,
TState? data,
int? lastFetch,
void Function()? reFetch,
LoadingError? error});
@override
$LoadingErrorCopyWith<$Res>? get error;
}
/// @nodoc
class __$$LoadableStateImplCopyWithImpl<TState, $Res>
extends _$LoadableStateCopyWithImpl<TState, $Res,
_$LoadableStateImpl<TState>>
implements _$$LoadableStateImplCopyWith<TState, $Res> {
__$$LoadableStateImplCopyWithImpl(_$LoadableStateImpl<TState> _value,
$Res Function(_$LoadableStateImpl<TState>) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isLoading = null,
Object? data = freezed,
Object? lastFetch = freezed,
Object? reFetch = freezed,
Object? error = freezed,
}) {
return _then(_$LoadableStateImpl<TState>(
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?,
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
as void Function()?,
error: freezed == error
? _value.error
: error // ignore: cast_nullable_to_non_nullable
as LoadingError?,
));
}
}
/// @nodoc
class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
const _$LoadableStateImpl(
{this.isLoading = true,
this.data = null,
this.lastFetch = null,
this.reFetch = null,
this.error = null})
: super._();
@override
@JsonKey()
final bool isLoading;
@override
@JsonKey()
final TState? data;
@override
@JsonKey()
final int? lastFetch;
@override
@JsonKey()
final void Function()? reFetch;
@override
@JsonKey()
final LoadingError? error;
@override
String toString() {
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LoadableStateImpl<TState> &&
(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), lastFetch, reFetch, error);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$LoadableStateImplCopyWith<TState, _$LoadableStateImpl<TState>>
get copyWith => __$$LoadableStateImplCopyWithImpl<TState,
_$LoadableStateImpl<TState>>(this, _$identity);
}
abstract class _LoadableState<TState> extends LoadableState<TState> {
const factory _LoadableState(
{final bool isLoading,
final TState? data,
final int? lastFetch,
final void Function()? reFetch,
final LoadingError? error}) = _$LoadableStateImpl<TState>;
const _LoadableState._() : super._();
@override
bool get isLoading;
@override
TState? get data;
@override
int? get lastFetch;
@override
void Function()? get reFetch;
@override
LoadingError? get error;
@override
@JsonKey(ignore: true)
_$$LoadableStateImplCopyWith<TState, _$LoadableStateImpl<TState>>
get copyWith => throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,11 @@
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 allowRetry,
}) = _LoadingError;
}

View File

@ -0,0 +1,152 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'loading_error.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$LoadingError {
String get message => throw _privateConstructorUsedError;
bool get allowRetry => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$LoadingErrorCopyWith<LoadingError> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LoadingErrorCopyWith<$Res> {
factory $LoadingErrorCopyWith(
LoadingError value, $Res Function(LoadingError) then) =
_$LoadingErrorCopyWithImpl<$Res, LoadingError>;
@useResult
$Res call({String message, bool allowRetry});
}
/// @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? allowRetry = null,
}) {
return _then(_value.copyWith(
message: null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
allowRetry: null == allowRetry
? _value.allowRetry
: allowRetry // ignore: cast_nullable_to_non_nullable
as bool,
) 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 allowRetry});
}
/// @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? allowRetry = null,
}) {
return _then(_$LoadingErrorImpl(
message: null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
allowRetry: null == allowRetry
? _value.allowRetry
: allowRetry // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
class _$LoadingErrorImpl implements _LoadingError {
const _$LoadingErrorImpl({required this.message, this.allowRetry = false});
@override
final String message;
@override
@JsonKey()
final bool allowRetry;
@override
String toString() {
return 'LoadingError(message: $message, allowRetry: $allowRetry)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LoadingErrorImpl &&
(identical(other.message, message) || other.message == message) &&
(identical(other.allowRetry, allowRetry) ||
other.allowRetry == allowRetry));
}
@override
int get hashCode => Object.hash(runtimeType, message, allowRetry);
@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 allowRetry}) = _$LoadingErrorImpl;
@override
String get message;
@override
bool get allowRetry;
@override
@JsonKey(ignore: true)
_$$LoadingErrorImplCopyWith<_$LoadingErrorImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'loadable_state_consumer.dart';
class LoadableStateBackgroundLoading extends StatelessWidget {
final bool visible;
const LoadableStateBackgroundLoading({required this.visible, super.key});
@override
Widget build(BuildContext context) => AnimatedSwitcher(
duration: LoadableStateConsumer.animationDuration,
transitionBuilder: (Widget child, Animation<double> animation) => SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, -1.0),
end: Offset.zero,
).animate(animation),
child: child,
),
child: visible ? const LinearProgressIndicator() : const SizedBox.shrink(),
);
}

View File

@ -0,0 +1,78 @@
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';
import '../bloc/loadable_state_state.dart';
import '../loadable_state.dart';
import 'loadable_state_background_loading.dart';
import 'loadable_state_error_bar.dart';
import 'loadable_state_error_screen.dart';
import 'loadable_state_primary_loading.dart';
class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>>, TState> extends StatelessWidget {
final Widget Function(TState state, bool loading) child;
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<TController>().state;
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(),
),
);
return BlocModule<LoadableStateBloc, LoadableStateState>(
create: (context) => LoadableStateBloc(),
child: (context, bloc, state) {
bloc.reFetch = loadableState.reFetch;
return Column(
children: [
LoadableStateErrorBar(visible: loadableState.showErrorBar(), message: loadableState.error?.message, lastUpdated: loadableState.lastFetch),
Expanded(
child: Stack(
children: [
LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()),
LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()),
LoadableStateErrorScreen(visible: loadableState.showError(), message: loadableState.error?.message),
AnimatedOpacity(
opacity: loadableState.showContent() ? 1.0 : 0.0,
duration: animationDuration,
curve: Curves.easeInOut,
child: childWidget,
),
],
),
)
],
);
}
);
}
}

View File

@ -0,0 +1,92 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../widget/infoDialog.dart';
import '../bloc/loadable_state_bloc.dart';
class LoadableStateErrorBar extends StatelessWidget {
final bool visible;
final String? message;
final int? lastUpdated;
const LoadableStateErrorBar({required this.visible, this.message, this.lastUpdated, super.key});
final Duration animationDuration = const Duration(milliseconds: 200);
@override
Widget build(BuildContext context) => AnimatedSize(
duration: animationDuration,
child: AnimatedSwitcher(
duration: animationDuration,
transitionBuilder: (Widget child, Animation<double> animation) => SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, -1.0),
end: Offset.zero,
).animate(animation),
child: child,
),
child: Visibility(
key: Key(visible.hashCode.toString()),
visible: visible,
replacement: const SizedBox(width: double.infinity),
child: Builder(
builder: (context) {
var bloc = context.watch<LoadableStateBloc>();
return InkWell(
onTap: () {
if(!bloc.isConnected()) return;
InfoDialog.show(context, 'Exception: ${message.toString()}');
},
child: Container(
height: 20,
decoration: BoxDecoration(
color: bloc.connectionColor(context),
),
child: LoadableStateErrorBarText(lastUpdated: lastUpdated),
),
);
},
)
)
),
);
}
class LoadableStateErrorBarText extends StatefulWidget {
final int? lastUpdated;
const LoadableStateErrorBarText({required this.lastUpdated, super.key});
@override
State<LoadableStateErrorBarText> createState() => _LoadableStateErrorBarTextState();
}
class _LoadableStateErrorBarTextState extends State<LoadableStateErrorBarText> {
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<LoadableStateBloc>();
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();
}
}

View File

@ -0,0 +1,51 @@
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;
final String? message;
const LoadableStateErrorScreen({required this.visible, this.message, super.key});
@override
Widget build(BuildContext context) {
final bloc = context.watch<LoadableStateBloc>();
return AnimatedOpacity(
opacity: visible ? 1.0 : 0.0,
duration: LoadableStateConsumer.animationDuration,
curve: Curves.easeInOut,
child: !visible ? null : Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(bloc.connectionIcon(), size: 40),
const SizedBox(height: 10),
Text(bloc.connectionText(), style: const TextStyle(fontSize: 20)),
if(bloc.allowRetry()) ...[
const SizedBox(height: 10),
TextButton(onPressed: () => bloc.reFetch!(), child: const Text('Erneut versuschen')),
const SizedBox(height: 40),
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,
),
),
],
],
),
),
);
}
}

View File

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'loadable_state_consumer.dart';
class LoadableStatePrimaryLoading extends StatelessWidget {
final bool visible;
const LoadableStatePrimaryLoading({required this.visible, super.key});
@override
Widget build(BuildContext context) => AnimatedOpacity(
opacity: visible ? 1.0 : 0.0,
duration: LoadableStateConsumer.animationDuration,
curve: Curves.easeInOut,
child: const Center(child: CircularProgressIndicator()),
);
}

View File

@ -0,0 +1 @@
abstract class Repository<TState> {}

View File

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

View File

@ -0,0 +1,91 @@
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';
import 'loadable_save_context.dart';
abstract class LoadableHydratedBloc<
TEvent extends LoadableHydratedBlocEvent<TState>,
TState,
TRepository extends Repository<TState>
> extends HydratedBloc<
LoadableHydratedBlocEvent<TState>,
LoadableState<TState>
> {
late TRepository _repository;
LoadableHydratedBloc() : super(const LoadableState()) {
on<Emit<TState>>((event, emit) => emit(LoadableState(
isLoading: event.loading,
data: event.state(innerState ?? fromNothing()),
lastFetch: DateTime.now().millisecondsSinceEpoch,
reFetch: retry
)));
on<RefetchStarted<TState>>((event, emit) => emit(LoadableState(
isLoading: true,
data: innerState,
lastFetch: state.lastFetch
)));
on<ClearState<TState>>((event, emit) => emit(const LoadableState()));
on<Error<TState>>((event, emit) => emit(LoadableState(
isLoading: false,
data: innerState,
lastFetch: state.lastFetch,
reFetch: retry,
error: event.error
)));
_repository = repository();
fetch();
}
TState? get innerState => state.data;
TRepository get repo => _repository;
void retry() {
log('Fetch retry triggered for ${TState.toString()}');
add(RefetchStarted<TState>());
fetch();
}
void fetch() {
log('Fetching data for ${TState.toString()}');
gatherData().catchError(
(e) {
log('Error while fetching ${TState.toString()}: ${e.toString()}');
add(Error(LoadingError(
message: e.message,
allowRetry: true,
)));
},
).then((value) {
log('Fetch for ${TState.toString()} completed!');
});
}
@override
fromJson(Map<String, dynamic> json) {
var rawData = LoadableSaveContext.unwrap(json);
return LoadableState(isLoading: true, lastFetch: rawData.meta.timestamp, data: fromStorage(rawData.data));
}
@override
Map<String, dynamic>? toJson(LoadableState<TState> state) => LoadableSaveContext.wrap(
toStorage(state.data),
state.lastFetch ?? DateTime.now().millisecondsSinceEpoch
);
Future<void> gatherData();
TRepository repository();
TState fromNothing();
TState fromStorage(Map<String, dynamic> json);
Map<String, dynamic>? toStorage(TState state);
}

View File

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

View File

@ -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<String, dynamic> json) => _$LoadableSaveContextFromJson(json);
static String dataKey = 'data';
static String metaKey = 'meta';
static Map<String, dynamic> wrap(Map<String, dynamic>? data, int lastFetch) =>
{dataKey: data, metaKey: LoadableSaveContext(timestamp: lastFetch).toJson()};
static ({Map<String, dynamic> data, LoadableSaveContext meta}) unwrap(Map<String, dynamic> data) =>
(data: data[dataKey] as Map<String, dynamic>, meta: LoadableSaveContext.fromJson(data[metaKey]));
}

View File

@ -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>(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<String, dynamic> json) {
return _LoadableSaveContext.fromJson(json);
}
/// @nodoc
mixin _$LoadableSaveContext {
int get timestamp => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$LoadableSaveContextCopyWith<LoadableSaveContext> 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<String, dynamic> 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<String, dynamic> toJson() {
return _$$LoadableSaveContextImplToJson(
this,
);
}
}
abstract class _LoadableSaveContext extends LoadableSaveContext {
const factory _LoadableSaveContext({required final int timestamp}) =
_$LoadableSaveContextImpl;
const _LoadableSaveContext._() : super._();
factory _LoadableSaveContext.fromJson(Map<String, dynamic> json) =
_$LoadableSaveContextImpl.fromJson;
@override
int get timestamp;
@override
@JsonKey(ignore: true)
_$$LoadableSaveContextImplCopyWith<_$LoadableSaveContextImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'loadable_save_context.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$LoadableSaveContextImpl _$$LoadableSaveContextImplFromJson(
Map<String, dynamic> json) =>
_$LoadableSaveContextImpl(
timestamp: json['timestamp'] as int,
);
Map<String, dynamic> _$$LoadableSaveContextImplToJson(
_$LoadableSaveContextImpl instance) =>
<String, dynamic>{
'timestamp': instance.timestamp,
};

View File

@ -0,0 +1,60 @@
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;
IconData icon;
Widget Function() create;
AppModule(this.name, this.icon, this.create);
static Map<Modules, AppModule> modules() => {
Modules.timetable: AppModule('Vertretung', Icons.calendar_month, Timetable.new),
Modules.talk: AppModule('Talk', Icons.chat, ChatList.new),
Modules.files: AppModule('Files', Icons.folder, Files.new),
Modules.marianumMessage: AppModule('Marianum Message', Icons.newspaper, MarianumMessageListView.new),
Modules.roomPlan: AppModule('Raumplan', Icons.location_pin, Roomplan.new),
Modules.gradeAveragesCalculator: AppModule('Notendurschnittsrechner', Icons.calculate, GradeAveragesView.new),
Modules.holidays: AppModule('Schulferien', Icons.holiday_village, Holidays.new),
};
static AppModule getModule(Modules module) => modules()[module]!;
Widget toListTile(BuildContext context) => ListTile(
leading: CenteredLeading(Icon(icon)),
title: Text(name),
onTap: () => pushScreen(context, withNavBar: false, screen: create()),
trailing: const Icon(Icons.arrow_right),
);
PersistentTabConfig toBottomTab(BuildContext context, {Widget Function(IconData icon)? itemBuilder}) => PersistentTabConfig(
screen: Breaker(breaker: BreakerArea.global, child: create()),
item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: itemBuilder == null ? Icon(icon) : itemBuilder(icon),
title: name
),
);
}
enum Modules {
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

@ -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 GradeAveragesGradingSystem gradingSystem,
required List<int> grades,
}) = _GradeAveragesState;
factory GradeAveragesState.fromJson(Map<String, dynamic> json) => _$GradeAveragesStateFromJson(json);
}
enum GradeAveragesGradingSystem {
highSchool,
middleSchool,
}

View File

@ -0,0 +1,180 @@
// 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>(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<String, dynamic> json) {
return _GradeAveragesState.fromJson(json);
}
/// @nodoc
mixin _$GradeAveragesState {
GradeAveragesGradingSystem get gradingSystem =>
throw _privateConstructorUsedError;
List<int> get grades => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$GradeAveragesStateCopyWith<GradeAveragesState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $GradeAveragesStateCopyWith<$Res> {
factory $GradeAveragesStateCopyWith(
GradeAveragesState value, $Res Function(GradeAveragesState) then) =
_$GradeAveragesStateCopyWithImpl<$Res, GradeAveragesState>;
@useResult
$Res call({GradeAveragesGradingSystem gradingSystem, List<int> 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? gradingSystem = null,
Object? grades = null,
}) {
return _then(_value.copyWith(
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
as List<int>,
) as $Val);
}
}
/// @nodoc
abstract class _$$GradeAveragesStateImplCopyWith<$Res>
implements $GradeAveragesStateCopyWith<$Res> {
factory _$$GradeAveragesStateImplCopyWith(_$GradeAveragesStateImpl value,
$Res Function(_$GradeAveragesStateImpl) then) =
__$$GradeAveragesStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({GradeAveragesGradingSystem gradingSystem, List<int> 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? gradingSystem = null,
Object? grades = null,
}) {
return _then(_$GradeAveragesStateImpl(
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
as List<int>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$GradeAveragesStateImpl implements _GradeAveragesState {
const _$GradeAveragesStateImpl(
{required this.gradingSystem, required final List<int> grades})
: _grades = grades;
factory _$GradeAveragesStateImpl.fromJson(Map<String, dynamic> json) =>
_$$GradeAveragesStateImplFromJson(json);
@override
final GradeAveragesGradingSystem gradingSystem;
final List<int> _grades;
@override
List<int> get grades {
if (_grades is EqualUnmodifiableListView) return _grades;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_grades);
}
@override
String toString() {
return 'GradeAveragesState(gradingSystem: $gradingSystem, grades: $grades)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$GradeAveragesStateImpl &&
(identical(other.gradingSystem, gradingSystem) ||
other.gradingSystem == gradingSystem) &&
const DeepCollectionEquality().equals(other._grades, _grades));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType, gradingSystem, const DeepCollectionEquality().hash(_grades));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$GradeAveragesStateImplCopyWith<_$GradeAveragesStateImpl> get copyWith =>
__$$GradeAveragesStateImplCopyWithImpl<_$GradeAveragesStateImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$GradeAveragesStateImplToJson(
this,
);
}
}
abstract class _GradeAveragesState implements GradeAveragesState {
const factory _GradeAveragesState(
{required final GradeAveragesGradingSystem gradingSystem,
required final List<int> grades}) = _$GradeAveragesStateImpl;
factory _GradeAveragesState.fromJson(Map<String, dynamic> json) =
_$GradeAveragesStateImpl.fromJson;
@override
GradeAveragesGradingSystem get gradingSystem;
@override
List<int> get grades;
@override
@JsonKey(ignore: true)
_$$GradeAveragesStateImplCopyWith<_$GradeAveragesStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'grade_averages_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$GradeAveragesStateImpl _$$GradeAveragesStateImplFromJson(
Map<String, dynamic> json) =>
_$GradeAveragesStateImpl(
gradingSystem: $enumDecode(
_$GradeAveragesGradingSystemEnumMap, json['gradingSystem']),
grades: (json['grades'] as List<dynamic>).map((e) => e as int).toList(),
);
Map<String, dynamic> _$$GradeAveragesStateImplToJson(
_$GradeAveragesStateImpl instance) =>
<String, dynamic>{
'gradingSystem':
_$GradeAveragesGradingSystemEnumMap[instance.gradingSystem]!,
'grades': instance.grades,
};
const _$GradeAveragesGradingSystemEnumMap = {
GradeAveragesGradingSystem.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 GradeAveragesView extends StatelessWidget {
const GradeAveragesView({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 jeweiligen Noten aus' : 'Wähle unten die Anzahl deiner jeweiligen Punkte aus'),
const SizedBox(height: 10),
const Expanded(
child: GradeAveragesListView()
),
],
),
);
},
),
);
}

View File

@ -0,0 +1,24 @@
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';
class MarianumMessageBloc extends LoadableHydratedBloc<MarianumMessageEvent, MarianumMessageState, MarianumMessageRepository> {
@override
Future<void> gatherData() 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<String, dynamic> json) => MarianumMessageState.fromJson(json);
@override
Map<String, dynamic>? toStorage(MarianumMessageState state) => state.toJson();
}

View File

@ -0,0 +1,5 @@
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import 'marianum_message_state.dart';
sealed class MarianumMessageEvent extends LoadableHydratedBlocEvent<MarianumMessageState> {}
class MessageEvent extends MarianumMessageEvent {}

View File

@ -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<String, dynamic> json) => _$MarianumMessageStateFromJson(json);
}
@freezed
class MarianumMessageList with _$MarianumMessageList {
const factory MarianumMessageList({
required String base,
required List<MarianumMessage> messages,
}) = _MarianumMessageList;
factory MarianumMessageList.fromJson(Map<String, dynamic> 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<String, dynamic> json) => _$MarianumMessageFromJson(json);
}
enum GradeAveragesGradingSystem {
highSchool,
middleSchool,
}

View File

@ -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>(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<String, dynamic> json) {
return _MarianumMessageState.fromJson(json);
}
/// @nodoc
mixin _$MarianumMessageState {
MarianumMessageList get messageList => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => 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({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<String, dynamic> 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<String, dynamic> toJson() {
return _$$MarianumMessageStateImplToJson(
this,
);
}
}
abstract class _MarianumMessageState implements MarianumMessageState {
const factory _MarianumMessageState(
{required final MarianumMessageList messageList}) =
_$MarianumMessageStateImpl;
factory _MarianumMessageState.fromJson(Map<String, dynamic> json) =
_$MarianumMessageStateImpl.fromJson;
@override
MarianumMessageList get messageList;
@override
@JsonKey(ignore: true)
_$$MarianumMessageStateImplCopyWith<_$MarianumMessageStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
MarianumMessageList _$MarianumMessageListFromJson(Map<String, dynamic> json) {
return _MarianumMessageList.fromJson(json);
}
/// @nodoc
mixin _$MarianumMessageList {
String get base => throw _privateConstructorUsedError;
List<MarianumMessage> get messages => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$MarianumMessageListCopyWith<MarianumMessageList> 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<MarianumMessage> 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<MarianumMessage>,
) 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<MarianumMessage> 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<MarianumMessage>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$MarianumMessageListImpl implements _MarianumMessageList {
const _$MarianumMessageListImpl(
{required this.base, required final List<MarianumMessage> messages})
: _messages = messages;
factory _$MarianumMessageListImpl.fromJson(Map<String, dynamic> json) =>
_$$MarianumMessageListImplFromJson(json);
@override
final String base;
final List<MarianumMessage> _messages;
@override
List<MarianumMessage> 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<String, dynamic> toJson() {
return _$$MarianumMessageListImplToJson(
this,
);
}
}
abstract class _MarianumMessageList implements MarianumMessageList {
const factory _MarianumMessageList(
{required final String base,
required final List<MarianumMessage> messages}) =
_$MarianumMessageListImpl;
factory _MarianumMessageList.fromJson(Map<String, dynamic> json) =
_$MarianumMessageListImpl.fromJson;
@override
String get base;
@override
List<MarianumMessage> get messages;
@override
@JsonKey(ignore: true)
_$$MarianumMessageListImplCopyWith<_$MarianumMessageListImpl> get copyWith =>
throw _privateConstructorUsedError;
}
MarianumMessage _$MarianumMessageFromJson(Map<String, dynamic> json) {
return _MarianumMessage.fromJson(json);
}
/// @nodoc
mixin _$MarianumMessage {
String get name => throw _privateConstructorUsedError;
String get date => throw _privateConstructorUsedError;
String get url => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$MarianumMessageCopyWith<MarianumMessage> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
}

View File

@ -0,0 +1,52 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'marianum_message_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$MarianumMessageStateImpl _$$MarianumMessageStateImplFromJson(
Map<String, dynamic> json) =>
_$MarianumMessageStateImpl(
messageList: MarianumMessageList.fromJson(
json['messageList'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$MarianumMessageStateImplToJson(
_$MarianumMessageStateImpl instance) =>
<String, dynamic>{
'messageList': instance.messageList,
};
_$MarianumMessageListImpl _$$MarianumMessageListImplFromJson(
Map<String, dynamic> json) =>
_$MarianumMessageListImpl(
base: json['base'] as String,
messages: (json['messages'] as List<dynamic>)
.map((e) => MarianumMessage.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$$MarianumMessageListImplToJson(
_$MarianumMessageListImpl instance) =>
<String, dynamic>{
'base': instance.base,
'messages': instance.messages,
};
_$MarianumMessageImpl _$$MarianumMessageImplFromJson(
Map<String, dynamic> json) =>
_$MarianumMessageImpl(
name: json['name'] as String,
date: json['date'] as String,
url: json['url'] as String,
);
Map<String, dynamic> _$$MarianumMessageImplToJson(
_$MarianumMessageImpl instance) =>
<String, dynamic>{
'name': instance.name,
'date': instance.date,
'url': instance.url,
};

View File

@ -0,0 +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 MhslDataLoader<MarianumMessageList> {
@override
Future<Response<String>> fetch() async => dio.get('/message/messages.json');
@override
MarianumMessageList assemble(DataLoaderResult data) => MarianumMessageList.fromJson(data.json);
}

View File

@ -0,0 +1,7 @@
import '../../../infrastructure/repository/repository.dart';
import '../bloc/marianum_message_state.dart';
import '../dataProvider/marianum_message_get_messages.dart';
class MarianumMessageRepository extends Repository<MarianumMessageState> {
Future<MarianumMessageList> getMessages() => MarianumMessageGetMessages().run();
}

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.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';
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) => BlocModule<MarianumMessageBloc, LoadableState<MarianumMessageState>>(
create: (context) => MarianumMessageBloc(),
child: (context, bloc, state) => Scaffold(
appBar: AppBar(
title: const Text('Marianum Message'),
),
body: LoadableStateConsumer<MarianumMessageBloc, MarianumMessageState>(
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, 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)));
},
);
}
),
),
)
);
}

View File

@ -2,12 +2,12 @@ import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../../../api/mhsl/message/getMessages/getMessagesResponse.dart'; import '../bloc/marianum_message_state.dart';
import '../../../../widget/confirmDialog.dart'; import '../../../../../widget/confirmDialog.dart';
class MessageView extends StatefulWidget { class MessageView extends StatefulWidget {
final String basePath; final String basePath;
final GetMessagesResponseObject message; final MarianumMessage message;
const MessageView({super.key, required this.basePath, required this.message}); const MessageView({super.key, required this.basePath, required this.message});
@override @override

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,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<Message> createState() => _MessageState();
}
class _MessageState extends State<Message> {
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Provider.of<MessageProps>(context, listen: false).run();
});
super.initState();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Marianum Message'),
),
body: Consumer<MessageProps>(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<MessageProps>(context, listen: false).run(renew: true);
return Future.delayed(const Duration(seconds: 3));
},
);
}),
);
}

View File

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

View File

@ -1,6 +1,7 @@
import 'package:filesize/filesize.dart'; import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../storage/base/settingsProvider.dart'; import '../../storage/base/settingsProvider.dart';
@ -109,6 +110,23 @@ class _DevToolsSettingsDialogState extends State<DevToolsSettingsDialog> {
}, },
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
), ),
ListTile(
leading: const CenteredLeading(Icon(Icons.data_object)),
title: const Text('BLOC State cache'),
subtitle: const Text('Lange 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),
),
], ],
); );
} }

View File

@ -1,29 +0,0 @@
import 'package:flutter/material.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
class ListItemNavigator extends StatelessWidget {
const ListItemNavigator({super.key, required this.icon, required this.text, required this.target, this.onLongPress, this.arrow = true});
final IconData icon;
final String text;
final bool arrow;
final Widget target;
final GestureLongPressCallback? onLongPress;
@override
Widget build(BuildContext context) {
onTabAction() => pushScreen(context, withNavBar: false, screen: target); //Navigator.push(context, MaterialPageRoute(builder: (context) => target));
onLongPressAction() => onLongPress;
return ListTile(
leading: Icon(icon),
title: Text(text),
trailing: arrow ? const Icon(Icons.arrow_right) : null,
onTap: onTabAction,
onLongPress: onLongPressAction,
);
}
}

View File

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

View File

@ -49,7 +49,7 @@ dependencies:
shared_preferences: ^2.0.15 shared_preferences: ^2.0.15
provider: ^6.0.4 provider: ^6.0.4
jiffy: ^6.1.0 jiffy: ^6.1.0
json_annotation: ^4.8.0 json_annotation: ^4.8.1
localstore: ^1.2.3 localstore: ^1.2.3
intl: ^0.18.0 intl: ^0.18.0
nextcloud: nextcloud:
@ -98,12 +98,18 @@ dependencies:
time_range_picker: ^2.2.0 time_range_picker: ^2.2.0
in_app_review: ^2.0.8 in_app_review: ^2.0.8
emoji_picker_flutter: ^2.1.1 emoji_picker_flutter: ^2.1.1
bloc: ^8.1.4
flutter_bloc: ^8.1.5
freezed_annotation: ^2.4.1
connectivity_plus: ^6.0.3
hydrated_bloc: ^9.1.5
dio: ^4.0.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
json_serializable: ^6.6.1 json_serializable: ^6.7.1
build_runner: ^2.3.3 build_runner: ^2.4.9
# The "flutter_lints" package below contains a set of recommended lints to # The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is # encourage good coding practices. The lint set provided by the package is
@ -111,6 +117,7 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^3.0.1 flutter_lints: ^3.0.1
freezed: ^2.5.2
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec