bloc for holidays

This commit is contained in:
Elias Müller 2024-06-12 15:53:13 +02:00
parent a33c4ddac5
commit fe93a94fc6
8 changed files with 37 additions and 178 deletions

View File

@ -15,8 +15,9 @@ 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);

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

@ -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, HolidaysView.new), Modules.holidays: AppModule('Schulferien', Icons.time_to_leave, HolidaysView.new),
}; };
static AppModule getModule(Modules module) => modules()[module]!; static AppModule getModule(Modules module) => modules()[module]!;

View File

@ -1,5 +1,3 @@
import 'dart:developer';
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart'; import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import '../repository/holidays_repository.dart'; import '../repository/holidays_repository.dart';
@ -9,7 +7,6 @@ import 'holidays_state.dart';
class HolidaysBloc extends LoadableHydratedBloc<HolidaysEvent, HolidaysState, HolidaysRepository> { class HolidaysBloc extends LoadableHydratedBloc<HolidaysEvent, HolidaysState, HolidaysRepository> {
HolidaysBloc() { HolidaysBloc() {
on<SetPastHolidaysVisible>((event, emit) { on<SetPastHolidaysVisible>((event, emit) {
log('SetPastHolidaysVisible: ${event.shouldBeVisible.toString()}');
add(Emit((state) => state.copyWith(showPastHolidays: event.shouldBeVisible))); add(Emit((state) => state.copyWith(showPastHolidays: event.shouldBeVisible)));
}); });
@ -19,9 +16,9 @@ class HolidaysBloc extends LoadableHydratedBloc<HolidaysEvent, HolidaysState, Ho
} }
bool showPastHolidays() => innerState?.showPastHolidays ?? false; bool showPastHolidays() => innerState?.showPastHolidays ?? false;
List<Holiday>? getHolidays() => innerState?.holidays.where( List<Holiday>? getHolidays() => innerState?.holidays
(element) => showPastHolidays() || DateTime.parse(element.end).isAfter(DateTime.now()) .where((element) => showPastHolidays() || DateTime.parse(element.end).isAfter(DateTime.now()))
).toList(); .toList() ?? [];
@override @override
fromNothing() => const HolidaysState(showPastHolidays: false, holidays: [], showDisclaimer: true); fromNothing() => const HolidaysState(showPastHolidays: false, holidays: [], showDisclaimer: true);

View File

@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import '../../../../../view/pages/more/holidays/holidays.dart';
import '../../../../../widget/animatedTime.dart'; import '../../../../../widget/animatedTime.dart';
import '../../../../../widget/list_view_util.dart'; import '../../../../../widget/list_view_util.dart';
import '../../../../../widget/centeredLeading.dart'; import '../../../../../widget/centeredLeading.dart';
import '../../../../../widget/debug/debugTile.dart'; import '../../../../../widget/debug/debugTile.dart';
import '../../../../../widget/string_extensions.dart';
import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart'; import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../infrastructure/utilityWidgets/bloc_module.dart'; import '../../../infrastructure/utilityWidgets/bloc_module.dart';
import '../bloc/holidays_bloc.dart'; import '../bloc/holidays_bloc.dart';
@ -41,12 +41,12 @@ class HolidaysView extends StatelessWidget {
title: const Text('Schulferien in Hessen'), title: const Text('Schulferien in Hessen'),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.warning_amber_outlined), icon: const Icon(Icons.info_outline),
onPressed: showDisclaimer, onPressed: showDisclaimer,
), ),
PopupMenuButton<bool>( PopupMenuButton<bool>(
initialValue: bloc.showPastHolidays(), initialValue: bloc.showPastHolidays(),
icon: const Icon(Icons.manage_history_outlined), icon: const Icon(Icons.history),
itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>( itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>(
value: e, value: e,
enabled: e != bloc.showPastHolidays(), enabled: e != bloc.showPastHolidays(),
@ -65,26 +65,31 @@ class HolidaysView extends StatelessWidget {
body: LoadableStateConsumer<HolidaysBloc, HolidaysState>( body: LoadableStateConsumer<HolidaysBloc, HolidaysState>(
child: (state, loading) => ListViewUtil.fromList<Holiday>(bloc.getHolidays(), (holiday) { child: (state, loading) => ListViewUtil.fromList<Holiday>(bloc.getHolidays(), (holiday) {
var holidayType = holiday.name.split(' ').first.capitalize(); var holidayType = holiday.name.split(' ').first.capitalize();
String formatDate(String enDate) => Jiffy.parse(enDate).format(pattern: 'dd.MM.yyyy'); 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( return ListTile(
leading: const CenteredLeading(Icon(Icons.calendar_month)), leading: const CenteredLeading(Icon(Icons.calendar_month)),
title: Text('${state.showPastHolidays}$holidayType ab ${formatDate(holiday.start)}'), title: Text('$holidayType ${getHolidayYear(holiday.start, holiday.end)}'),
subtitle: Text('bis ${formatDate(holiday.end)}'), subtitle: Text('${formatDate(holiday.start)} - ${formatDate(holiday.end)}'),
onTap: () => showDialog(context: context, builder: (context) => SimpleDialog( onTap: () => showDialog(context: context, builder: (context) => SimpleDialog(
title: Text('$holidayType ${holiday.year} in Hessen'), title: Text('$holidayType ${holiday.year} in Hessen'),
children: [ children: [
ListTile( ListTile(
leading: const CenteredLeading(Icon(Icons.signpost_outlined)), leading: const CenteredLeading(Icon(Icons.signpost_outlined)),
title: Text(holiday.name), title: Text(holiday.name.capitalize()),
subtitle: Text(holiday.slug), subtitle: Text(holiday.slug.capitalize()),
), ),
ListTile( ListTile(
leading: const Icon(Icons.arrow_forward), leading: const Icon(Icons.date_range_outlined),
title: Text('vom ${formatDate(holiday.start)}'), title: Text('vom ${formatDate(holiday.start)}'),
), ),
ListTile( ListTile(
leading: const Icon(Icons.arrow_back), leading: const Icon(Icons.date_range_outlined),
title: Text('bis zum ${formatDate(holiday.end)}'), title: Text('bis zum ${formatDate(holiday.end)}'),
), ),
Visibility( Visibility(

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

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