diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e8f9718c..b4a9535b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,8 +5,6 @@ android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> - - @@ -14,12 +12,8 @@ android:name="android.permission.VIBRATE" /> - - - - + + > alarmSortOptions = [ ListSortOption((context) => AppLocalizations.of(context)!.timeOfDayDesc, sortTimeOfDayDescending), ]; -int sortRemainingTimeDescending(Alarm a, Alarm b) { +int sortRemainingTimeAscending(Alarm a, Alarm b) { if (a.currentScheduleDateTime == null && b.currentScheduleDateTime == null) { return 0; } else if (a.currentScheduleDateTime == null) { @@ -29,7 +29,7 @@ int sortRemainingTimeDescending(Alarm a, Alarm b) { return remainingB.compareTo(remainingA); } -int sortRemainingTimeAscending(Alarm a, Alarm b) { +int sortRemainingTimeDescending(Alarm a, Alarm b) { if (a.currentScheduleDateTime == null && b.currentScheduleDateTime == null) { return 0; } else if (a.currentScheduleDateTime == null) { diff --git a/lib/alarm/logic/new_alarm_snackbar.dart b/lib/alarm/logic/new_alarm_snackbar.dart index e0d2f881..25985cee 100644 --- a/lib/alarm/logic/new_alarm_snackbar.dart +++ b/lib/alarm/logic/new_alarm_snackbar.dart @@ -1,30 +1,76 @@ import 'package:clock_app/alarm/types/alarm.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -String getNewAlarmSnackbarText(Alarm alarm) { +String getRemainingAlarmTimeText(BuildContext context, Alarm alarm) { Duration etaNextAlarm = alarm.currentScheduleDateTime!.difference(DateTime.now().toLocal()); String etaText = ''; + AppLocalizations localizations = AppLocalizations.of(context)!; + + if (etaNextAlarm.inDays > 0) { + etaText = localizations.daysString(etaNextAlarm.inDays); + } else if (etaNextAlarm.inHours > 0) { + int hours = etaNextAlarm.inHours; + int minutes = etaNextAlarm.inMinutes % 60; + if (minutes > 0) { + etaText = localizations.combinedTime(localizations.hoursString(hours), + localizations.minutesString(minutes)); + } else { + etaText = localizations.hoursString(hours); + } + } else if (etaNextAlarm.inMinutes > 0) { + int minutes = etaNextAlarm.inMinutes; + etaText = localizations.minutesString(minutes); + } else { + etaText = localizations.lessThanOneMinute; + } + + return etaText; +} + +String getShortRemainingAlarmTimeText(BuildContext context, Alarm alarm) { + Duration etaNextAlarm = + alarm.currentScheduleDateTime!.difference(DateTime.now().toLocal()); + + String etaText = ''; + + AppLocalizations localizations = AppLocalizations.of(context)!; + if (etaNextAlarm.inDays > 0) { - int days = etaNextAlarm.inDays; - String dayTextSuffix = days <= 1 ? 'day' : 'days'; - etaText = '$days $dayTextSuffix'; + etaText = localizations.daysString(etaNextAlarm.inDays); } else if (etaNextAlarm.inHours > 0) { int hours = etaNextAlarm.inHours; int minutes = etaNextAlarm.inMinutes % 60; - String hourTextSuffix = hours <= 1 ? 'hour' : 'hours'; - String minuteTextSuffix = minutes <= 1 ? 'minute' : 'minutes'; - String hoursText = '$hours $hourTextSuffix'; - String minutesText = minutes == 0 ? '' : ' and $minutes $minuteTextSuffix'; - etaText = '$hoursText$minutesText'; + if (minutes > 0) { + etaText = '${localizations.shortHoursString(hours)} ${localizations.shortMinutesString(minutes)}'; + } else { + etaText = localizations.shortHoursString(hours); + } } else if (etaNextAlarm.inMinutes > 0) { int minutes = etaNextAlarm.inMinutes; - String minuteTextSuffix = minutes <= 1 ? 'minute' : 'minutes'; - etaText = '$minutes $minuteTextSuffix'; + etaText = localizations.shortMinutesString(minutes); } else { - etaText = 'less than 1 minute'; + etaText = localizations.shortMinutesString(1); } - return 'Alarm will ring in $etaText'; + return etaText; +} + +String getNewAlarmText(BuildContext context, Alarm alarm) { + AppLocalizations localizations = AppLocalizations.of(context)!; + + final etaText = getRemainingAlarmTimeText(context, alarm); + + return localizations.alarmRingInMessage(etaText); +} + +String getNextAlarmText(BuildContext context, Alarm alarm) { + AppLocalizations localizations = AppLocalizations.of(context)!; + + final etaText = getShortRemainingAlarmTimeText(context, alarm); + + return localizations.nextAlarmIn(etaText); } diff --git a/lib/alarm/screens/alarm_screen.dart b/lib/alarm/screens/alarm_screen.dart index cc96b771..4e7aba13 100644 --- a/lib/alarm/screens/alarm_screen.dart +++ b/lib/alarm/screens/alarm_screen.dart @@ -2,6 +2,7 @@ import 'package:clock_app/alarm/data/alarm_list_filters.dart'; import 'package:clock_app/alarm/data/alarm_sort_options.dart'; import 'package:clock_app/alarm/logic/new_alarm_snackbar.dart'; import 'package:clock_app/alarm/types/alarm.dart'; +import 'package:clock_app/alarm/utils/next_alarm.dart'; import 'package:clock_app/alarm/widgets/alarm_card.dart'; import 'package:clock_app/alarm/widgets/alarm_description.dart'; import 'package:clock_app/alarm/widgets/alarm_time_picker.dart'; @@ -10,18 +11,18 @@ import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/picker_result.dart'; import 'package:clock_app/common/types/time.dart'; import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/list/customize_list_item_screen.dart'; import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; import 'package:clock_app/common/widgets/time_picker.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:flutter/material.dart'; import 'package:great_list_view/great_list_view.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - - typedef AlarmCardBuilder = Widget Function( BuildContext context, int index, @@ -40,6 +41,8 @@ class _AlarmScreenState extends State { late Setting _showInstantAlarmButton; late Setting _showFilters; late Setting _showSort; + late Setting _showNextAlarm; + late Alarm? nextAlarm; void update(value) { setState(() {}); @@ -53,20 +56,22 @@ class _AlarmScreenState extends State { _showInstantAlarmButton = appSettings .getGroup("Developer Options") .getSetting("Show Instant Alarm Button"); - _showFilters = appSettings - .getGroup("Alarm") - .getGroup("Filters") - .getSetting("Show Filters"); - _showSort = appSettings - .getGroup("Alarm") - .getGroup("Filters") - .getSetting("Show Sort"); + final filtersGroup = appSettings.getGroup("Alarm").getGroup("Filters"); + _showFilters = filtersGroup.getSetting("Show Filters"); + _showSort = filtersGroup.getSetting("Show Sort"); + _showNextAlarm = filtersGroup.getSetting("Show Next Alarm"); + // appSettings.getGroup("Accessibility").getSetting("Left Handed Mode"); _showInstantAlarmButton.addListener(update); _showFilters.addListener(update); + _showNextAlarm.addListener(update); _showSort.addListener(update); + ListenerManager.addOnChangeListener("alarms", update); + + nextAlarm = getNextAlarm(); + // ListenerManager().addListener(); } @@ -75,6 +80,8 @@ class _AlarmScreenState extends State { _showInstantAlarmButton.removeListener(update); _showFilters.removeListener(update); _showSort.removeListener(update); + _showNextAlarm.removeListener(update); + ListenerManager.removeOnChangeListener("alarms", update); super.dispose(); } @@ -119,14 +126,17 @@ class _AlarmScreenState extends State { ScaffoldMessenger.of(context).removeCurrentSnackBar(); DateTime? nextScheduleDateTime = alarm.currentScheduleDateTime; if (nextScheduleDateTime == null) return; - ScaffoldMessenger.of(context).showSnackBar( - getSnackbar(getNewAlarmSnackbarText(alarm), fab: true, navBar: true)); + ScaffoldMessenger.of(context).showSnackBar(getSnackbar( + getNewAlarmText(context, alarm), + fab: true, + navBar: true)); }); } Future _handleEnableChangeAlarm(Alarm alarm, bool value) async { if (!alarm.canBeDisabledWhenSnoozed && !value && alarm.isSnoozed) { - showSnackBar(context, AppLocalizations.of(context)!.cannotDisableAlarmWhileSnoozedSnackbar, + showSnackBar(context, + AppLocalizations.of(context)!.cannotDisableAlarmWhileSnoozedSnackbar, fab: true, navBar: true); } else { await alarm.setIsEnabled(value, @@ -140,8 +150,12 @@ class _AlarmScreenState extends State { List alarms, bool value) async { for (var alarm in alarms) { if (!alarm.canBeDisabledWhenSnoozed && !value && alarm.isSnoozed) { - showSnackBar(context, AppLocalizations.of(context)!.cannotDisableAlarmWhileSnoozedSnackbar, - fab: true, navBar: true); + showSnackBar( + context, + AppLocalizations.of(context)! + .cannotDisableAlarmWhileSnoozedSnackbar, + fab: true, + navBar: true); } else { await alarm.setIsEnabled(value, "_handleEnableChangeMultipleAlarms(): Alarm enable set to $value by user"); @@ -173,6 +187,24 @@ class _AlarmScreenState extends State { _listController.changeItems((alarms) {}); } + List> getListFilterItems() { + List> listFilterItems = + _showFilters.value ? [...alarmListFilters] : []; + + if (nextAlarm != null && _showNextAlarm.value) { + if (nextAlarm!.currentScheduleDateTime != null) { + listFilterItems.insert( + 0, + ListFilter( + (context) => getNextAlarmText(context, nextAlarm!), + (alarm) => alarm.id == nextAlarm!.id, + )); + } + } + + return listFilterItems; + } + @override Widget build(BuildContext context) { Future selectTime() async { @@ -221,29 +253,38 @@ class _AlarmScreenState extends State { }, placeholderText: AppLocalizations.of(context)!.noAlarmMessage, reloadOnPop: true, - listFilters: _showFilters.value ? alarmListFilters : [], + onSaveItems: (items) { + nextAlarm = getNextAlarm(); + setState(() {}); + }, + // header: getNextAlarmWidget(), + listFilters: getListFilterItems(), customActions: _showFilters.value ? [ ListFilterCustomAction( - name: AppLocalizations.of(context)!.enableAllFilteredAlarmsAction, + name: AppLocalizations.of(context)! + .enableAllFilteredAlarmsAction, icon: Icons.alarm_on_rounded, action: (alarms) { _handleEnableChangeMultiple(alarms, true); }), ListFilterCustomAction( - name: AppLocalizations.of(context)!.disableAllFilteredAlarmsAction, + name: AppLocalizations.of(context)! + .disableAllFilteredAlarmsAction, icon: Icons.alarm_off_rounded, action: (alarms) { _handleEnableChangeMultiple(alarms, false); }), ListFilterCustomAction( - name: AppLocalizations.of(context)!.skipAllFilteredAlarmsAction, + name: AppLocalizations.of(context)! + .skipAllFilteredAlarmsAction, icon: Icons.skip_next_rounded, action: (alarms) { _handleSkipChangeMultiple(alarms, true); }), ListFilterCustomAction( - name: AppLocalizations.of(context)!.cancelSkipAllFilteredAlarmsAction, + name: AppLocalizations.of(context)! + .cancelSkipAllFilteredAlarmsAction, icon: Icons.skip_next_rounded, action: (alarms) { _handleSkipChangeMultiple(alarms, false); diff --git a/lib/alarm/utils/next_alarm.dart b/lib/alarm/utils/next_alarm.dart new file mode 100644 index 00000000..ebb8eaf3 --- /dev/null +++ b/lib/alarm/utils/next_alarm.dart @@ -0,0 +1,16 @@ + + +import 'package:clock_app/alarm/types/alarm.dart'; +import 'package:clock_app/common/utils/json_serialize.dart'; +import 'package:clock_app/common/utils/list_storage.dart'; + +Alarm? getNextAlarm () { + List alarms = loadListSync('alarms'); + if (alarms.isEmpty) return null; + alarms.sort((a, b) { + if (a.currentScheduleDateTime == null) return 1; + if (b.currentScheduleDateTime == null) return -1; + return a.currentScheduleDateTime!.compareTo(b.currentScheduleDateTime!); + }); + return alarms.first; +} diff --git a/lib/common/utils/json_serialize.dart b/lib/common/utils/json_serialize.dart index 55c8016d..c052c95f 100644 --- a/lib/common/utils/json_serialize.dart +++ b/lib/common/utils/json_serialize.dart @@ -9,6 +9,7 @@ import 'package:clock_app/common/types/tag.dart'; import 'package:clock_app/common/types/time.dart'; import 'package:clock_app/clock/types/city.dart'; import 'package:clock_app/common/types/json.dart'; +import 'package:clock_app/stopwatch/types/lap.dart'; import 'package:clock_app/stopwatch/types/stopwatch.dart'; import 'package:clock_app/theme/types/color_scheme.dart'; import 'package:clock_app/theme/types/style_theme.dart'; @@ -27,6 +28,7 @@ final fromJsonFactories = { StyleTheme: (Json json) => StyleTheme.fromJson(json), AlarmTask: (Json json) => AlarmTask.fromJson(json), Time: (Json json) => Time.fromJson(json), + Lap: (Json json) => Lap.fromJson(json), TimeOfDay: (Json json) => TimeOfDayUtils.fromJson(json), FileItem: (Json json) => FileItem.fromJson(json), AlarmEvent: (Json json) => AlarmEvent.fromJson(json), @@ -34,19 +36,20 @@ final fromJsonFactories = { Tag: (Json json) => Tag.fromJson(json), }; - String listToString(List items) => json.encode( items.map((item) => item.toJson()).toList(), ); List listFromString(String encodedItems) { if (!fromJsonFactories.containsKey(T)) { - throw Exception("No fromJson factory for type '$T'"); + throw Exception( + "No fromJson factory for type '$T'. Please add one in the file 'common/utils/json_serialize.dart'"); } try { - return (json.decode(encodedItems) as List) - .map((json) => fromJsonFactories[T]!(json)) - .toList(); + List rawList = json.decode(encodedItems) as List; + Function fromJson = fromJsonFactories[T]!; + List list = rawList.map((json) => fromJson(json)).toList(); + return list; } catch (e) { debugPrint("Error decoding string: ${e.toString()}"); rethrow; diff --git a/lib/common/utils/list_storage.dart b/lib/common/utils/list_storage.dart index 437e38c4..71383cf5 100644 --- a/lib/common/utils/list_storage.dart +++ b/lib/common/utils/list_storage.dart @@ -61,12 +61,13 @@ Future> loadList(String key) async { Future saveList( String key, List list) async { - await saveTextFile(key, listToString(list)); + await saveTextFile(key, listToString(list)); } Future initList( - String key, List value) async { - await initTextFile(key, listToString(value)); + String key, List list) async { + + await initTextFile(key, listToString(list)); } Future initTextFile(String key, String value) async { diff --git a/lib/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index 9e4f3fe2..429eb7cb 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -39,6 +39,8 @@ class CustomListView extends StatefulWidget { this.sortOptions = const [], this.initialSortIndex = 0, this.onChangeSortIndex, + this.header, + }); final List items; @@ -60,6 +62,7 @@ class CustomListView extends StatefulWidget { final List> customActions; final List> sortOptions; final Function(int index)? onChangeSortIndex; + final Widget? header; @override State createState() => _CustomListViewState(); @@ -414,6 +417,7 @@ class _CustomListViewState ), ), ), + if(widget.header != null) widget.header!, Expanded( flex: 1, child: Stack(children: [ @@ -462,6 +466,8 @@ class _CustomListViewState reorderDecorationBuilder: widget.isReorderable ? reorderableListDecorator : null, footer: const SizedBox(height: 64 + 80), + // header: widget.header, + // cacheExtent: double.infinity, ), ), diff --git a/lib/common/widgets/list/persistent_list_view.dart b/lib/common/widgets/list/persistent_list_view.dart index ac453fc8..fb7b3b6d 100644 --- a/lib/common/widgets/list/persistent_list_view.dart +++ b/lib/common/widgets/list/persistent_list_view.dart @@ -75,6 +75,8 @@ class PersistentListView extends StatefulWidget { this.listFilters = const [], this.customActions = const [], this.sortOptions = const [], + this.header, + this.onSaveItems = null, // this.initialSortIndex = 0, }); @@ -91,10 +93,12 @@ class PersistentListView extends StatefulWidget { final bool isDuplicateEnabled; final bool reloadOnPop; final bool shouldInsertOnTop; + final Widget? header; // final int initialSortIndex; final List> listFilters; final List> customActions; final List> sortOptions; + final Function(List items)? onSaveItems; @override State createState() => _PersistentListViewState(); @@ -159,10 +163,12 @@ class _PersistentListViewState } } - void _saveItems() { + void _saveItems () async { if (widget.saveTag.isNotEmpty) { - saveList(widget.saveTag, _items); + await saveList(widget.saveTag, _items); } + widget.onSaveItems?.call(_items); + } void _handleChangeSort(int index) { @@ -191,6 +197,7 @@ class _PersistentListViewState sortOptions: widget.sortOptions, initialSortIndex: _initialSortIndex, onChangeSortIndex: _handleChangeSort, + header: widget.header, ); } } diff --git a/lib/common/widgets/time_picker.dart b/lib/common/widgets/time_picker.dart index c86a596a..8f651041 100644 --- a/lib/common/widgets/time_picker.dart +++ b/lib/common/widgets/time_picker.dart @@ -2820,8 +2820,15 @@ class _TimePickerDialogState extends State TextTheme textTheme = theme.textTheme; ColorScheme colorScheme = theme.colorScheme; - bool use24hMode = MediaQuery.of(context).alwaysUse24HourFormat || - appSettings.getSetting("Time Format").value == TimeFormat.h24; + TimeFormat timeFormat = appSettings.getSetting("Time Format").value; + + bool use24hMode = false; + if (timeFormat == TimeFormat.device) { + use24hMode = MediaQuery.of(context).alwaysUse24HourFormat; + } else { + use24hMode = + appSettings.getSetting("Time Format").value == TimeFormat.h24; + } switch (type) { case TimePickerType.spinner: diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a95d89ae..a94311d7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -674,7 +674,35 @@ "editTagLabel": "Edit Tag", "@editTagLabel": {}, "tagNamePlaceholder": "Tag name", - "@tagNamePlaceholder": {} - - + "@tagNamePlaceholder": {}, + "hoursString": "{count, plural, =0{} =1{1 hour} other{{count} hours}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{1 minute} other{{count} minutes}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{1 second} other{{count} seconds}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 day} other{{count} days}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 week} other{{count} weeks}}", + "@weeksString": {}, + "monthsString": "{count, plural, =0{} =1{1 month} other{{count} months}}", + "@monthsString": {}, + "yearsString": "{count, plural, =0{} =1{1 year} other{{count} years}}", + "@yearsString": {}, + "lessThanOneMinute": "less than 1 minute", + "@lessThanOneMinute": {}, + "alarmRingInMessage": "Alarm will ring in {duration}", + "@alarmRingInMessage": {}, + "nextAlarmIn": "Next: {duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} and {minutes}", + "@combinedTime": {}, + "shortHoursString": "{hours}h", + "@shortTimeFormat": {}, + "shortMinutesString": "{minutes}m", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}s", + "@shortSecondsString": {}, + "showNextAlarm": "Show Next Alarm", + "@showNextAlarm": {} } diff --git a/lib/navigation/screens/nav_scaffold.dart b/lib/navigation/screens/nav_scaffold.dart index 0770b4a8..904a6acf 100644 --- a/lib/navigation/screens/nav_scaffold.dart +++ b/lib/navigation/screens/nav_scaffold.dart @@ -57,7 +57,7 @@ class _NavScaffoldState extends State { DateTime? nextScheduleDateTime = alarm.currentScheduleDateTime; if (nextScheduleDateTime == null) return; ScaffoldMessenger.of(context).showSnackBar( - getSnackbar(getNewAlarmSnackbarText(alarm), fab: true, navBar: true)); + getSnackbar(getNewAlarmText(context, alarm), fab: true, navBar: true)); }); } diff --git a/lib/settings/data/alarm_app_settings_schema.dart b/lib/settings/data/alarm_app_settings_schema.dart index 89a3a6c3..eaf9076a 100644 --- a/lib/settings/data/alarm_app_settings_schema.dart +++ b/lib/settings/data/alarm_app_settings_schema.dart @@ -77,6 +77,9 @@ SettingGroup alarmAppSettingsSchema = SettingGroup( (context) => AppLocalizations.of(context)!.showFiltersSetting, true), SwitchSetting("Show Sort", (context) => AppLocalizations.of(context)!.showSortSetting, true), + SwitchSetting("Show Next Alarm", + (context) => AppLocalizations.of(context)!.showNextAlarm, false), + ]), SettingGroup( "Notifications", diff --git a/lib/settings/data/general_settings_schema.dart b/lib/settings/data/general_settings_schema.dart index 1bbd4174..1843e658 100644 --- a/lib/settings/data/general_settings_schema.dart +++ b/lib/settings/data/general_settings_schema.dart @@ -59,11 +59,11 @@ final dateFormatOptions = [ final longDateFormatOptions = [ _getDateSettingOption("EEE, MMM d"), - _getDateSettingOption("EEE, MMMM d "), + _getDateSettingOption("EEE, MMMM d"), _getDateSettingOption("EEE, d MMM"), _getDateSettingOption("EEE, d MMMM"), _getDateSettingOption("EEEE, MMM d"), - _getDateSettingOption("EEEE, MMMM d "), + _getDateSettingOption("EEEE, MMMM d"), _getDateSettingOption("EEEE, d MMM"), _getDateSettingOption("EEEE, d MMMM"), ]; diff --git a/lib/stopwatch/screens/stopwatch_screen.dart b/lib/stopwatch/screens/stopwatch_screen.dart index a4645316..68a3bf13 100644 --- a/lib/stopwatch/screens/stopwatch_screen.dart +++ b/lib/stopwatch/screens/stopwatch_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:clock_app/common/types/list_controller.dart'; import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/list/custom_list_view.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/notifications/data/notification_channel.dart'; @@ -17,8 +18,6 @@ import 'package:clock_app/stopwatch/widgets/stopwatch_ticker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - - class StopwatchScreen extends StatefulWidget { const StopwatchScreen({super.key}); @@ -30,9 +29,8 @@ class _StopwatchScreenState extends State { final _listController = ListController(); late Setting _showNotificationSetting; - - late final ClockStopwatch _stopwatch; + late final ClockStopwatch _stopwatch; void update(dynamic value) { setState(() {}); @@ -42,7 +40,7 @@ class _StopwatchScreenState extends State { void initState() { super.initState(); _stopwatch = loadListSync('stopwatches').first; - + _showNotificationSetting = appSettings.getGroup("Stopwatch").getSetting("Show Notification"); @@ -68,7 +66,6 @@ class _StopwatchScreenState extends State { @override void dispose() { - // updateNotificationInterval?.cancel(); // updateNotificationInterval = null; @@ -88,18 +85,23 @@ class _StopwatchScreenState extends State { void _handleAddLap() { if (_stopwatch.currentLapTime.inMilliseconds == 0) return; + _stopwatch.finishLap(_stopwatch.laps.first); + _listController.changeItems((laps) => {}); _listController.addItem(_stopwatch.getLap()); saveList('stopwatches', [_stopwatch]); showProgressNotification(); } void _handleToggleState() { + if (_stopwatch.isStopped) { + _listController.addItem(_stopwatch.getLap()); + } setState(() { _stopwatch.toggleState(); }); + saveList('stopwatches', [_stopwatch]); if (_stopwatch.isRunning) { - // ticker!.start(); showProgressNotification(); } else { stopwatchNotificationInterval?.cancel(); @@ -137,16 +139,18 @@ class _StopwatchScreenState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - StopwatchTicker(stopwatch:_stopwatch), - const SizedBox(height: 8), + StopwatchTicker(stopwatch: _stopwatch), + const SizedBox(height: 8), Expanded( child: CustomListView( items: _stopwatch.laps, listController: _listController, - itemBuilder: (lap) => LapCard( - key: ValueKey(lap), - lap: lap, - ), + itemBuilder: (lap) => lap.isActive + ? ActiveLapCard(stopwatch: _stopwatch) + : LapCard( + key: ValueKey(lap), + lap: lap, + ), placeholderText: AppLocalizations.of(context)!.noLapsMessage, isDeleteEnabled: false, isDuplicateEnabled: false, @@ -182,5 +186,3 @@ class _StopwatchScreenState extends State { ); } } - - diff --git a/lib/stopwatch/types/lap.dart b/lib/stopwatch/types/lap.dart index e5f3a7d3..803c96c2 100644 --- a/lib/stopwatch/types/lap.dart +++ b/lib/stopwatch/types/lap.dart @@ -3,27 +3,45 @@ import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/timer/types/time_duration.dart'; class Lap extends ListItem { - late int number; - late TimeDuration lapTime; - late TimeDuration elapsedTime; + late int _number; + late TimeDuration _lapTime; + late TimeDuration _elapsedTime; + late bool _isActive; + int get number => _number; + bool get isActive => _isActive; + set isActive(bool value) => _isActive = value; + set lapTime(TimeDuration value) => _lapTime = value; + set elapsedTime(TimeDuration value) => _elapsedTime = value; + TimeDuration get lapTime => _lapTime; + TimeDuration get elapsedTime => _elapsedTime; @override int get id => number; @override bool get isDeletable => false; - Lap({required this.elapsedTime, required this.number, required this.lapTime}); + Lap( + {required int number, + TimeDuration elapsedTime = const TimeDuration(), + TimeDuration lapTime = const TimeDuration(), + bool isActive = false}) + : _lapTime = lapTime, + _number = number, + _elapsedTime = elapsedTime, + _isActive = isActive; Lap.fromJson(Json? json) { if (json == null) { - number = 0; - lapTime = TimeDuration.zero; - elapsedTime = TimeDuration.zero; + _number = 0; + _lapTime = TimeDuration.zero; + _elapsedTime = TimeDuration.zero; + _isActive = false; return; } - number = json['number'] ?? 0; - lapTime = TimeDuration.fromJson(json['lapTime']); - elapsedTime = TimeDuration.fromJson(json['elapsedTime']); + _number = json['number'] ?? 0; + _lapTime = TimeDuration.fromJson(json['lapTime']); + _elapsedTime = TimeDuration.fromJson(json['elapsedTime']); + _isActive = json['isActive'] ?? false; } @override @@ -31,17 +49,23 @@ class Lap extends ListItem { 'number': number, 'lapTime': lapTime.toJson(), 'elapsedTime': elapsedTime.toJson(), + 'isActive': _isActive, }; @override copy() { - return Lap(elapsedTime: elapsedTime, number: number, lapTime: lapTime); + return Lap( + elapsedTime: elapsedTime, + number: number, + lapTime: lapTime, + isActive: _isActive); } @override void copyFrom(other) { - number = other.number; - lapTime = TimeDuration.from(other.lapTime); - elapsedTime = TimeDuration.from(other.elapsedTime); + _number = other.number; + _lapTime = TimeDuration.from(other.lapTime); + _elapsedTime = TimeDuration.from(other.elapsedTime); + _isActive = other._isActive; } } diff --git a/lib/stopwatch/types/stopwatch.dart b/lib/stopwatch/types/stopwatch.dart index 19a88ef4..cc3a2a68 100644 --- a/lib/stopwatch/types/stopwatch.dart +++ b/lib/stopwatch/types/stopwatch.dart @@ -1,10 +1,12 @@ import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/timer_state.dart'; import 'package:clock_app/common/utils/duration.dart'; +import 'package:clock_app/common/utils/json_serialize.dart'; import 'package:clock_app/stopwatch/types/lap.dart'; import 'package:clock_app/timer/types/time_duration.dart'; import 'package:flutter/material.dart'; +// All time units are in milliseconds class ClockStopwatch extends JsonSerializable { int _elapsedMillisecondsOnPause = 0; DateTime _startTime = DateTime(0); @@ -16,28 +18,37 @@ class ClockStopwatch extends JsonSerializable { int get id => _id; List get laps => _laps; + List get finishedLaps => _laps.where((lap) => !lap.isActive).toList(); int get elapsedMilliseconds => _state == TimerState.running ? DateTime.now().difference(_startTime).toTimeDuration().inMilliseconds : _elapsedMillisecondsOnPause; + TimeDuration get elapsedTime => + TimeDuration.fromMilliseconds(elapsedMilliseconds); bool get isRunning => _state == TimerState.running; + bool get isStopped => _state == TimerState.stopped; bool get isStarted => _state == TimerState.running || _state == TimerState.paused; TimerState get state => _state; TimeDuration get currentLapTime => - TimeDuration.fromMilliseconds(elapsedMilliseconds - - (_laps.isNotEmpty ? _laps.first.elapsedTime.inMilliseconds : 0)); - Lap? get previousLap => _laps.isNotEmpty ? _laps.first : null; + TimeDuration.fromMilliseconds(elapsedMilliseconds - lastLapElapsedTime); + int get lastLapElapsedTime { + if (finishedLaps.isEmpty) return 0; + return finishedLaps.first.elapsedTime.inMilliseconds; + } + + Lap? get previousLap => finishedLaps.isNotEmpty ? finishedLaps.first : null; Lap? get fastestLap => _fastestLap; Lap? get slowestLap => _slowestLap; Lap? get averageLap { - if (_laps.isEmpty) return null; - var totalMilliseconds = _laps.fold( + if (finishedLaps.isEmpty) return null; + var totalMilliseconds = finishedLaps.fold( 0, (previousValue, lap) => previousValue + lap.lapTime.inMilliseconds); return Lap( - elapsedTime: - TimeDuration.fromMilliseconds(totalMilliseconds ~/ _laps.length), + elapsedTime: TimeDuration.fromMilliseconds( + totalMilliseconds ~/ finishedLaps.length), number: _laps.length + 1, - lapTime: TimeDuration.fromMilliseconds(totalMilliseconds ~/ _laps.length), + lapTime: TimeDuration.fromMilliseconds( + totalMilliseconds ~/ finishedLaps.length), ); } @@ -95,28 +106,36 @@ class ClockStopwatch extends JsonSerializable { } void updateFastestAndSlowestLap() { - _fastestLap = _laps.reduce((value, element) => + if (finishedLaps.isEmpty) return; + _fastestLap = finishedLaps.reduce((value, element) => value.lapTime.inMilliseconds < element.lapTime.inMilliseconds ? value : element); - _slowestLap = _laps.reduce((value, element) => + _slowestLap = finishedLaps.reduce((value, element) => value.lapTime.inMilliseconds > element.lapTime.inMilliseconds ? value : element); } void addLap() { - if (currentLapTime.inMilliseconds == 0) return; + if (_laps.isNotEmpty) { + if (currentLapTime.inMilliseconds == 0) return; + finishLap(_laps.first); + updateFastestAndSlowestLap(); + } _laps.insert(0, getLap()); - updateFastestAndSlowestLap(); } + void finishLap(Lap lap) { + // This needs to be set before elapsedTime and isActive + lap.lapTime = currentLapTime; + lap.elapsedTime = TimeDuration.fromMilliseconds(elapsedMilliseconds); + lap.isActive = false; + } + + // Lap getLap() { - return Lap( - elapsedTime: TimeDuration.fromMilliseconds(elapsedMilliseconds), - number: _laps.length + 1, - lapTime: currentLapTime, - ); + return Lap(number: finishedLaps.length + 1, isActive: true); } @override @@ -126,7 +145,7 @@ class ClockStopwatch extends JsonSerializable { 'elapsedMillisecondsOnPause': _elapsedMillisecondsOnPause, 'startTime': _startTime.toIso8601String(), 'state': _state.toString(), - 'laps': _laps.map((e) => e.toJson()).toList(), + 'laps': listToString(_laps), }; } @@ -143,6 +162,8 @@ class ClockStopwatch extends JsonSerializable { (e) => e.toString() == (json['state'] ?? ''), orElse: () => TimerState.stopped); _id = json['id'] ?? UniqueKey().hashCode; - _laps = ((json['laps'] ?? []) as List).map((e) => Lap.fromJson(e)).toList(); + // _finishedLaps = []; + _laps = listFromString(json['laps'] ?? ''); + updateFastestAndSlowestLap(); } } diff --git a/lib/stopwatch/widgets/lap_card.dart b/lib/stopwatch/widgets/lap_card.dart index 5ce040a5..bb279d7d 100644 --- a/lib/stopwatch/widgets/lap_card.dart +++ b/lib/stopwatch/widgets/lap_card.dart @@ -1,23 +1,67 @@ import 'package:clock_app/stopwatch/types/lap.dart'; +import 'package:clock_app/stopwatch/types/stopwatch.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class LapCard extends StatefulWidget { - const LapCard({super.key, required this.lap, this.onInit}); +class LapCard extends StatelessWidget { + const LapCard({super.key, required this.lap}); final Lap lap; - final VoidCallback? onInit; @override - State createState() => _LapCardState(); + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Text('${lap.number}'), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(lap.lapTime.toTimeString(showMilliseconds: true), + style: Theme.of(context).textTheme.displaySmall), + Text( + '${AppLocalizations.of(context)!.elapsedTime}: ${lap.elapsedTime.toTimeString(showMilliseconds: true)}'), + ], + ), + ], + ), + ); + } +} + +class ActiveLapCard extends StatefulWidget { + const ActiveLapCard({ + super.key, + required this.stopwatch, + }); + + final ClockStopwatch stopwatch; + + @override + State createState() => _ActiveLapCardState(); } -class _LapCardState extends State { +class _ActiveLapCardState extends State { + late Ticker ticker; + + void tick(Duration elapsed) { + setState(() {}); + } + @override void initState() { + ticker = Ticker(tick); + ticker.start(); super.initState(); - // widget.onInit?.call(); + } + + @override + void dispose() { + ticker.dispose(); + super.dispose(); } @override @@ -26,15 +70,17 @@ class _LapCardState extends State { padding: const EdgeInsets.all(16.0), child: Row( children: [ - Text('${widget.lap.number}'), + Text('${widget.stopwatch.laps.length}'), const SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(widget.lap.lapTime.toTimeString(showMilliseconds: true), + Text( + widget.stopwatch.currentLapTime + .toTimeString(showMilliseconds: true), style: Theme.of(context).textTheme.displaySmall), Text( - '${AppLocalizations.of(context)!.elapsedTime}: ${widget.lap.elapsedTime.toTimeString(showMilliseconds: true)}'), + '${AppLocalizations.of(context)!.elapsedTime}: ${widget.stopwatch.elapsedTime.toTimeString(showMilliseconds: true)}'), ], ), ], diff --git a/lib/stopwatch/widgets/stopwatch_ticker.dart b/lib/stopwatch/widgets/stopwatch_ticker.dart index 5a3bae65..9bbe4a5f 100644 --- a/lib/stopwatch/widgets/stopwatch_ticker.dart +++ b/lib/stopwatch/widgets/stopwatch_ticker.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - class StopwatchTicker extends StatefulWidget { const StopwatchTicker({super.key, required this.stopwatch}); @@ -31,10 +30,6 @@ class _StopwatchTickerState extends State { } void tick(Duration elapsed) { - // var t = elapsed.inMicroseconds * 1e-6; - // double radius = 100; - // drawState.x = radius * math.sin(t); - // drawState.y = radius * math.cos(t); setState(() {}); } @@ -77,9 +72,6 @@ class _StopwatchTickerState extends State { ticker.stop(); ticker.dispose(); - // updateNotificationInterval?.cancel(); - // updateNotificationInterval = null; - super.dispose(); } diff --git a/lib/timer/screens/timer_notification_screen.dart b/lib/timer/screens/timer_notification_screen.dart index 21a8e95e..4a291c79 100644 --- a/lib/timer/screens/timer_notification_screen.dart +++ b/lib/timer/screens/timer_notification_screen.dart @@ -36,13 +36,13 @@ class _TimerNotificationScreenState extends State { ); void _addTime() { - AlarmNotificationManager.snoozeAlarm( - widget.scheduleIds[0], ScheduledNotificationType.timer); + AlarmNotificationManager.dismissNotification(widget.scheduleIds[0], + AlarmDismissType.snooze, ScheduledNotificationType.timer); } void _stop() { - AlarmNotificationManager.dismissAlarm( - widget.scheduleIds[0], ScheduledNotificationType.timer); + AlarmNotificationManager.dismissNotification(widget.scheduleIds[0], + AlarmDismissType.dismiss, ScheduledNotificationType.timer); } @override diff --git a/pubspec.yaml b/pubspec.yaml index d00008a4..25068e71 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: clock_app description: An alarm, clock, timer and stowatch app. publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 0.5.0-beta3+22 +version: 0.5.1+24 environment: sdk: ">=2.18.6 <4.0.0" @@ -41,6 +41,8 @@ dependencies: git: url: https://github.com/AhsanSarwar45/great_list_view ref: master + # great_list_view: + # path: "../great_list_view" get_storage: ^2.1.1 queue: ^3.1.0+2 table_calendar: ^3.0.8