diff --git a/lib/bloc/scroll_status/scroll_status_bloc.dart b/lib/bloc/scroll_status/scroll_status_bloc.dart index 6f9d6a98..b1e4e604 100644 --- a/lib/bloc/scroll_status/scroll_status_bloc.dart +++ b/lib/bloc/scroll_status/scroll_status_bloc.dart @@ -7,7 +7,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; part 'scroll_status_event.dart'; - part 'scroll_status_state.dart'; class ScrollStatusBloc extends Bloc { diff --git a/lib/bloc/theme/theme_state.dart b/lib/bloc/theme/theme_state.dart index c5442111..c3e840a3 100644 --- a/lib/bloc/theme/theme_state.dart +++ b/lib/bloc/theme/theme_state.dart @@ -12,10 +12,4 @@ class ThemeState { themeData: themeData ?? this.themeData, ); } - - IconData getIconData() { - return themeData == AppTheme.lightTheme - ? Icons.brightness_low - : Icons.dark_mode_outlined; - } } diff --git a/lib/chabo.dart b/lib/chabo.dart index 254b9bb5..9fc3a444 100644 --- a/lib/chabo.dart +++ b/lib/chabo.dart @@ -6,7 +6,9 @@ import 'package:chabo/bloc/status/status_bloc.dart'; import 'package:chabo/bloc/theme/theme_bloc.dart'; import 'package:chabo/bloc/time_slots/time_slots_bloc.dart'; import 'package:chabo/cubits/floating_actions_cubit.dart'; +import 'package:chabo/cubits/time_format_cubit.dart'; import 'package:chabo/helpers/device_helper.dart'; +import 'package:chabo/models/enums/time_format.dart'; import 'package:chabo/screens/forecast_screen.dart'; import 'package:chabo/service/notification_service.dart'; import 'package:chabo/service/storage_service.dart'; @@ -50,6 +52,14 @@ class Chabo extends StatelessWidget { )..init(), ), + /// Bloc intended to manage the displayed time format + BlocProvider( + create: (_) => TimeFormatCubit( + storageService, + const TimeFormatState(timeFormat: TimeFormat.twentyFourHours), + )..init(), + ), + /// Bloc intended to manage the forecast displayed BlocProvider( create: (_) => ForecastBloc( diff --git a/lib/const.dart b/lib/const.dart index 8dd80967..db68d221 100644 --- a/lib/const.dart +++ b/lib/const.dart @@ -1,4 +1,5 @@ import 'package:chabo/models/enums/day.dart'; +import 'package:chabo/models/enums/time_format.dart'; import 'package:chabo/models/time_slot.dart'; import 'package:chabo/models/web_link_icon.dart'; import 'package:flutter/material.dart'; @@ -83,6 +84,7 @@ class Const { 'NOTIFICATION_FAVORITE_SLOTS_SETTINGS_VALUE'; static const String notificationFavoriteSlotsDaysValueKey = 'NOTIFICATION_FAVORITE_SLOTS_DAYS_SETTINGS_VALUE'; + static const String timeFormatKey = 'TIME_FORMAT'; /// Notifications static const String androidAppLogoPath = @@ -128,6 +130,7 @@ class Const { /// UI static const bool isRightHandedDefaultValue = true; + static const TimeFormat timeFormatDefaultValue = TimeFormat.twentyFourHours; /// Android Notifications static const String androidTicket = 'ticker'; diff --git a/lib/cubits/time_format_cubit.dart b/lib/cubits/time_format_cubit.dart new file mode 100644 index 00000000..e5548021 --- /dev/null +++ b/lib/cubits/time_format_cubit.dart @@ -0,0 +1,50 @@ +import 'package:chabo/const.dart'; +import 'package:chabo/models/enums/time_format.dart'; +import 'package:chabo/service/storage_service.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class TimeFormatCubit extends Cubit { + final StorageService storageService; + + TimeFormatCubit(this.storageService, super.initialState); + + void setTimeFormat() { + final newDateFormat = state.timeFormat == TimeFormat.twentyFourHours + ? TimeFormat.twelveHours + : TimeFormat.twentyFourHours; + storageService.saveTimeFormat(Const.timeFormatKey, newDateFormat); + emit( + state.copyWith( + timeFormat: newDateFormat, + ), + ); + } + + void init() { + final timeFormat = storageService.readTimeFormat(Const.timeFormatKey) ?? + Const.timeFormatDefaultValue; + emit( + state.copyWith( + timeFormat: timeFormat, + ), + ); + } +} + +class TimeFormatState extends Equatable { + final TimeFormat timeFormat; + + const TimeFormatState({ + required this.timeFormat, + }); + + TimeFormatState copyWith({TimeFormat? timeFormat}) { + return TimeFormatState( + timeFormat: timeFormat ?? this.timeFormat, + ); + } + + @override + List get props => [timeFormat]; +} diff --git a/lib/dialogs/days_of_the_week_dialog.dart b/lib/dialogs/days_of_the_week_dialog.dart index 914d55ee..54646b4e 100644 --- a/lib/dialogs/days_of_the_week_dialog.dart +++ b/lib/dialogs/days_of_the_week_dialog.dart @@ -1,5 +1,7 @@ import 'package:chabo/bloc/notification/notification_bloc.dart'; +import 'package:chabo/cubits/time_format_cubit.dart'; import 'package:chabo/custom_properties.dart'; +import 'package:chabo/extensions/time_of_day_extension.dart'; import 'package:chabo/models/enums/day.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -19,81 +21,86 @@ class DaysOfTheWeekDialog extends StatelessWidget { ), content: BlocBuilder( builder: (context, state) { - return Wrap( - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 5, - runSpacing: 10, - children: [ - ElevatedButton( - onPressed: () {}, // ignore: no-empty-block - child: DropdownButtonHideUnderline( - child: DropdownButton( - borderRadius: BorderRadius.circular(12.0), - onChanged: (Day? value) { - if (value != null) { - BlocProvider.of(context).add( - DayNotificationValueEvent( - day: value, - ), - ); - } - }, - value: state.dayNotificationValue, - items: Day.values - .map( - (day) => DropdownMenuItem( - value: day, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - day.localizedName(context), - style: const TextStyle( - fontWeight: FontWeight.bold, + return BlocBuilder( + builder: (context, timeFormatState) { + return Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 5, + runSpacing: 10, + children: [ + ElevatedButton( + onPressed: () {}, // ignore: no-empty-block + child: DropdownButtonHideUnderline( + child: DropdownButton( + borderRadius: BorderRadius.circular(12.0), + onChanged: (Day? value) { + if (value != null) { + BlocProvider.of(context).add( + DayNotificationValueEvent( + day: value, + ), + ); + } + }, + value: state.dayNotificationValue, + items: Day.values + .map( + (day) => DropdownMenuItem( + value: day, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + day.localizedName(context), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), ), ), - ), - ), - ) - .toList(), + ) + .toList(), + ), + ), ), - ), - ), - Text( - ' ${AppLocalizations.of(context)!.dayNotificationAt} ', - style: Theme.of(context).textTheme.titleMedium, - ), - ElevatedButton( - onPressed: () { - showTimePicker( - initialEntryMode: TimePickerEntryMode.dial, - context: context, - initialTime: state.dayNotificationTimeValue, - builder: (BuildContext context, Widget? child) { - return MediaQuery( - data: MediaQuery.of(context), - child: child!, - ); - }, - ).then( - (value) => { - if (value != null) - { - BlocProvider.of(context).add( - DayNotificationTimeValueEvent( - time: value, - ), - ), + Text( + ' ${AppLocalizations.of(context)!.dayNotificationAt} ', + style: Theme.of(context).textTheme.titleMedium, + ), + ElevatedButton( + onPressed: () { + showTimePicker( + initialEntryMode: TimePickerEntryMode.dial, + context: context, + initialTime: state.dayNotificationTimeValue, + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context), + child: child!, + ); + }, + ).then( + (value) => { + if (value != null) + { + BlocProvider.of(context).add( + DayNotificationTimeValueEvent( + time: value, + ), + ), + }, }, + ); }, - ); - }, - child: Text( - state.dayNotificationTimeValue.format(context), - style: Theme.of(context).textTheme.titleMedium, - ), - ), - ], + child: Text( + state.dayNotificationTimeValue + .toFormattedString(timeFormatState.timeFormat), + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ], + ); + }, ); }, ), diff --git a/lib/dialogs/time_slot_dialog.dart b/lib/dialogs/time_slot_dialog.dart index 48298391..46e736f2 100644 --- a/lib/dialogs/time_slot_dialog.dart +++ b/lib/dialogs/time_slot_dialog.dart @@ -1,5 +1,8 @@ import 'package:chabo/bloc/time_slots/time_slots_bloc.dart'; +import 'package:chabo/cubits/time_format_cubit.dart'; import 'package:chabo/custom_properties.dart'; +import 'package:chabo/extensions/time_of_day_extension.dart'; +import 'package:chabo/models/enums/time_format.dart'; import 'package:chabo/models/time_slot.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -22,88 +25,100 @@ class TimeSlotDialog extends StatelessWidget { ), ), content: BlocBuilder( - builder: (context, state) { - return Wrap( - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 5, - runSpacing: 10, - children: [ - Text( - ' ${AppLocalizations.of(context)!.favoriteSlotsFrom} ', - style: textTheme.titleMedium, - ), - ElevatedButton( - onPressed: () { - showTimePicker( - initialEntryMode: TimePickerEntryMode.dialOnly, - context: context, - initialTime: state.timeSlots[index].from, - builder: (BuildContext context, Widget? child) { - return MediaQuery( - data: MediaQuery.of(context), - child: child!, - ); - }, - ).then((value) => { - if (value != null) - { - BlocProvider.of(context).add( - TimeSlotChanged( - timeSlot: TimeSlot( - name: state.timeSlots[index].name, - from: value, - to: state.timeSlots[index].to, + builder: (context, timeSlotState) { + return BlocBuilder( + builder: (context, timeFormatState) { + return Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 5, + runSpacing: 10, + children: [ + Text( + ' ${AppLocalizations.of(context)!.favoriteSlotsFrom} ', + style: textTheme.titleMedium, + ), + ElevatedButton( + onPressed: () { + showTimePicker( + initialEntryMode: TimePickerEntryMode.dialOnly, + context: context, + initialTime: timeSlotState.timeSlots[index].from, + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + alwaysUse24HourFormat: + timeFormatState.timeFormat == + TimeFormat.twentyFourHours), + child: child!, + ); + }, + ).then((value) => { + if (value != null) + { + BlocProvider.of(context).add( + TimeSlotChanged( + timeSlot: TimeSlot( + name: timeSlotState.timeSlots[index].name, + from: value, + to: timeSlotState.timeSlots[index].to, + ), + index: index, + ), ), - index: index, - ), - ), - }, - }); - }, - child: Text( - state.timeSlots[index].from.format(context), - style: textTheme.titleMedium, - ), - ), - Text( - ' ${AppLocalizations.of(context)!.favoriteSlotsTo} ', - style: textTheme.titleMedium, - ), - ElevatedButton( - onPressed: () { - showTimePicker( - initialEntryMode: TimePickerEntryMode.dialOnly, - context: context, - initialTime: state.timeSlots[index].to, - builder: (BuildContext context, Widget? child) { - return MediaQuery( - data: MediaQuery.of(context), - child: child!, - ); + }, + }); }, - ).then((value) => { - if (value != null) - { - BlocProvider.of(context).add( - TimeSlotChanged( - timeSlot: TimeSlot( - name: state.timeSlots[index].name, - from: state.timeSlots[index].from, - to: value, + child: Text( + timeSlotState.timeSlots[index].from + .toFormattedString(timeFormatState.timeFormat), + style: textTheme.titleMedium, + ), + ), + Text( + ' ${AppLocalizations.of(context)!.favoriteSlotsTo} ', + style: textTheme.titleMedium, + ), + ElevatedButton( + onPressed: () { + showTimePicker( + initialEntryMode: TimePickerEntryMode.dialOnly, + context: context, + initialTime: timeSlotState.timeSlots[index].to, + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + alwaysUse24HourFormat: + timeFormatState.timeFormat == + TimeFormat.twentyFourHours), + child: child!, + ); + }, + ).then((value) => { + if (value != null) + { + BlocProvider.of(context).add( + TimeSlotChanged( + timeSlot: TimeSlot( + name: timeSlotState.timeSlots[index].name, + from: timeSlotState.timeSlots[index].from, + to: value, + ), + index: index, + ), ), - index: index, - ), - ), - }, - }); - }, - child: Text( - state.timeSlots[index].to.format(context), - style: textTheme.titleMedium, - ), - ), - ], + }, + }); + }, + child: Text( + timeSlotState.timeSlots[index].to + .toFormattedString(timeFormatState.timeFormat), + style: textTheme.titleMedium, + ), + ), + ], + ); + }, ); }, ), diff --git a/lib/extensions/date_time_extension.dart b/lib/extensions/date_time_extension.dart index 0d7875a5..26e3941b 100644 --- a/lib/extensions/date_time_extension.dart +++ b/lib/extensions/date_time_extension.dart @@ -1,6 +1,9 @@ +import 'package:chabo/cubits/time_format_cubit.dart'; import 'package:chabo/models/enums/day.dart'; +import 'package:chabo/models/enums/time_format.dart'; import 'package:enum_to_string/enum_to_string.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; extension DateTimeExtension on DateTime { @@ -37,8 +40,9 @@ extension DateTimeExtension on DateTime { } TextSpan toLocalizedTextSpan(BuildContext context, Color foregroundColor) { + final timeFormat = context.read().state.timeFormat; final languageCode = Localizations.localeOf(context).languageCode; - var stringDate = DateFormat.jm(languageCode).format( + var stringDate = DateFormat(timeFormat.icuName, languageCode).format( this, ); var timeMarker = ''; diff --git a/lib/extensions/time_of_day_extension.dart b/lib/extensions/time_of_day_extension.dart new file mode 100644 index 00000000..5f3d9545 --- /dev/null +++ b/lib/extensions/time_of_day_extension.dart @@ -0,0 +1,11 @@ +import 'package:chabo/models/enums/time_format.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +extension TilmeOfDayExtension on TimeOfDay { + String toFormattedString(TimeFormat timeFormat) { + final now = DateTime.now(); + final dt = DateTime(now.year, now.month, now.day, hour, minute); + return DateFormat(timeFormat.icuName).format(dt); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5fc5343f..ad46d55e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -23,7 +23,7 @@ "theChabanBridgeIsClosed": "the Chaban bridge is closed to traffic", "theChabanBridgeWillSoonClose": "the Chaban bridge will soon close to traffic", "willSoonClose": "close to traffic soon", - "settingsTitle": "Settings", + "settingsClose": "Close", "notificationsTitle": "Notifications", "information": "Information", "dialogInformationContentThe": "", @@ -55,8 +55,8 @@ "informationAboutTheApp": "Information about the app", "about": "About", "disclaimer": "Disclaimer: provisional closures. Subject to confirmation from the Harbor Master's Office.", - "themeSetting": "Theme", - "themeSettingSubtitle": "Set the theme of the app", + "openSetting": "Settings", + "themeSettingSubtitle": "Application theme", "lightTheme": "Light theme", "darkTheme": "Dark theme", "systemTheme": "System theme", @@ -222,5 +222,6 @@ }, "wineFestivalSailBoats": "Wine Festival Sailboats", "externalLinks": "External links", - "rate": "Rate" + "rate": "Rate", + "timeFormatSubTitle": "Time format" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index fa1ddbb1..95a8bc05 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -23,7 +23,7 @@ "theChabanBridgeIsClosed": "el puente Chaban está cerrado al tráfico", "theChabanBridgeWillSoonClose": "el puente Chaban cerca del tráfico pronto", "willSoonClose": "cerca del tráfico pronto", - "settingsTitle": "Ajustes", + "settingsClose": "Cerca", "notificationsTitle": "Notificaciónes", "information": "Información", "dialogInformationContentThe": "el ", @@ -55,8 +55,8 @@ "informationAboutTheApp": "Información sobre la aplicación", "about": "Acerca de", "disclaimer": "Descargo de responsabilidad: cierres provisionales. Sujeto a confirmación de la Capitanía de Puerto.", - "themeSetting": "Tema", - "themeSettingSubtitle": "Establezca el tema de la aplicación", + "openSetting": "Ajustes", + "themeSettingSubtitle": "Tema de la aplicación", "lightTheme": "Tema claro", "darkTheme": "Tema oscuro", "systemTheme": "Tema del sistema", @@ -222,5 +222,6 @@ }, "wineFestivalSailBoats": "Veleros de la Fiesta del Vino", "externalLinks": "Enlaces externos", - "rate": "Califica" + "rate": "Califica", + "timeFormatSubTitle": "Formato de hora" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 37f953ad..9fe66d1b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -23,7 +23,7 @@ "theChabanBridgeIsClosed": "le pont Chaban est fermé à la circulation", "theChabanBridgeWillSoonClose": "le pont Chaban ferme bientôt à la circulation", "willSoonClose": "ferme bientôt à la circulation", - "settingsTitle": "Paramètres", + "settingsClose": "Fermer", "notificationsTitle": "Notifications", "information": "Information", "dialogInformationContentThe": "le ", @@ -55,8 +55,8 @@ "informationAboutTheApp": "Informations concernant l'application", "about": "À propos", "disclaimer": "Avis de non-responsabilité : fermetures provisoires. Sous réserve de confirmation de la Capitainerie.", - "themeSetting": "Thème", - "themeSettingSubtitle": "Choisir le thème de l'application", + "openSetting": "Paramètres", + "themeSettingSubtitle": "Thème de l'application", "lightTheme": "Thème clair", "darkTheme": "Thème sombre", "systemTheme": "Thème système", @@ -222,5 +222,6 @@ }, "wineFestivalSailBoats": "Voiliers de la fête du vin", "externalLinks": "Liens externes", - "rate": "Noter" + "rate": "Noter", + "timeFormatSubTitle": "Format d'affichage des heures" } diff --git a/lib/models/abstract_forecast.dart b/lib/models/abstract_forecast.dart index cdf0525d..c1335ec3 100644 --- a/lib/models/abstract_forecast.dart +++ b/lib/models/abstract_forecast.dart @@ -1,4 +1,5 @@ import 'package:chabo/bloc/time_slots/time_slots_bloc.dart'; +import 'package:chabo/cubits/time_format_cubit.dart'; import 'package:chabo/custom_properties.dart'; import 'package:chabo/extensions/color_scheme_extension.dart'; import 'package:chabo/extensions/date_time_extension.dart'; @@ -6,9 +7,11 @@ import 'package:chabo/extensions/string_extension.dart'; import 'package:chabo/models/enums/day.dart'; import 'package:chabo/models/enums/forecast_closing_reason.dart'; import 'package:chabo/models/enums/forecast_closing_type.dart'; +import 'package:chabo/models/enums/time_format.dart'; import 'package:chabo/models/time_slot.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; @@ -63,6 +66,7 @@ abstract class AbstractForecast extends Equatable { List getCoreInformationWidget(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; + final timeFormat = context.read().state.timeFormat; var infoFromString = AppLocalizations.of(context)!.dialogInformationContentThe.capitalize(); @@ -70,9 +74,9 @@ abstract class AbstractForecast extends Equatable { ' ${AppLocalizations.of(context)!.dialogInformationContentFromStart} '; var infoToString2 = ' ${AppLocalizations.of(context)!.dialogInformationContentFromEnd} '; - var circulationReOpeningDateString = - DateFormat.jm(Localizations.localeOf(context).languageCode) - .format(circulationReOpeningDate); + var circulationReOpeningDateString = DateFormat( + timeFormat.icuName, Localizations.localeOf(context).languageCode) + .format(circulationReOpeningDate); if (isDuringTwoDays) { infoFromString = AppLocalizations.of(context)! .dialogInformationContentThe2 @@ -109,7 +113,8 @@ abstract class AbstractForecast extends Equatable { color: colorScheme.error, ), child: Text( - DateFormat.jm(Localizations.localeOf(context).languageCode) + DateFormat(timeFormat.icuName, + Localizations.localeOf(context).languageCode) .format(circulationClosingDate), style: TextStyle( fontWeight: FontWeight.bold, diff --git a/lib/models/boat_forecast.dart b/lib/models/boat_forecast.dart index 347f4e26..6f5b8764 100644 --- a/lib/models/boat_forecast.dart +++ b/lib/models/boat_forecast.dart @@ -1,3 +1,4 @@ +import 'package:chabo/cubits/time_format_cubit.dart'; import 'package:chabo/extensions/boats_extension.dart'; import 'package:chabo/extensions/color_scheme_extension.dart'; import 'package:chabo/extensions/duration_extension.dart'; @@ -6,7 +7,9 @@ import 'package:chabo/models/abstract_forecast.dart'; import 'package:chabo/models/boat.dart'; import 'package:chabo/models/enums/forecast_closing_reason.dart'; import 'package:chabo/models/enums/forecast_closing_type.dart'; +import 'package:chabo/models/enums/time_format.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; @@ -88,16 +91,17 @@ class BoatForecast extends AbstractForecast { @override RichText getInformationWidget(BuildContext context) { + final timeFormat = context.read().state.timeFormat; final colorScheme = Theme.of(context).colorScheme; var schedule = circulationClosingDate .add(Duration(microseconds: closedDuration.inMicroseconds ~/ 2)); - var scheduleString = - DateFormat.jm(Localizations.localeOf(context).languageCode) - .format(schedule); + var scheduleString = DateFormat( + timeFormat.icuName, Localizations.localeOf(context).languageCode) + .format(schedule); if (isDuringTwoDays) { scheduleString = - '${MaterialLocalizations.of(context).formatMediumDate(schedule)} ${AppLocalizations.of(context)!.at} ${DateFormat.jm(Localizations.localeOf(context).languageCode).format(schedule)}'; + '${MaterialLocalizations.of(context).formatMediumDate(schedule)} ${AppLocalizations.of(context)!.at} ${DateFormat(timeFormat.icuName, Localizations.localeOf(context).languageCode).format(schedule)}'; } return RichText( @@ -136,9 +140,10 @@ class BoatForecast extends AbstractForecast { @override String getNotificationTimeMessage(BuildContext context) { + final timeFormat = context.read().state.timeFormat; return AppLocalizations.of(context)!.notificationTimeBoatMessage( boats.toLocalizedString(context), - DateFormat.Hm().format(circulationClosingDate), + DateFormat(timeFormat.icuName).format(circulationClosingDate), closedDuration.durationToString(context), ); } diff --git a/lib/models/enums/time_format.dart b/lib/models/enums/time_format.dart new file mode 100644 index 00000000..ed461fb2 --- /dev/null +++ b/lib/models/enums/time_format.dart @@ -0,0 +1,28 @@ +enum TimeFormat { + twelveHours, + twentyFourHours; +} + +extension TimeFormatExtension on TimeFormat? { + String get text { + switch (this) { + case TimeFormat.twelveHours: + return '12h'; + case TimeFormat.twentyFourHours: + return '24h'; + default: + return 'no_value'; + } + } + + String get icuName { + switch (this) { + case TimeFormat.twelveHours: + return 'h:mm a'; + case TimeFormat.twentyFourHours: + return 'HH:mm'; + default: + return 'no_value'; + } + } +} diff --git a/lib/models/maintenance_forecast.dart b/lib/models/maintenance_forecast.dart index 40d48591..0e3585f7 100644 --- a/lib/models/maintenance_forecast.dart +++ b/lib/models/maintenance_forecast.dart @@ -1,9 +1,12 @@ +import 'package:chabo/cubits/time_format_cubit.dart'; import 'package:chabo/extensions/color_scheme_extension.dart'; import 'package:chabo/extensions/duration_extension.dart'; import 'package:chabo/models/abstract_forecast.dart'; import 'package:chabo/models/enums/forecast_closing_reason.dart'; import 'package:chabo/models/enums/forecast_closing_type.dart'; +import 'package:chabo/models/enums/time_format.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; @@ -73,8 +76,9 @@ class MaintenanceForecast extends AbstractForecast { @override String getNotificationTimeMessage(BuildContext context) { + final timeFormat = context.read().state.timeFormat; return AppLocalizations.of(context)!.notificationTimeMaintenanceMessage( - DateFormat.Hm().format(circulationClosingDate), + DateFormat(timeFormat.icuName).format(circulationClosingDate), closedDuration.durationToString(context), ); } diff --git a/lib/screens/notification_screen/notification_screen.dart b/lib/screens/notification_screen/notification_screen.dart index 3412c36c..1bb4ca81 100644 --- a/lib/screens/notification_screen/notification_screen.dart +++ b/lib/screens/notification_screen/notification_screen.dart @@ -3,13 +3,16 @@ import 'dart:ui'; import 'package:chabo/bloc/notification/notification_bloc.dart'; import 'package:chabo/bloc/time_slots/time_slots_bloc.dart'; import 'package:chabo/cubits/floating_actions_cubit.dart'; +import 'package:chabo/cubits/time_format_cubit.dart'; import 'package:chabo/custom_properties.dart'; import 'package:chabo/custom_widget_state.dart'; import 'package:chabo/dialogs/days_of_the_week_dialog.dart'; import 'package:chabo/extensions/color_scheme_extension.dart'; import 'package:chabo/extensions/duration_extension.dart'; +import 'package:chabo/extensions/time_of_day_extension.dart'; import 'package:chabo/misc/no_scaling_animation.dart'; import 'package:chabo/models/enums/day.dart'; +import 'package:chabo/models/enums/time_format.dart'; import 'package:chabo/widgets/time_slot_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -77,215 +80,229 @@ class _NotificationScreenState extends CustomWidgetState { ), child: BlocBuilder( builder: (context, notificationState) { - return Column( - children: [ - _FavoriteSlotsWidget( - highlightTimeSlots: widget.highlightTimeSlots ?? false, - timeSlotsEnabledForNotifications: - notificationState.timeSlotsEnabledForNotifications, - ), - const SizedBox( - height: 10, - ), - const Divider( - height: 5, - indent: 25, - endIndent: 25, - ), - const SizedBox( - height: 10, - ), - _CustomListTileWidget( - onChanged: (bool value) => - BlocProvider.of(context).add( - OpeningNotificationStateEvent( - enabled: value, + return BlocBuilder( + builder: (context, timeFormatState) { + return Column( + children: [ + _FavoriteSlotsWidget( + highlightTimeSlots: + widget.highlightTimeSlots ?? false, + timeSlotsEnabledForNotifications: notificationState + .timeSlotsEnabledForNotifications, ), - ), - enabled: notificationState.openingNotificationEnabled, - title: AppLocalizations.of(context)! - .openingNotificationTitle, - subtitle: AppLocalizations.of(context)! - .openingNotificationExplanation, - leadingIcon: Icons.check_circle, - iconColor: Colors.green, - constrainedBySlots: - notificationState.timeSlotsEnabledForNotifications, - ), - _CustomListTileWidget( - onChanged: (bool value) => - BlocProvider.of(context).add( - ClosingNotificationStateEvent( - enabled: value, + const SizedBox( + height: 10, ), - ), - enabled: notificationState.closingNotificationEnabled, - title: AppLocalizations.of(context)! - .closingNotificationTitle, - subtitle: AppLocalizations.of(context)! - .closingNotificationExplanation, - leadingIcon: Icons.block_rounded, - iconColor: Colors.red, - constrainedBySlots: - notificationState.timeSlotsEnabledForNotifications, - ), - const SizedBox( - height: 20, - ), - _CustomListTileWidget( - onTap: () { - showTimePicker( - initialEntryMode: TimePickerEntryMode.dial, - context: context, - initialTime: notificationState - .durationNotificationValue - .durationToTimeOfDay(), - builder: (BuildContext context, Widget? child) { - return MediaQuery( - data: MediaQuery.of(context).copyWith( - alwaysUse24HourFormat: true, - ), - child: child!, - ); - }, - ).then( - (value) => { - if (value != null) - { - BlocProvider.of(context).add( - DurationNotificationValueEvent( - duration: Duration( - hours: value.hour, - minutes: value.minute, - ), + const Divider( + height: 5, + indent: 25, + endIndent: 25, + ), + const SizedBox( + height: 10, + ), + _CustomListTileWidget( + onChanged: (bool value) => + BlocProvider.of(context).add( + OpeningNotificationStateEvent( + enabled: value, + ), + ), + enabled: notificationState.openingNotificationEnabled, + title: AppLocalizations.of(context)! + .openingNotificationTitle, + subtitle: AppLocalizations.of(context)! + .openingNotificationExplanation, + leadingIcon: Icons.check_circle, + iconColor: Colors.green, + constrainedBySlots: notificationState + .timeSlotsEnabledForNotifications, + ), + _CustomListTileWidget( + onChanged: (bool value) => + BlocProvider.of(context).add( + ClosingNotificationStateEvent( + enabled: value, + ), + ), + enabled: notificationState.closingNotificationEnabled, + title: AppLocalizations.of(context)! + .closingNotificationTitle, + subtitle: AppLocalizations.of(context)! + .closingNotificationExplanation, + leadingIcon: Icons.block_rounded, + iconColor: Colors.red, + constrainedBySlots: notificationState + .timeSlotsEnabledForNotifications, + ), + const SizedBox( + height: 20, + ), + _CustomListTileWidget( + onTap: () { + showTimePicker( + initialEntryMode: TimePickerEntryMode.dial, + context: context, + initialTime: notificationState + .durationNotificationValue + .durationToTimeOfDay(), + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + alwaysUse24HourFormat: true, ), - ), + child: child!, + ); }, - }, - ); - }, - onChanged: (bool value) => - BlocProvider.of(context).add( - DurationNotificationStateEvent( - enabled: value, - ), - ), - enabled: notificationState.durationNotificationEnabled, - title: AppLocalizations.of(context)! - .durationNotificationTitle( - notificationState.durationNotificationValue - .durationToString(context), - ), - subtitle: AppLocalizations.of(context)! - .durationNotificationExplanation( - notificationState.durationNotificationValue - .durationToString(context), - ), - leadingIcon: Icons.timer_outlined, - constrainedBySlots: - notificationState.timeSlotsEnabledForNotifications, - ), - _CustomListTileWidget( - onTap: () { - showTimePicker( - initialEntryMode: TimePickerEntryMode.dial, - context: context, - initialTime: notificationState.timeNotificationValue, - builder: (BuildContext context, Widget? child) { - return MediaQuery( - data: MediaQuery.of(context).copyWith( - alwaysUse24HourFormat: false, - ), - child: child!, - ); - }, - ).then( - (value) => { - if (value != null) - { - BlocProvider.of(context).add( - TimeNotificationValueEvent( - time: TimeOfDay( - hour: value.hour, - minute: value.minute, + ).then( + (value) => { + if (value != null) + { + BlocProvider.of(context) + .add( + DurationNotificationValueEvent( + duration: Duration( + hours: value.hour, + minutes: value.minute, + ), + ), ), - ), - ), + }, }, + ); }, - ); - }, - onChanged: (bool value) => - BlocProvider.of(context).add( - TimeNotificationStateEvent( - enabled: value, + onChanged: (bool value) => + BlocProvider.of(context).add( + DurationNotificationStateEvent( + enabled: value, + ), + ), + enabled: + notificationState.durationNotificationEnabled, + title: AppLocalizations.of(context)! + .durationNotificationTitle( + notificationState.durationNotificationValue + .durationToString(context), + ), + subtitle: AppLocalizations.of(context)! + .durationNotificationExplanation( + notificationState.durationNotificationValue + .durationToString(context), + ), + leadingIcon: Icons.timer_outlined, + constrainedBySlots: notificationState + .timeSlotsEnabledForNotifications, ), - ), - enabled: notificationState.timeNotificationEnabled, - title: - AppLocalizations.of(context)!.timeNotificationTitle( - notificationState.timeNotificationValue.format(context), - ), - subtitle: AppLocalizations.of(context)! - .timeNotificationExplanation( - notificationState.timeNotificationValue.format(context), - ), - leadingIcon: Icons.plus_one_outlined, - constrainedBySlots: - notificationState.timeSlotsEnabledForNotifications, - ), - _CustomListTileWidget( - onTap: () { - showDialog( - context: context, - builder: ( - BuildContext context, - ) { - return BackdropFilter( - filter: ImageFilter.blur( - sigmaX: CustomProperties.blurSigmaX, - sigmaY: CustomProperties.blurSigmaY, - ), - child: const DaysOfTheWeekDialog(), + _CustomListTileWidget( + onTap: () { + showTimePicker( + initialEntryMode: TimePickerEntryMode.dial, + context: context, + initialTime: + notificationState.timeNotificationValue, + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + alwaysUse24HourFormat: + timeFormatState.timeFormat == + TimeFormat.twentyFourHours, + ), + child: child!, + ); + }, + ).then( + (value) => { + if (value != null) + { + BlocProvider.of(context) + .add( + TimeNotificationValueEvent( + time: TimeOfDay( + hour: value.hour, + minute: value.minute, + ), + ), + ), + }, + }, ); }, - ).then( - (value) => { - if (value != null) - { - BlocProvider.of(context).add( - DayNotificationValueEvent( - day: value, + onChanged: (bool value) => + BlocProvider.of(context).add( + TimeNotificationStateEvent( + enabled: value, + ), + ), + enabled: notificationState.timeNotificationEnabled, + title: AppLocalizations.of(context)! + .timeNotificationTitle( + notificationState.timeNotificationValue + .toFormattedString(timeFormatState.timeFormat), + ), + subtitle: AppLocalizations.of(context)! + .timeNotificationExplanation( + notificationState.timeNotificationValue + .toFormattedString(timeFormatState.timeFormat), + ), + leadingIcon: Icons.plus_one_outlined, + constrainedBySlots: notificationState + .timeSlotsEnabledForNotifications, + ), + _CustomListTileWidget( + onTap: () { + showDialog( + context: context, + builder: ( + BuildContext context, + ) { + return BackdropFilter( + filter: ImageFilter.blur( + sigmaX: CustomProperties.blurSigmaX, + sigmaY: CustomProperties.blurSigmaY, ), - ), + child: const DaysOfTheWeekDialog(), + ); + }, + ).then( + (value) => { + if (value != null) + { + BlocProvider.of(context) + .add( + DayNotificationValueEvent( + day: value, + ), + ), + }, }, + ); }, - ); - }, - enabled: notificationState.dayNotificationEnabled, - title: AppLocalizations.of(context)!.dayNotificationTitle( - notificationState.dayNotificationValue - .localizedName(context), - ), - subtitle: AppLocalizations.of(context)! - .dayNotificationExplanation( - notificationState.dayNotificationValue - .localizedName(context), - notificationState.dayNotificationTimeValue.format( - context, - ), - ), - leadingIcon: Icons.calendar_month_outlined, - onChanged: (bool value) => - BlocProvider.of(context).add( - DayNotificationStateEvent( - enabled: value, + enabled: notificationState.dayNotificationEnabled, + title: AppLocalizations.of(context)! + .dayNotificationTitle( + notificationState.dayNotificationValue + .localizedName(context), + ), + subtitle: AppLocalizations.of(context)! + .dayNotificationExplanation( + notificationState.dayNotificationValue + .localizedName(context), + notificationState.dayNotificationTimeValue + .toFormattedString(timeFormatState.timeFormat), + ), + leadingIcon: Icons.calendar_month_outlined, + onChanged: (bool value) => + BlocProvider.of(context).add( + DayNotificationStateEvent( + enabled: value, + ), + ), + constrainedBySlots: notificationState + .timeSlotsEnabledForNotifications, ), - ), - constrainedBySlots: - notificationState.timeSlotsEnabledForNotifications, - ), - ], + ], + ); + }, ); }, ), diff --git a/lib/service/storage_service.dart b/lib/service/storage_service.dart index e57da657..1baf49ef 100644 --- a/lib/service/storage_service.dart +++ b/lib/service/storage_service.dart @@ -3,6 +3,7 @@ import 'dart:developer' as developer; import 'package:chabo/models/enums/day.dart'; import 'package:chabo/models/enums/theme_state_status.dart'; +import 'package:chabo/models/enums/time_format.dart'; import 'package:chabo/models/time_slot.dart'; import 'package:enum_to_string/enum_to_string.dart'; import 'package:flutter/material.dart'; @@ -71,6 +72,12 @@ class StorageService { return await sharedPreferences.setString(key, jsonEncode(timeSlots)); } + Future saveTimeFormat(String key, TimeFormat value) async { + developer.log('{$key: $value}', name: 'storage-service.on.saveTimeFormat'); + + return await sharedPreferences.setString(key, value.name); + } + String? readString(String key) { final value = sharedPreferences.getString(key); developer.log('{$key: $value}', name: 'storage-service.on.readString'); @@ -172,4 +179,17 @@ class StorageService { return timeSlotList; } } + + TimeFormat? readTimeFormat(String key) { + final stringValue = sharedPreferences.getString(key); + if (stringValue == null) { + return null; + } else { + final value = EnumToString.fromString(TimeFormat.values, stringValue); + developer.log('{$key: $value}', + name: 'storage-service.on.readTimeFormat'); + + return value; + } + } } diff --git a/lib/widgets/bottom_sheets/settings_modal_bottom_sheet.dart b/lib/widgets/bottom_sheets/settings_modal_bottom_sheet.dart new file mode 100644 index 00000000..19e93264 --- /dev/null +++ b/lib/widgets/bottom_sheets/settings_modal_bottom_sheet.dart @@ -0,0 +1,155 @@ +import 'package:animated_toggle_switch/animated_toggle_switch.dart'; +import 'package:chabo/bloc/theme/theme_bloc.dart'; +import 'package:chabo/cubits/time_format_cubit.dart'; +import 'package:chabo/custom_properties.dart'; +import 'package:chabo/models/enums/theme_state_status.dart'; +import 'package:chabo/models/enums/time_format.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class SettingsModalBottomSheet extends StatelessWidget { + const SettingsModalBottomSheet({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + BlocBuilder( + builder: (context, state) { + return Wrap( + direction: Axis.vertical, + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, + spacing: 15, + children: [ + Text( + AppLocalizations.of(context)!.themeSettingSubtitle, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + AnimatedToggleSwitch.size( + current: state.status, + values: const [ + ThemeStateStatus.light, + ThemeStateStatus.dark, + ThemeStateStatus.system, + ], + style: ToggleStyle( + backgroundColor: colorScheme.surface, + indicatorColor: colorScheme.primary, + borderColor: colorScheme.inverseSurface, + ), + borderWidth: 1.5, + indicatorSize: const Size.fromWidth(65), + iconBuilder: (value) { + return Icon( + value.icon, + color: state.status == value + ? colorScheme.onPrimary + : colorScheme.onSurface, + ); + }, + onChanged: (value) => + BlocProvider.of(context).add( + ThemeChanged( + status: value, + ), + ), + ), + AnimatedSwitcher( + duration: const Duration( + milliseconds: CustomProperties.shortAnimationDurationMs, + ), + reverseDuration: const Duration( + milliseconds: CustomProperties.shortAnimationDurationMs, + ), + transitionBuilder: + (Widget child, Animation animation) { + return SlideTransition( + position: Tween( + begin: const Offset(0.0, 1.0), + end: const Offset(0.0, 0.0), + ).animate(animation), + child: FadeTransition( + opacity: CurvedAnimation( + parent: animation, + curve: Curves.easeIn, + ), + child: child, + ), + ); + }, + child: Text( + key: ValueKey( + state.status.text(context), + ), + state.status.text(context), + ), + ), + ], + ); + }, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 10), + child: Divider(), + ), + BlocBuilder( + builder: (context, state) { + return Wrap( + direction: Axis.vertical, + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, + spacing: 15, + children: [ + Text( + AppLocalizations.of(context)!.timeFormatSubTitle, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + AnimatedToggleSwitch.size( + current: state.timeFormat, + values: const [ + TimeFormat.twelveHours, + TimeFormat.twentyFourHours, + ], + style: ToggleStyle( + backgroundColor: colorScheme.surface, + indicatorColor: colorScheme.primary, + borderColor: colorScheme.inverseSurface, + ), + borderWidth: 1.5, + indicatorSize: const Size.fromWidth(65), + iconBuilder: (value) { + return Text( + value.text, + style: TextStyle( + color: state.timeFormat == value + ? colorScheme.onPrimary + : colorScheme.onSurface, + fontWeight: FontWeight.bold), + ); + }, + onChanged: (value) => + context.read().setTimeFormat(), + ), + ], + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/floating_actions/floating_actions_widget.dart b/lib/widgets/floating_actions/floating_actions_widget.dart index 6b8f0950..dddb0bd2 100644 --- a/lib/widgets/floating_actions/floating_actions_widget.dart +++ b/lib/widgets/floating_actions/floating_actions_widget.dart @@ -7,8 +7,8 @@ import 'package:chabo/helpers/device_helper.dart'; import 'package:chabo/screens/chabo_about_screen/chabo_about_screen.dart'; import 'package:chabo/screens/notification_screen/notification_screen.dart'; import 'package:chabo/widgets/ad_banner_widget.dart'; +import 'package:chabo/widgets/bottom_sheets/settings_modal_bottom_sheet.dart'; import 'package:chabo/widgets/current_docked_boat_button.dart'; -import 'package:chabo/widgets/theme_switcher_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -117,7 +117,7 @@ class _FloatingActionsWidgetState extends State ), ), builder: (context) { - return const ThemeSwitcherWidget(); + return const SettingsModalBottomSheet(); }, ); context @@ -126,10 +126,10 @@ class _FloatingActionsWidgetState extends State }, content: [ Text( - AppLocalizations.of(context)!.themeSetting, + AppLocalizations.of(context)!.openSetting, ), const Icon( - Icons.format_paint_rounded, + Icons.settings, ), ], isRightHanded: state.isRightHanded, @@ -258,7 +258,7 @@ class _FloatingActionsWidgetState extends State ), child: state.isMenuOpen ? Text( - AppLocalizations.of(context)!.settingsTitle, + AppLocalizations.of(context)!.settingsClose, style: Theme.of(context).textTheme.titleMedium, textAlign: TextAlign.start, ) @@ -266,10 +266,10 @@ class _FloatingActionsWidgetState extends State ), state.isMenuOpen ? const Icon( - Icons.close, + Icons.expand_more, ) : const Icon( - Icons.settings, + Icons.expand_less_outlined, ), ], isSpaced: state.isMenuOpen, diff --git a/lib/widgets/forecast/forecast_widget/closing_info_widget.dart b/lib/widgets/forecast/forecast_widget/closing_info_widget.dart index 579ae44c..2663259a 100644 --- a/lib/widgets/forecast/forecast_widget/closing_info_widget.dart +++ b/lib/widgets/forecast/forecast_widget/closing_info_widget.dart @@ -2,8 +2,10 @@ part of 'forecast_widget.dart'; class _ClosingInfoWidget extends StatelessWidget { final AbstractForecast forecast; + final TimeFormat timeFormat; - const _ClosingInfoWidget({Key? key, required this.forecast}) + const _ClosingInfoWidget( + {Key? key, required this.forecast, required this.timeFormat}) : super(key: key); @override diff --git a/lib/widgets/forecast/forecast_widget/forecast_widget.dart b/lib/widgets/forecast/forecast_widget/forecast_widget.dart index 67498d87..53d738d9 100644 --- a/lib/widgets/forecast/forecast_widget/forecast_widget.dart +++ b/lib/widgets/forecast/forecast_widget/forecast_widget.dart @@ -1,14 +1,17 @@ import 'dart:ui'; +import 'package:chabo/cubits/time_format_cubit.dart'; import 'package:chabo/custom_properties.dart'; import 'package:chabo/extensions/color_scheme_extension.dart'; import 'package:chabo/extensions/date_time_extension.dart'; import 'package:chabo/extensions/duration_extension.dart'; import 'package:chabo/helpers/device_helper.dart'; import 'package:chabo/models/abstract_forecast.dart'; +import 'package:chabo/models/enums/time_format.dart'; import 'package:chabo/models/time_slot.dart'; import 'package:chabo/widgets/bottom_sheets/forecast_information_bottom_sheet.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; @@ -135,21 +138,25 @@ class ForecastWidget extends StatelessWidget { Stack( alignment: Alignment.center, children: [ - Row( - children: [ - Flexible( - flex: 2, - child: _ClosingInfoWidget( - forecast: forecast, - ), - ), - Flexible( - flex: 2, - child: _OpeningInfoWidget( - forecast: forecast, - ), - ), - ], + BlocBuilder( + builder: (context, state) { + return Row( + children: [ + Flexible( + flex: 2, + child: _ClosingInfoWidget( + forecast: forecast, + timeFormat: state.timeFormat), + ), + Flexible( + flex: 2, + child: _OpeningInfoWidget( + forecast: forecast, + timeFormat: state.timeFormat), + ), + ], + ); + }, ), _DurationWidget( forecast: forecast, diff --git a/lib/widgets/forecast/forecast_widget/opening_info_widget.dart b/lib/widgets/forecast/forecast_widget/opening_info_widget.dart index 3b8a9427..a4bbc9f9 100644 --- a/lib/widgets/forecast/forecast_widget/opening_info_widget.dart +++ b/lib/widgets/forecast/forecast_widget/opening_info_widget.dart @@ -2,8 +2,10 @@ part of 'forecast_widget.dart'; class _OpeningInfoWidget extends StatelessWidget { final AbstractForecast forecast; + final TimeFormat timeFormat; - const _OpeningInfoWidget({Key? key, required this.forecast}) + const _OpeningInfoWidget( + {Key? key, required this.forecast, required this.timeFormat}) : super(key: key); @override diff --git a/lib/widgets/theme_switcher_widget.dart b/lib/widgets/theme_switcher_widget.dart deleted file mode 100644 index 8d1ec0cc..00000000 --- a/lib/widgets/theme_switcher_widget.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:animated_toggle_switch/animated_toggle_switch.dart'; -import 'package:chabo/bloc/theme/theme_bloc.dart'; -import 'package:chabo/custom_properties.dart'; -import 'package:chabo/models/enums/theme_state_status.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ThemeSwitcherWidget extends StatelessWidget { - const ThemeSwitcherWidget({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return BlocBuilder( - builder: (context, state) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Wrap( - direction: Axis.vertical, - crossAxisAlignment: WrapCrossAlignment.center, - runAlignment: WrapAlignment.center, - spacing: 15, - children: [ - Text( - AppLocalizations.of(context)!.themeSettingSubtitle, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - AnimatedToggleSwitch.size( - current: state.status, - values: const [ - ThemeStateStatus.light, - ThemeStateStatus.dark, - ThemeStateStatus.system, - ], - style: ToggleStyle( - backgroundColor: colorScheme.surface, - indicatorColor: colorScheme.primary, - borderColor: colorScheme.inverseSurface, - ), - borderWidth: 1.5, - indicatorSize: const Size.fromWidth(65), - iconBuilder: (value) { - return Icon( - value.icon, - color: state.status == value - ? colorScheme.onPrimary - : colorScheme.onSurface, - ); - }, - onChanged: (value) => BlocProvider.of(context).add( - ThemeChanged( - status: value, - ), - ), - ), - AnimatedSwitcher( - duration: const Duration( - milliseconds: CustomProperties.shortAnimationDurationMs, - ), - reverseDuration: const Duration( - milliseconds: CustomProperties.shortAnimationDurationMs, - ), - transitionBuilder: (Widget child, Animation animation) { - return SlideTransition( - position: Tween( - begin: const Offset(0.0, 1.0), - end: const Offset(0.0, 0.0), - ).animate(animation), - child: FadeTransition( - opacity: CurvedAnimation( - parent: animation, - curve: Curves.easeIn, - ), - child: child, - ), - ); - }, - child: Text( - key: ValueKey( - state.status.text(context), - ), - state.status.text(context), - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/lib/widgets/time_slot_widget.dart b/lib/widgets/time_slot_widget.dart index dbaf5a53..e616053a 100644 --- a/lib/widgets/time_slot_widget.dart +++ b/lib/widgets/time_slot_widget.dart @@ -1,8 +1,10 @@ import 'dart:ui'; import 'package:chabo/bloc/notification/notification_bloc.dart'; +import 'package:chabo/cubits/time_format_cubit.dart'; import 'package:chabo/custom_properties.dart'; import 'package:chabo/dialogs/time_slot_dialog.dart'; +import 'package:chabo/extensions/time_of_day_extension.dart'; import 'package:chabo/models/time_slot.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -47,20 +49,24 @@ class TimeSlotWidget extends StatelessWidget { }, child: Padding( padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - Text( - timeSlot.name != '' - ? timeSlot.name - : AppLocalizations.of(context)! - .favoriteTimeSlotDefaultName(index + 1), - style: Theme.of(context).textTheme.titleMedium, - ), - Text( - '${timeSlot.from.format(context)} - ${timeSlot.to.format(context)}', - style: Theme.of(context).textTheme.labelSmall, - ), - ], + child: BlocBuilder( + builder: (context, timeFormatState) { + return Column( + children: [ + Text( + timeSlot.name != '' + ? timeSlot.name + : AppLocalizations.of(context)! + .favoriteTimeSlotDefaultName(index + 1), + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + '${timeSlot.from.toFormattedString(timeFormatState.timeFormat)} - ${timeSlot.to.toFormattedString(timeFormatState.timeFormat)}', + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ); + }, ), ), ); diff --git a/test/localized_testable_widget.dart b/test/localized_testable_widget.dart index 88ba4610..e8c6fcce 100644 --- a/test/localized_testable_widget.dart +++ b/test/localized_testable_widget.dart @@ -1,30 +1,63 @@ +import 'package:chabo/cubits/time_format_cubit.dart'; +import 'package:chabo/models/enums/time_format.dart'; +import 'package:chabo/service/storage_service.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -Widget localizedTestableWidgetEN({required Widget child}) => MaterialApp( - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('en', ''), - ], - home: child, - ); +Future localizedTestableWidgetEN({required Widget child}) async { + SharedPreferences.setMockInitialValues({}); + final StorageService storageService = StorageService( + sharedPreferences: await SharedPreferences.getInstance(), + ); -Widget localizedTestableWidgetFR({required Widget child}) => MaterialApp( - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('fr', ''), - ], - locale: const Locale('fr', ''), - home: child, - ); + return MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en', ''), + ], + home: BlocProvider( + create: (BuildContext context) => TimeFormatCubit( + storageService, + const TimeFormatState( + timeFormat: TimeFormat.twelveHours, + )), + child: child, + ), + ); +} + +Future localizedTestableWidgetFR({required Widget child}) async { + SharedPreferences.setMockInitialValues({}); + final StorageService storageService = StorageService( + sharedPreferences: await SharedPreferences.getInstance(), + ); + + return MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('fr', ''), + ], + locale: const Locale('fr', ''), + home: BlocProvider( + create: (BuildContext context) => TimeFormatCubit( + storageService, + const TimeFormatState( + timeFormat: TimeFormat.twentyFourHours, + )), + child: child, + ), + ); +} diff --git a/test/services/storage_service_test.dart b/test/services/storage_service_test.dart index fa9ec93e..c02a18e4 100644 --- a/test/services/storage_service_test.dart +++ b/test/services/storage_service_test.dart @@ -1,5 +1,6 @@ import 'package:chabo/models/enums/day.dart'; import 'package:chabo/models/enums/theme_state_status.dart'; +import 'package:chabo/models/enums/time_format.dart'; import 'package:chabo/models/time_slot.dart'; import 'package:chabo/service/storage_service.dart'; import 'package:flutter/material.dart'; @@ -109,4 +110,14 @@ void main() { expect(saveResult, true); expect(readResult, days); }); + + test('Save & Read TimeFormat', () async { + final saveResult = await storageService.saveTimeFormat( + 'KEY_TIMEFORMAT', + TimeFormat.twentyFourHours, + ); + final readResult = storageService.readTimeFormat('KEY_TIMEFORMAT'); + expect(saveResult, true); + expect(readResult, TimeFormat.twentyFourHours); + }); } diff --git a/test/units/boat_forecast_test.dart b/test/units/boat_forecast_test.dart index 3e90da13..f3f82a45 100644 --- a/test/units/boat_forecast_test.dart +++ b/test/units/boat_forecast_test.dart @@ -86,7 +86,7 @@ void main() { 'Same day', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { final RichText richText = @@ -108,7 +108,7 @@ void main() { 'During tow days', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { final RichText richText = diff --git a/test/units/boats_test.dart b/test/units/boats_test.dart index f7bfdf77..051d69e0 100644 --- a/test/units/boats_test.dart +++ b/test/units/boats_test.dart @@ -24,7 +24,7 @@ void main() { group('toNames', () { testWidgets('1 Boat', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { var names = boats1.getNames(context); @@ -39,7 +39,7 @@ void main() { testWidgets('2 Boats', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { var names = boats2.getNames(context); @@ -54,7 +54,7 @@ void main() { testWidgets('3 Boats', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { var names = boats3.getNames(context); @@ -71,7 +71,7 @@ void main() { group('toLocalizedString', () { testWidgets('1 Boat', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { var names = boats1.toLocalizedString(context); @@ -86,7 +86,7 @@ void main() { testWidgets('2 Boats', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { var names = boats2.toLocalizedString(context); @@ -104,7 +104,7 @@ void main() { testWidgets('3 Boats', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { var names = boats3.toLocalizedString(context); @@ -124,7 +124,7 @@ void main() { group('toLocalizedMoonHarborStatus', () { testWidgets('1 Boat', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { final RichText richText = RichText( @@ -148,7 +148,7 @@ void main() { testWidgets('2 Boat', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { final RichText richText = RichText( @@ -172,7 +172,7 @@ void main() { testWidgets('3 Boat', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { final RichText richText = RichText( @@ -196,7 +196,7 @@ void main() { testWidgets('4 Boat', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { final RichText richText = RichText( diff --git a/test/units/custom_extensions_tests.dart b/test/units/custom_extensions_tests.dart index 588f1a24..5a30d888 100644 --- a/test/units/custom_extensions_tests.dart +++ b/test/units/custom_extensions_tests.dart @@ -71,7 +71,7 @@ void main() { testWidgets('toLocalizedTextSpan - EN', (WidgetTester tester) async { final dateTime = DateTime(2023, 5, 11, 15, 0); await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { var richText = RichText( @@ -92,7 +92,7 @@ void main() { testWidgets('toLocalizedTextSpan - FR', (WidgetTester tester) async { final dateTime = DateTime(2023, 5, 11, 15, 0); await tester.pumpWidget( - localizedTestableWidgetFR( + await localizedTestableWidgetFR( child: Builder( builder: (BuildContext context) { var richText = RichText( diff --git a/test/units/maintenance_forecast_test.dart b/test/units/maintenance_forecast_test.dart index e6382100..16ebf412 100644 --- a/test/units/maintenance_forecast_test.dart +++ b/test/units/maintenance_forecast_test.dart @@ -48,7 +48,7 @@ void main() { 'Display info TextSpan (same day)', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { final RichText richText = @@ -70,7 +70,7 @@ void main() { 'Display info TextSpan (tow days)', (WidgetTester tester) async { await tester.pumpWidget( - localizedTestableWidgetEN( + await localizedTestableWidgetEN( child: Builder( builder: (BuildContext context) { final RichText richText =