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

Reviewed-on: #72
Reviewed-by: Pupsi <larslukasneuhaus@gmx.de>
This commit is contained in:
Elias Müller 2024-06-23 21:00:45 +00:00
commit ddeeaeaeac
29 changed files with 834 additions and 201 deletions

View File

@ -1,5 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'getHolidaysResponse.dart'; import 'getHolidaysResponse.dart';
@ -7,11 +6,10 @@ import 'getHolidaysResponse.dart';
class GetHolidays { class GetHolidays {
Future<GetHolidaysResponse> query() async { Future<GetHolidaysResponse> query() async {
var response = (await http.get(Uri.parse('https://ferien-api.de/api/v1/holidays/HE'))).body; var response = (await http.get(Uri.parse('https://ferien-api.de/api/v1/holidays/HE'))).body;
var data = jsonDecode(response) as List<dynamic>;
return GetHolidaysResponse( return GetHolidaysResponse(
List<GetHolidaysResponseObject>.from( List<GetHolidaysResponseObject>.from(
jsonDecode(response).map<GetHolidaysResponseObject>( data.map<GetHolidaysResponseObject>((e) => GetHolidaysResponseObject.fromJson(e as Map<String, dynamic>))
GetHolidaysResponseObject.fromJson
)
) )
); );
} }

View File

@ -15,8 +15,7 @@ class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
return GetHolidaysResponse( return GetHolidaysResponse(
List<GetHolidaysResponseObject>.from( List<GetHolidaysResponseObject>.from(
parsedListJson.map<GetHolidaysResponseObject>( parsedListJson.map<GetHolidaysResponseObject>(
// ignore: unnecessary_lambdas (i) => GetHolidaysResponseObject.fromJson(i as Map<String, dynamic>)
(dynamic i) => GetHolidaysResponseObject.fromJson(i)
) )
) )
); );

View File

@ -0,0 +1,9 @@
import 'package:dio/dio.dart';
import '../../infrastructure/dataLoader/data_loader.dart';
abstract class HolidayDataLoader<TResult> extends DataLoader<TResult> {
HolidayDataLoader() : super(Dio(BaseOptions(
baseUrl: 'https://ferien-api.de/api/v1/',
)));
}

View File

@ -1,6 +1,6 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'data_loader.dart'; import '../../infrastructure/dataLoader/data_loader.dart';
abstract class MhslDataLoader<TResult> extends DataLoader<TResult> { abstract class MhslDataLoader<TResult> extends DataLoader<TResult> {
MhslDataLoader() : super(Dio(BaseOptions( MhslDataLoader() : super(Dio(BaseOptions(

View File

@ -35,8 +35,12 @@ abstract class DataLoader<TResult> {
} }
class DataLoaderResult { class DataLoaderResult {
final Map<String, dynamic> json; final dynamic json;
final Map<String, String> headers; final Map<String, String> headers;
Map<String, dynamic> asMap() => json as Map<String, dynamic>;
List<dynamic> asList() => json as List<dynamic>;
List<Map<String, dynamic>> asListOfMaps() => asList().map((e) => e as Map<String, dynamic>).toList();
DataLoaderResult({required this.json, required this.headers}); DataLoaderResult({required this.json, required this.headers});
} }

View File

@ -9,11 +9,11 @@ class LoadableState<TState> with _$LoadableState {
const LoadableState._(); const LoadableState._();
const factory LoadableState({ const factory LoadableState({
@Default(true) bool isLoading, required bool isLoading,
@Default(null) TState? data, required TState? data,
@Default(null) int? lastFetch, required int? lastFetch,
@Default(null) void Function()? reFetch, required void Function()? reFetch,
@Default(null) LoadingError? error, required LoadingError? error,
}) = _LoadableState; }) = _LoadableState;
bool _hasError() => error != null; bool _hasError() => error != null;

View File

@ -15,14 +15,20 @@ import 'loadable_state_primary_loading.dart';
class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>>, TState> extends StatelessWidget { class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>>, TState> extends StatelessWidget {
final Widget Function(TState state, bool loading) child; final Widget Function(TState state, bool loading) child;
final void Function(TState state)? onLoad;
final bool wrapWithScrollView; final bool wrapWithScrollView;
const LoadableStateConsumer({required this.child, this.wrapWithScrollView = false, super.key}); const LoadableStateConsumer({required this.child, this.onLoad, this.wrapWithScrollView = false, super.key});
static Duration animationDuration = const Duration(milliseconds: 200); static Duration animationDuration = const Duration(milliseconds: 200);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var loadableState = context.watch<TController>().state; var loadableState = context.watch<TController>().state;
if(!loadableState.isLoading && onLoad != null) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => onLoad!(loadableState.data));
}
var childWidget = ConditionalWrapper( var childWidget = ConditionalWrapper(
condition: loadableState.reFetch != null, condition: loadableState.reFetch != null,
wrapper: (child) => RefreshIndicator( wrapper: (child) => RefreshIndicator(

View File

@ -89,4 +89,3 @@ class _LoadableStateErrorBarTextState extends State<LoadableStateErrorBarText> {
super.dispose(); super.dispose();
} }
} }

View File

@ -5,14 +5,19 @@ class BlocModule<TBloc extends StateStreamableSource<TState>, TState> extends St
final TBloc Function(BuildContext context) create; final TBloc Function(BuildContext context) create;
final Widget Function(BuildContext context, TBloc bloc, TState state) child; final Widget Function(BuildContext context, TBloc bloc, TState state) child;
final bool autoRebuild; final bool autoRebuild;
const BlocModule({required this.create, required this.child, this.autoRebuild = false, super.key}); final void Function(BuildContext context, TBloc bloc)? onInitialisation;
const BlocModule({required this.create, required this.child, this.autoRebuild = false, this.onInitialisation, super.key});
Widget rebuildChild(BuildContext context) => child(context, context.watch<TBloc>(), context.watch<TBloc>().state); 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); Widget staticChild(BuildContext context) => child(context, context.read<TBloc>(), context.read<TBloc>().state);
@override @override
Widget build(BuildContext context) => BlocProvider<TBloc>( Widget build(BuildContext context) => BlocProvider<TBloc>(
create: create, create: (context) {
var bloc = create(context);
this.onInitialisation != null ? this.onInitialisation!(context, bloc) : null;
return bloc;
},
child: Builder( child: Builder(
builder: (context) => autoRebuild builder: (context) => autoRebuild
? rebuildChild(context) ? rebuildChild(context)

View File

@ -17,23 +17,40 @@ abstract class LoadableHydratedBloc<
LoadableState<TState> LoadableState<TState>
> { > {
late TRepository _repository; late TRepository _repository;
LoadableHydratedBloc() : super(const LoadableState()) { LoadableHydratedBloc() : super(const LoadableState(
error: null,
data: null,
isLoading: true,
lastFetch: null,
reFetch: null,
)) {
on<Emit<TState>>((event, emit) => emit(LoadableState( on<Emit<TState>>((event, emit) {
isLoading: event.loading, emit(LoadableState(
isLoading: state.isLoading,
data: event.state(innerState ?? fromNothing()),
lastFetch: state.lastFetch,
reFetch: retry,
error: state.error,
));
});
on<DataGathered<TState>>((event, emit) => emit(LoadableState(
isLoading: false,
data: event.state(innerState ?? fromNothing()), data: event.state(innerState ?? fromNothing()),
lastFetch: DateTime.now().millisecondsSinceEpoch, lastFetch: DateTime.now().millisecondsSinceEpoch,
reFetch: retry reFetch: retry,
error: null,
))); )));
on<RefetchStarted<TState>>((event, emit) => emit(LoadableState( on<RefetchStarted<TState>>((event, emit) => emit(LoadableState(
isLoading: true, isLoading: true,
data: innerState, data: innerState,
lastFetch: state.lastFetch lastFetch: state.lastFetch,
reFetch: null,
error: null,
))); )));
on<ClearState<TState>>((event, emit) => emit(const LoadableState()));
on<Error<TState>>((event, emit) => emit(LoadableState( on<Error<TState>>((event, emit) => emit(LoadableState(
isLoading: false, isLoading: false,
data: innerState, data: innerState,
@ -61,7 +78,7 @@ abstract class LoadableHydratedBloc<
(e) { (e) {
log('Error while fetching ${TState.toString()}: ${e.toString()}'); log('Error while fetching ${TState.toString()}: ${e.toString()}');
add(Error(LoadingError( add(Error(LoadingError(
message: e.message, message: e.message ?? e.toString(),
allowRetry: true, allowRetry: true,
))); )));
}, },
@ -73,14 +90,29 @@ abstract class LoadableHydratedBloc<
@override @override
fromJson(Map<String, dynamic> json) { fromJson(Map<String, dynamic> json) {
var rawData = LoadableSaveContext.unwrap(json); var rawData = LoadableSaveContext.unwrap(json);
return LoadableState(isLoading: true, lastFetch: rawData.meta.timestamp, data: fromStorage(rawData.data)); return LoadableState(
isLoading: true,
data: fromStorage(rawData.data),
lastFetch: rawData.meta.timestamp,
reFetch: null,
error: null,
);
} }
@override @override
Map<String, dynamic>? toJson(LoadableState<TState> state) => LoadableSaveContext.wrap( Map<String, dynamic>? toJson(LoadableState<TState> state) {
toStorage(state.data), Map<String, dynamic>? data;
try {
data = state.data == null ? null : toStorage(state.data);
} catch(e) {
log('Failed to save state ${TState.toString()}: ${e.toString()}');
}
return LoadableSaveContext.wrap(
data,
state.lastFetch ?? DateTime.now().millisecondsSinceEpoch state.lastFetch ?? DateTime.now().millisecondsSinceEpoch
); );
}
Future<void> gatherData(); Future<void> gatherData();
TRepository repository(); TRepository repository();

View File

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

View File

@ -21,4 +21,3 @@ class LoadableSaveContext with _$LoadableSaveContext {
static ({Map<String, dynamic> data, LoadableSaveContext meta}) unwrap(Map<String, dynamic> data) => static ({Map<String, dynamic> data, LoadableSaveContext meta}) unwrap(Map<String, dynamic> data) =>
(data: data[dataKey] as Map<String, dynamic>, meta: LoadableSaveContext.fromJson(data[metaKey])); (data: data[dataKey] as Map<String, dynamic>, meta: LoadableSaveContext.fromJson(data[metaKey]));
} }

View File

@ -4,12 +4,12 @@ import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import '../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart'; import '../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
import '../../../model/breakers/Breaker.dart'; import '../../../model/breakers/Breaker.dart';
import '../../../view/pages/files/files.dart'; import '../../../view/pages/files/files.dart';
import '../../../view/pages/more/holidays/holidays.dart';
import '../../../view/pages/more/roomplan/roomplan.dart'; import '../../../view/pages/more/roomplan/roomplan.dart';
import '../../../view/pages/talk/chatList.dart'; import '../../../view/pages/talk/chatList.dart';
import '../../../view/pages/timetable/timetable.dart'; import '../../../view/pages/timetable/timetable.dart';
import '../../../widget/centeredLeading.dart'; import '../../../widget/centeredLeading.dart';
import 'gradeAverages/view/grade_averages_view.dart'; import 'gradeAverages/view/grade_averages_view.dart';
import 'holidays/view/holidays_view.dart';
import 'marianumMessage/view/marianum_message_list_view.dart'; import 'marianumMessage/view/marianum_message_list_view.dart';
class AppModule { class AppModule {
@ -26,7 +26,7 @@ class AppModule {
Modules.marianumMessage: AppModule('Marianum Message', Icons.newspaper, MarianumMessageListView.new), Modules.marianumMessage: AppModule('Marianum Message', Icons.newspaper, MarianumMessageListView.new),
Modules.roomPlan: AppModule('Raumplan', Icons.location_pin, Roomplan.new), Modules.roomPlan: AppModule('Raumplan', Icons.location_pin, Roomplan.new),
Modules.gradeAveragesCalculator: AppModule('Notendurschnittsrechner', Icons.calculate, GradeAveragesView.new), Modules.gradeAveragesCalculator: AppModule('Notendurschnittsrechner', Icons.calculate, GradeAveragesView.new),
Modules.holidays: AppModule('Schulferien', Icons.holiday_village, Holidays.new), Modules.holidays: AppModule('Schulferien', Icons.flight, HolidaysView.new),
}; };
static AppModule getModule(Modules module) => modules()[module]!; static AppModule getModule(Modules module) => modules()[module]!;

View File

@ -0,0 +1,37 @@
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import '../repository/holidays_repository.dart';
import 'holidays_event.dart';
import 'holidays_state.dart';
class HolidaysBloc extends LoadableHydratedBloc<HolidaysEvent, HolidaysState, HolidaysRepository> {
HolidaysBloc() {
on<SetPastHolidaysVisible>((event, emit) {
add(Emit((state) => state.copyWith(showPastHolidays: event.shouldBeVisible)));
});
on<DisclaimerDismissed>((event, emit) => add(
Emit((state) => state.copyWith(showDisclaimer: false))
));
}
bool showPastHolidays() => innerState?.showPastHolidays ?? false;
bool showDisclaimerOnEntry() => innerState?.showDisclaimer ?? false;
List<Holiday>? getHolidays() => innerState?.holidays
.where((element) => showPastHolidays() || DateTime.parse(element.end).isAfter(DateTime.now()))
.toList() ?? [];
@override
fromNothing() => const HolidaysState(showPastHolidays: false, holidays: [], showDisclaimer: true);
@override
fromStorage(Map<String, dynamic> json) => HolidaysState.fromJson(json);
@override
Future<void> gatherData() async {
var holidays = await repo.getHolidays();
add(DataGathered((state) => state.copyWith(holidays: holidays)));
}
@override
repository() => HolidaysRepository();
@override
Map<String, dynamic>? toStorage(state) => state.toJson();
}

View File

@ -0,0 +1,9 @@
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import 'holidays_state.dart';
sealed class HolidaysEvent extends LoadableHydratedBlocEvent<HolidaysState> {}
class SetPastHolidaysVisible extends HolidaysEvent {
final bool shouldBeVisible;
SetPastHolidaysVisible(this.shouldBeVisible);
}
class DisclaimerDismissed extends HolidaysEvent {}

View File

@ -0,0 +1,30 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'holidays_state.freezed.dart';
part 'holidays_state.g.dart';
@freezed
class HolidaysState with _$HolidaysState {
const factory HolidaysState({
required bool showPastHolidays,
required bool showDisclaimer,
required List<Holiday> holidays,
}) = _HolidaysState;
factory HolidaysState.fromJson(Map<String, Object?> json) => _$HolidaysStateFromJson(json);
}
@freezed
class Holiday with _$Holiday {
const factory Holiday({
required String start,
required String end,
required int year,
required String stateCode,
required String name,
required String slug,
}) = _Holiday;
factory Holiday.fromJson(Map<String, Object?> json) => _$HolidayFromJson(json);
}

View File

@ -0,0 +1,463 @@
// 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 'holidays_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');
HolidaysState _$HolidaysStateFromJson(Map<String, dynamic> json) {
return _HolidaysState.fromJson(json);
}
/// @nodoc
mixin _$HolidaysState {
bool get showPastHolidays => throw _privateConstructorUsedError;
bool get showDisclaimer => throw _privateConstructorUsedError;
List<Holiday> get holidays => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$HolidaysStateCopyWith<HolidaysState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $HolidaysStateCopyWith<$Res> {
factory $HolidaysStateCopyWith(
HolidaysState value, $Res Function(HolidaysState) then) =
_$HolidaysStateCopyWithImpl<$Res, HolidaysState>;
@useResult
$Res call(
{bool showPastHolidays, bool showDisclaimer, List<Holiday> holidays});
}
/// @nodoc
class _$HolidaysStateCopyWithImpl<$Res, $Val extends HolidaysState>
implements $HolidaysStateCopyWith<$Res> {
_$HolidaysStateCopyWithImpl(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? showPastHolidays = null,
Object? showDisclaimer = null,
Object? holidays = null,
}) {
return _then(_value.copyWith(
showPastHolidays: null == showPastHolidays
? _value.showPastHolidays
: showPastHolidays // ignore: cast_nullable_to_non_nullable
as bool,
showDisclaimer: null == showDisclaimer
? _value.showDisclaimer
: showDisclaimer // ignore: cast_nullable_to_non_nullable
as bool,
holidays: null == holidays
? _value.holidays
: holidays // ignore: cast_nullable_to_non_nullable
as List<Holiday>,
) as $Val);
}
}
/// @nodoc
abstract class _$$HolidaysStateImplCopyWith<$Res>
implements $HolidaysStateCopyWith<$Res> {
factory _$$HolidaysStateImplCopyWith(
_$HolidaysStateImpl value, $Res Function(_$HolidaysStateImpl) then) =
__$$HolidaysStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{bool showPastHolidays, bool showDisclaimer, List<Holiday> holidays});
}
/// @nodoc
class __$$HolidaysStateImplCopyWithImpl<$Res>
extends _$HolidaysStateCopyWithImpl<$Res, _$HolidaysStateImpl>
implements _$$HolidaysStateImplCopyWith<$Res> {
__$$HolidaysStateImplCopyWithImpl(
_$HolidaysStateImpl _value, $Res Function(_$HolidaysStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? showPastHolidays = null,
Object? showDisclaimer = null,
Object? holidays = null,
}) {
return _then(_$HolidaysStateImpl(
showPastHolidays: null == showPastHolidays
? _value.showPastHolidays
: showPastHolidays // ignore: cast_nullable_to_non_nullable
as bool,
showDisclaimer: null == showDisclaimer
? _value.showDisclaimer
: showDisclaimer // ignore: cast_nullable_to_non_nullable
as bool,
holidays: null == holidays
? _value._holidays
: holidays // ignore: cast_nullable_to_non_nullable
as List<Holiday>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$HolidaysStateImpl
with DiagnosticableTreeMixin
implements _HolidaysState {
const _$HolidaysStateImpl(
{required this.showPastHolidays,
required this.showDisclaimer,
required final List<Holiday> holidays})
: _holidays = holidays;
factory _$HolidaysStateImpl.fromJson(Map<String, dynamic> json) =>
_$$HolidaysStateImplFromJson(json);
@override
final bool showPastHolidays;
@override
final bool showDisclaimer;
final List<Holiday> _holidays;
@override
List<Holiday> get holidays {
if (_holidays is EqualUnmodifiableListView) return _holidays;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_holidays);
}
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'HolidaysState(showPastHolidays: $showPastHolidays, showDisclaimer: $showDisclaimer, holidays: $holidays)';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('type', 'HolidaysState'))
..add(DiagnosticsProperty('showPastHolidays', showPastHolidays))
..add(DiagnosticsProperty('showDisclaimer', showDisclaimer))
..add(DiagnosticsProperty('holidays', holidays));
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$HolidaysStateImpl &&
(identical(other.showPastHolidays, showPastHolidays) ||
other.showPastHolidays == showPastHolidays) &&
(identical(other.showDisclaimer, showDisclaimer) ||
other.showDisclaimer == showDisclaimer) &&
const DeepCollectionEquality().equals(other._holidays, _holidays));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, showPastHolidays, showDisclaimer,
const DeepCollectionEquality().hash(_holidays));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$HolidaysStateImplCopyWith<_$HolidaysStateImpl> get copyWith =>
__$$HolidaysStateImplCopyWithImpl<_$HolidaysStateImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$HolidaysStateImplToJson(
this,
);
}
}
abstract class _HolidaysState implements HolidaysState {
const factory _HolidaysState(
{required final bool showPastHolidays,
required final bool showDisclaimer,
required final List<Holiday> holidays}) = _$HolidaysStateImpl;
factory _HolidaysState.fromJson(Map<String, dynamic> json) =
_$HolidaysStateImpl.fromJson;
@override
bool get showPastHolidays;
@override
bool get showDisclaimer;
@override
List<Holiday> get holidays;
@override
@JsonKey(ignore: true)
_$$HolidaysStateImplCopyWith<_$HolidaysStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
Holiday _$HolidayFromJson(Map<String, dynamic> json) {
return _Holiday.fromJson(json);
}
/// @nodoc
mixin _$Holiday {
String get start => throw _privateConstructorUsedError;
String get end => throw _privateConstructorUsedError;
int get year => throw _privateConstructorUsedError;
String get stateCode => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
String get slug => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$HolidayCopyWith<Holiday> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $HolidayCopyWith<$Res> {
factory $HolidayCopyWith(Holiday value, $Res Function(Holiday) then) =
_$HolidayCopyWithImpl<$Res, Holiday>;
@useResult
$Res call(
{String start,
String end,
int year,
String stateCode,
String name,
String slug});
}
/// @nodoc
class _$HolidayCopyWithImpl<$Res, $Val extends Holiday>
implements $HolidayCopyWith<$Res> {
_$HolidayCopyWithImpl(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? start = null,
Object? end = null,
Object? year = null,
Object? stateCode = null,
Object? name = null,
Object? slug = null,
}) {
return _then(_value.copyWith(
start: null == start
? _value.start
: start // ignore: cast_nullable_to_non_nullable
as String,
end: null == end
? _value.end
: end // ignore: cast_nullable_to_non_nullable
as String,
year: null == year
? _value.year
: year // ignore: cast_nullable_to_non_nullable
as int,
stateCode: null == stateCode
? _value.stateCode
: stateCode // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
slug: null == slug
? _value.slug
: slug // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$HolidayImplCopyWith<$Res> implements $HolidayCopyWith<$Res> {
factory _$$HolidayImplCopyWith(
_$HolidayImpl value, $Res Function(_$HolidayImpl) then) =
__$$HolidayImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String start,
String end,
int year,
String stateCode,
String name,
String slug});
}
/// @nodoc
class __$$HolidayImplCopyWithImpl<$Res>
extends _$HolidayCopyWithImpl<$Res, _$HolidayImpl>
implements _$$HolidayImplCopyWith<$Res> {
__$$HolidayImplCopyWithImpl(
_$HolidayImpl _value, $Res Function(_$HolidayImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? start = null,
Object? end = null,
Object? year = null,
Object? stateCode = null,
Object? name = null,
Object? slug = null,
}) {
return _then(_$HolidayImpl(
start: null == start
? _value.start
: start // ignore: cast_nullable_to_non_nullable
as String,
end: null == end
? _value.end
: end // ignore: cast_nullable_to_non_nullable
as String,
year: null == year
? _value.year
: year // ignore: cast_nullable_to_non_nullable
as int,
stateCode: null == stateCode
? _value.stateCode
: stateCode // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
slug: null == slug
? _value.slug
: slug // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$HolidayImpl with DiagnosticableTreeMixin implements _Holiday {
const _$HolidayImpl(
{required this.start,
required this.end,
required this.year,
required this.stateCode,
required this.name,
required this.slug});
factory _$HolidayImpl.fromJson(Map<String, dynamic> json) =>
_$$HolidayImplFromJson(json);
@override
final String start;
@override
final String end;
@override
final int year;
@override
final String stateCode;
@override
final String name;
@override
final String slug;
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'Holiday(start: $start, end: $end, year: $year, stateCode: $stateCode, name: $name, slug: $slug)';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('type', 'Holiday'))
..add(DiagnosticsProperty('start', start))
..add(DiagnosticsProperty('end', end))
..add(DiagnosticsProperty('year', year))
..add(DiagnosticsProperty('stateCode', stateCode))
..add(DiagnosticsProperty('name', name))
..add(DiagnosticsProperty('slug', slug));
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$HolidayImpl &&
(identical(other.start, start) || other.start == start) &&
(identical(other.end, end) || other.end == end) &&
(identical(other.year, year) || other.year == year) &&
(identical(other.stateCode, stateCode) ||
other.stateCode == stateCode) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.slug, slug) || other.slug == slug));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, start, end, year, stateCode, name, slug);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$HolidayImplCopyWith<_$HolidayImpl> get copyWith =>
__$$HolidayImplCopyWithImpl<_$HolidayImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$HolidayImplToJson(
this,
);
}
}
abstract class _Holiday implements Holiday {
const factory _Holiday(
{required final String start,
required final String end,
required final int year,
required final String stateCode,
required final String name,
required final String slug}) = _$HolidayImpl;
factory _Holiday.fromJson(Map<String, dynamic> json) = _$HolidayImpl.fromJson;
@override
String get start;
@override
String get end;
@override
int get year;
@override
String get stateCode;
@override
String get name;
@override
String get slug;
@override
@JsonKey(ignore: true)
_$$HolidayImplCopyWith<_$HolidayImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,43 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'holidays_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$HolidaysStateImpl _$$HolidaysStateImplFromJson(Map<String, dynamic> json) =>
_$HolidaysStateImpl(
showPastHolidays: json['showPastHolidays'] as bool,
showDisclaimer: json['showDisclaimer'] as bool,
holidays: (json['holidays'] as List<dynamic>)
.map((e) => Holiday.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$$HolidaysStateImplToJson(_$HolidaysStateImpl instance) =>
<String, dynamic>{
'showPastHolidays': instance.showPastHolidays,
'showDisclaimer': instance.showDisclaimer,
'holidays': instance.holidays,
};
_$HolidayImpl _$$HolidayImplFromJson(Map<String, dynamic> json) =>
_$HolidayImpl(
start: json['start'] as String,
end: json['end'] as String,
year: json['year'] as int,
stateCode: json['stateCode'] as String,
name: json['name'] as String,
slug: json['slug'] as String,
);
Map<String, dynamic> _$$HolidayImplToJson(_$HolidayImpl instance) =>
<String, dynamic>{
'start': instance.start,
'end': instance.end,
'year': instance.year,
'stateCode': instance.stateCode,
'name': instance.name,
'slug': instance.slug,
};

View File

@ -0,0 +1,13 @@
import 'package:dio/dio.dart';
import '../../../basis/dataloader/holiday_data_loader.dart';
import '../../../infrastructure/dataLoader/data_loader.dart';
import '../bloc/holidays_state.dart';
class HolidaysGetHolidays extends HolidayDataLoader<List<Holiday>> {
@override
List<Holiday> assemble(DataLoaderResult data) => data.asListOfMaps().map(Holiday.fromJson).toList();
@override
Future<Response<String>> fetch() => dio.get('/holidays/HE');
}

View File

@ -0,0 +1,7 @@
import '../../../infrastructure/repository/repository.dart';
import '../bloc/holidays_state.dart';
import '../dataProvider/holidays_get_holidays.dart';
class HolidaysRepository extends Repository<HolidaysState> {
Future<List<Holiday>> getHolidays() => HolidaysGetHolidays().run();
}

View File

@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import '../../../../../widget/animatedTime.dart';
import '../../../../../widget/list_view_util.dart';
import '../../../../../widget/centeredLeading.dart';
import '../../../../../widget/debug/debugTile.dart';
import '../../../../../widget/string_extensions.dart';
import '../../../infrastructure/loadableState/loadable_state.dart';
import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../infrastructure/utilityWidgets/bloc_module.dart';
import '../bloc/holidays_bloc.dart';
import '../bloc/holidays_event.dart';
import '../bloc/holidays_state.dart';
class HolidaysView extends StatelessWidget {
const HolidaysView({super.key});
@override
Widget build(BuildContext context) => BlocModule<HolidaysBloc, LoadableState<HolidaysState>>(
create: (context) => HolidaysBloc(),
autoRebuild: true,
child: (context, bloc, state) {
void showDisclaimer() {
showDialog(context: context, builder: (context) => AlertDialog(
title: const Text('Richtigkeit und Bereitstellung der Daten'),
content: const Text(''
'Sämtliche Datumsangaben sind ohne Gewähr.\n'
'Ich übernehme weder Verantwortung für die Richtigkeit der Daten noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können.\n\n'
'Die Daten stammen von https://ferien-api.de/'),
actions: [
TextButton(child: const Text('Okay'), onPressed: () => Navigator.of(context).pop()),
],
));
}
return Scaffold(
appBar: AppBar(
title: const Text('Schulferien in Hessen'),
actions: [
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: showDisclaimer,
),
PopupMenuButton<bool>(
initialValue: bloc.showPastHolidays(),
icon: const Icon(Icons.history),
itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>(
value: e,
enabled: e != bloc.showPastHolidays(),
child: Row(
children: [
Icon(e ? Icons.history_outlined : Icons.history_toggle_off_outlined, color: Theme.of(context).colorScheme.onSurface),
const SizedBox(width: 15),
Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen')
],
)
)).toList(),
onSelected: (e) => bloc.add(SetPastHolidaysVisible(e)),
),
],
),
body: LoadableStateConsumer<HolidaysBloc, HolidaysState>(
onLoad: (state) {
if(state.showDisclaimer) showDisclaimer();
bloc.add(DisclaimerDismissed());
},
child: (state, loading) => ListViewUtil.fromList<Holiday>(bloc.getHolidays(), (holiday) {
var holidayType = holiday.name.split(' ').first.capitalize();
String formatDate(String date) => Jiffy.parse(date).format(pattern: 'dd.MM.yyyy');
String getYear(String date, {String format = 'yyyy'}) => Jiffy.parse(date).format(pattern: format);
String getHolidayYear(String startDate, String endDate) => getYear(startDate) == getYear(endDate)
? getYear(startDate)
: '${getYear(startDate)}/${getYear(endDate, format: 'yy')}';
return ListTile(
leading: const CenteredLeading(Icon(Icons.calendar_month)),
title: Text('$holidayType ${getHolidayYear(holiday.start, holiday.end)}'),
subtitle: Text('${formatDate(holiday.start)} - ${formatDate(holiday.end)}'),
onTap: () => showDialog(context: context, builder: (context) => SimpleDialog(
title: Text('$holidayType ${holiday.year} in Hessen'),
children: [
ListTile(
leading: const CenteredLeading(Icon(Icons.signpost_outlined)),
title: Text(holiday.name.capitalize()),
subtitle: Text(holiday.slug.capitalize()),
),
ListTile(
leading: const Icon(Icons.date_range_outlined),
title: Text('vom ${formatDate(holiday.start)}'),
),
ListTile(
leading: const Icon(Icons.date_range_outlined),
title: Text('bis zum ${formatDate(holiday.end)}'),
),
Visibility(
visible: !DateTime.parse(holiday.start).difference(DateTime.now()).isNegative,
replacement: ListTile(
leading: const CenteredLeading(Icon(Icons.content_paste_search_outlined)),
title: Text(Jiffy.parse(holiday.start).fromNow()),
),
child: ListTile(
leading: const CenteredLeading(Icon(Icons.timer_outlined)),
title: AnimatedTime(callback: () => DateTime.parse(holiday.start).difference(DateTime.now())),
subtitle: Text(Jiffy.parse(holiday.start).fromNow()),
),
),
DebugTile(context).jsonData(holiday.toJson()),
],
)),
trailing: const Icon(Icons.arrow_right),
);
}),
),
);
},
);
}

View File

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

View File

@ -1,7 +1,7 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../../infrastructure/dataLoader/data_loader.dart'; import '../../../infrastructure/dataLoader/data_loader.dart';
import '../../../infrastructure/dataLoader/mhsl_data_loader.dart'; import '../../../basis/dataloader/mhsl_data_loader.dart';
import '../bloc/marianum_message_state.dart'; import '../bloc/marianum_message_state.dart';
class MarianumMessageGetMessages extends MhslDataLoader<MarianumMessageList> { class MarianumMessageGetMessages extends MhslDataLoader<MarianumMessageList> {

View File

@ -60,12 +60,17 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
Padding( Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: TextField( child: TextField(
onChanged: (value) {
if(value.trim().toLowerCase() == 'ranzig') {
_feedbackInput.text = 'selber';
}
},
controller: _feedbackInput, controller: _feedbackInput,
autofocus: true, autofocus: true,
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
label: const Text('Feedback und Verbesserungen'), label: const Text('Feedback und Verbesserungen'),
errorText: _textFieldEmpty ? 'Bitte gib eine Beschreibung an' : null, errorText: _textFieldEmpty ? 'Bitte gib eine Beschreibung an???' : null,
), ),
minLines: 4, minLines: 4,
maxLines: 7, maxLines: 7,

View File

@ -1,157 +0,0 @@
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:provider/provider.dart';
import '../../../../model/holidays/holidaysProps.dart';
import '../../../../storage/base/settingsProvider.dart';
import '../../../../widget/centeredLeading.dart';
import '../../../../widget/confirmDialog.dart';
import '../../../../widget/debug/debugTile.dart';
import '../../../../widget/loadingSpinner.dart';
import '../../../../widget/placeholderView.dart';
import '../../../../widget/animatedTime.dart';
class Holidays extends StatefulWidget {
const Holidays({super.key});
@override
State<Holidays> createState() => _HolidaysState();
}
extension StringExtension on String {
String capitalize() => '${this[0].toUpperCase()}${substring(1).toLowerCase()}';
}
class _HolidaysState extends State<Holidays> {
late SettingsProvider settings = Provider.of<SettingsProvider>(context, listen: false);
late bool showPastEvents = settings.val().holidaysSettings.showPastEvents;
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Provider.of<HolidaysProps>(context, listen: false).run();
if(!settings.val().holidaysSettings.dismissedDisclaimer) showDisclaimer();
});
super.initState();
}
String parseString(String enDate) => Jiffy.parse(enDate).format(pattern: 'dd.MM.yyyy');
void showDisclaimer() {
showDialog(context: context, builder: (context) => AlertDialog(
title: const Text('Richtigkeit und Bereitstellung der Daten'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(''
'Sämtliche Datumsangaben sind ohne Gewähr.\n'
'Ich übernehme weder Verantwortung für die Richtigkeit der Daten noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können.\n\n'
'Die Daten stammen von https://ferien-api.de/'),
const SizedBox(height: 30),
ListTile(
title: const Text('Diese Meldung nicht mehr anzeigen'),
trailing: Checkbox(
value: settings.val().holidaysSettings.dismissedDisclaimer,
onChanged: (value) => settings.val(write: true).holidaysSettings.dismissedDisclaimer = value!,
),
)
],
),
actions: [
TextButton(child: const Text('ferien-api.de besuchen'), onPressed: () => ConfirmDialog.openBrowser(context, 'https://ferien-api.de/')),
TextButton(child: const Text('Okay'), onPressed: () => Navigator.of(context).pop()),
],
));
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Schulferien in Hessen'),
actions: [
IconButton(
icon: const Icon(Icons.warning_amber_outlined),
onPressed: showDisclaimer,
),
PopupMenuButton<bool>(
initialValue: settings.val().holidaysSettings.showPastEvents,
icon: const Icon(Icons.manage_history_outlined),
itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>(
value: e,
enabled: e != showPastEvents,
child: Row(
children: [
Icon(e ? Icons.history_outlined : Icons.history_toggle_off_outlined, color: Theme.of(context).colorScheme.onSurface),
const SizedBox(width: 15),
Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen')
],
)
)).toList(),
onSelected: (e) {
setState(() {
showPastEvents = e;
settings.val(write: true).holidaysSettings.showPastEvents = e;
});
},
),
],
),
body: Consumer<HolidaysProps>(builder: (context, value, child) {
if(value.primaryLoading()) return const LoadingSpinner();
var holidays = value.getHolidaysResponse.data;
if(!showPastEvents) holidays = holidays.where((element) => DateTime.parse(element.end).isAfter(DateTime.now())).toList();
if(holidays.isEmpty) return const PlaceholderView(icon: Icons.search_off, text: 'Es wurden keine Ferieneinträge gefunden!');
return ListView.builder(
itemCount: holidays.length,
itemBuilder: (context, index) {
var holiday = holidays[index];
var holidayType = holiday.name.split(' ').first.capitalize();
return ListTile(
leading: const CenteredLeading(Icon(Icons.calendar_month)),
title: Text('$holidayType ab ${parseString(holiday.start)}'),
subtitle: Text('bis ${parseString(holiday.end)}'),
onTap: () => showDialog(context: context, builder: (context) => SimpleDialog(
title: Text('$holidayType ${holiday.year} in Hessen'),
children: [
ListTile(
leading: const CenteredLeading(Icon(Icons.signpost_outlined)),
title: Text(holiday.name),
subtitle: Text(holiday.slug),
),
ListTile(
leading: const Icon(Icons.arrow_forward),
title: Text('vom ${parseString(holiday.start)}'),
),
ListTile(
leading: const Icon(Icons.arrow_back),
title: Text('bis zum ${parseString(holiday.end)}'),
),
Visibility(
visible: !DateTime.parse(holiday.start).difference(DateTime.now()).isNegative,
replacement: ListTile(
leading: const CenteredLeading(Icon(Icons.content_paste_search_outlined)),
title: Text(Jiffy.parse(holiday.start).fromNow()),
),
child: ListTile(
leading: const CenteredLeading(Icon(Icons.timer_outlined)),
title: AnimatedTime(callback: () => DateTime.parse(holiday.start).difference(DateTime.now())),
subtitle: Text(Jiffy.parse(holiday.start).fromNow()),
),
),
DebugTile(context).jsonData(holiday.toJson()),
],
)),
trailing: const Icon(Icons.arrow_right),
);
},
);
},
)
);
}

View File

@ -2,7 +2,6 @@ import 'package:better_open_file/better_open_file.dart';
import 'package:bubble/bubble.dart'; import 'package:bubble/bubble.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis;
import 'package:flowder/flowder.dart'; import 'package:flowder/flowder.dart';
import 'package:flutter/cupertino.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';

View File

@ -0,0 +1,9 @@
import 'package:flutter/material.dart';
class ListViewUtil {
static ListView fromList<T>(List<T>? items, Widget Function(T item) map) => ListView.builder(
itemCount: items?.length ?? 0,
itemBuilder: (context, index) => items != null ? map(items[index]) : null,
);
}

View File

@ -0,0 +1,3 @@
extension StringExtensions on String {
String capitalize() => '${this[0].toUpperCase()}${substring(1).toLowerCase()}';
}