diff --git a/lib/alarm/data/alarm_settings_schema.dart b/lib/alarm/data/alarm_settings_schema.dart index ffb055b5..e22f7f23 100644 --- a/lib/alarm/data/alarm_settings_schema.dart +++ b/lib/alarm/data/alarm_settings_schema.dart @@ -252,6 +252,7 @@ SettingGroup alarmSettingsSchema = SettingGroup( 30, 5, unit: "minutes", + snapLength: 1, enableConditions: [ ValueCondition(["Enabled"], (value) => value == true) ]), diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index 62f868d7..805e4a01 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -8,6 +8,7 @@ import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/notifications/logic/alarm_notifications.dart'; import 'package:clock_app/system/logic/initialize_isolate.dart'; +import 'package:clock_app/timer/types/time_duration.dart'; import 'package:clock_app/timer/types/timer.dart'; import 'package:flutter/foundation.dart'; import 'package:clock_app/alarm/logic/schedule_alarm.dart'; @@ -70,14 +71,18 @@ void stopScheduledNotification(List message) { int scheduleId = message[0]; RingtonePlayer.stop(); AlarmStopAction action = AlarmStopAction.values.byName(message[2]); + // message[3] is optional snoozeSeconds (total seconds, no rounding) + int? snoozeSeconds = message.length > 3 && message[3] != null + ? (message[3] as num).toInt() + : null; ScheduledNotificationType notificationType = ScheduledNotificationType.values.byName(message[1]); if (notificationType == ScheduledNotificationType.alarm) { - stopAlarm(scheduleId, action); + stopAlarm(scheduleId, action, snoozeSeconds: snoozeSeconds); } else if (notificationType == ScheduledNotificationType.timer) { - stopTimer(scheduleId, action); + stopTimer(scheduleId, action, snoozeSeconds: snoozeSeconds); } logger.t( @@ -178,10 +183,18 @@ void setVolume(double volume) { RingtonePlayer.setVolume(volume / 100); } -void stopAlarm(int scheduleId, AlarmStopAction action) async { - logger.i("[stopAlarm] Stopping alarm $scheduleId with action: ${action.name}"); +void stopAlarm(int scheduleId, AlarmStopAction action, + {int? snoozeSeconds}) async { + logger.i( + "[stopAlarm] Stopping alarm $scheduleId with action: ${action.name}, snoozeSeconds=$snoozeSeconds"); if (action == AlarmStopAction.snooze) { - await updateAlarmById(scheduleId, (alarm) async => await alarm.snooze()); + if (snoozeSeconds != null) { + await updateAlarmById(scheduleId, + (alarm) async => await alarm.snooze(seconds: snoozeSeconds)); + } else { + await updateAlarmById(scheduleId, (alarm) async => + await alarm.snooze(minutes: alarm.snoozeLength.toInt())); + } // await createSnoozeNotification(scheduleId); } else if (action == AlarmStopAction.dismiss) { // If there was a timer ringing when the alarm was triggered, resume it now @@ -232,15 +245,25 @@ void triggerTimer(int scheduleId, Json params) async { ); } -void stopTimer(int scheduleId, AlarmStopAction action) async { - logger.i("Stopping timer $scheduleId with action: ${action.name}"); +void stopTimer(int scheduleId, AlarmStopAction action, + {int? snoozeSeconds}) async { + logger.i("Stopping timer $scheduleId with action: ${action.name}, snoozeSeconds=$snoozeSeconds"); ClockTimer? timer = getTimerById(scheduleId); if (timer == null) return; if (action == AlarmStopAction.snooze) { - updateTimerById(scheduleId, (timer) async { - await timer.snooze(); - }); - } else if (action == AlarmStopAction.dismiss) { + if (snoozeSeconds == 0) { + action = AlarmStopAction.dismiss; + } else if (snoozeSeconds == null) { + updateTimerById(scheduleId, (timer) async { + await timer.snooze(); + }); + } else { + updateTimerById(scheduleId, (timer) async { + await timer.snooze(duration: TimeDuration(seconds: snoozeSeconds)); + }); + } + } + if (action == AlarmStopAction.dismiss) { // If there was an alarm already ringing when the timer was triggered, we // need to resume it now if (RingingManager.isAlarmRinging) { diff --git a/lib/alarm/screens/alarm_notification_screen.dart b/lib/alarm/screens/alarm_notification_screen.dart index d4e82fe6..1146bd0f 100644 --- a/lib/alarm/screens/alarm_notification_screen.dart +++ b/lib/alarm/screens/alarm_notification_screen.dart @@ -12,7 +12,9 @@ import 'package:clock_app/notifications/types/alarm_notification_arguments.dart' import 'package:clock_app/navigation/types/alignment.dart'; import 'package:clock_app/notifications/widgets/notification_actions/slide_notification_action.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/timer/widgets/duration_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class AlarmNotificationScreen extends StatefulWidget { const AlarmNotificationScreen({ @@ -100,6 +102,16 @@ class _AlarmNotificationScreenState extends State { ScheduledNotificationType.alarm); } + void _customSnooze() async { + final duration = await showDurationPicker(context); + if (duration != null) { + _setNextWidget(); + // Pass snoozeSeconds (total seconds, no rounding loss for sub-minute values). + dismissAlarmNotification(widget.scheduleId, AlarmDismissType.snooze, + ScheduledNotificationType.alarm, snoozeSeconds: duration.inSeconds); + } + } + @override Widget build(BuildContext context) { return WillPopScope( @@ -153,6 +165,22 @@ class _AlarmNotificationScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ _currentWidget, + if (appSettings + .getGroup("General") + .getGroup("Interactions") + .getSetting("Enable Custom Snooze") + .value && + alarm.canBeSnoozed) + TextButton( + onPressed: _customSnooze, + child: Text( + AppLocalizations.of(context)!.customSnoozeButton, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Theme.of(context).colorScheme.primary), + ), + ), ], ), ), diff --git a/lib/alarm/types/alarm.dart b/lib/alarm/types/alarm.dart index d4f004e3..b62d0c20 100644 --- a/lib/alarm/types/alarm.dart +++ b/lib/alarm/types/alarm.dart @@ -42,6 +42,7 @@ class Alarm extends CustomizableListItem { bool _markedForDeletion = false; // bool _isFinished = false; DateTime? _snoozeTime; + Duration _snoozeDuration = Duration.zero; int _snoozeCount = 0; DateTime? _skippedTime; SettingGroup _settings = SettingGroup( @@ -142,6 +143,7 @@ class Alarm extends CustomizableListItem { _snoozeCount = alarm._snoozeCount, _snoozeTime = alarm._snoozeTime, _markedForDeletion = alarm._markedForDeletion, + _snoozeDuration = alarm._snoozeDuration, _skippedTime = alarm._skippedTime, _settings = alarm._settings.copy() { _schedules = createSchedules(_settings); @@ -153,6 +155,7 @@ class Alarm extends CustomizableListItem { _time = other._time; _snoozeCount = other._snoozeCount; _snoozeTime = other._snoozeTime; + _snoozeDuration = other._snoozeDuration; _skippedTime = other._skippedTime; _settings = other._settings.copy(); _schedules = other._schedules; @@ -215,25 +218,27 @@ class Alarm extends CustomizableListItem { } } - Future snooze() async { + Future snooze({int? minutes, int? seconds}) async { + final selectedDuration = seconds != null + ? Duration(seconds: seconds) + : Duration(minutes: minutes ?? snoozeLength.floor()); // The alarm can only be snoozed the number of times specified in the settings _snoozeCount++; // When the alarm rang, it was disabled, so we need to enable it again if the user presses snooze _isEnabled = true; // Snoozing should cancel any skip _skippedTime = null; - _snoozeTime = DateTime.now().add( - Duration(minutes: snoozeLength.floor()), - ); + _snoozeTime = DateTime.now().add(selectedDuration); + _snoozeDuration = selectedDuration; await _scheduleSnooze(); } Future _scheduleSnooze() async { await scheduleSnoozeAlarm( id, - Duration(minutes: snoozeLength.floor()), + _snoozeDuration, ScheduledNotificationType.alarm, - "_scheduleSnooze(): Alarm snoozed for $snoozeLength minutes", + "_scheduleSnooze(): Alarm snoozed for ${_snoozeDuration.inMinutes} minutes", ); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7596b366..0d9593c3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -533,6 +533,10 @@ "@showSortSetting": {}, "notificationsSettingGroup": "Notifications", "@notificationsSettingGroup": {}, + "enableCustomSnoozeSetting": "Enable Custom Snooze", + "@enableCustomSnoozeSetting": {}, + "customSnoozeButton": "Custom Snooze...", + "@customSnoozeButton": {}, "showUpcomingAlarmNotificationSetting": "Show Upcoming Alarm Notifications", "@showUpcomingAlarmNotificationSetting": {}, "upcomingLeadTimeSetting": "Upcoming Lead Time", diff --git a/lib/notifications/logic/alarm_notifications.dart b/lib/notifications/logic/alarm_notifications.dart index 6d0827fe..b763d7a9 100644 --- a/lib/notifications/logic/alarm_notifications.dart +++ b/lib/notifications/logic/alarm_notifications.dart @@ -128,8 +128,10 @@ Future closeAlarmNotification(ScheduledNotificationType type) async { logger.t("[closeAlarmNotification] Notification closed"); } -Future snoozeAlarm(int scheduleId, ScheduledNotificationType type) async { - await stopAlarm(scheduleId, type, AlarmStopAction.snooze); +Future snoozeAlarm(int scheduleId, ScheduledNotificationType type, + {int? snoozeSeconds}) async { + await stopAlarm(scheduleId, type, AlarmStopAction.snooze, + snoozeSeconds: snoozeSeconds); } Future dismissAlarm( @@ -138,15 +140,18 @@ Future dismissAlarm( } Future stopAlarm(int scheduleId, ScheduledNotificationType type, - AlarmStopAction action) async { + AlarmStopAction action, { + int? snoozeSeconds, // Optional custom snooze duration in total seconds +}) async { // Send a message to tell the alarm isolate to run the code to stop alarm // See stopScheduledNotification in lib/alarm/logic/alarm_isolate.dart IsolateNameServer.lookupPortByName(stopAlarmPortName) - ?.send([scheduleId, type.name, action.name]); + ?.send([scheduleId, type.name, action.name, snoozeSeconds]); } Future dismissAlarmNotification(int scheduleId, - AlarmDismissType dismissType, ScheduledNotificationType type) async { + AlarmDismissType dismissType, ScheduledNotificationType type, + {int? snoozeSeconds}) async { logger.t("[dismissAlarmNotification]"); switch (dismissType) { @@ -159,7 +164,7 @@ Future dismissAlarmNotification(int scheduleId, }); break; case AlarmDismissType.snooze: - await snoozeAlarm(scheduleId, type); + await snoozeAlarm(scheduleId, type, snoozeSeconds: snoozeSeconds); break; case AlarmDismissType.unsnooze: diff --git a/lib/settings/data/general_settings_schema.dart b/lib/settings/data/general_settings_schema.dart index 3044e917..843ca9cd 100644 --- a/lib/settings/data/general_settings_schema.dart +++ b/lib/settings/data/general_settings_schema.dart @@ -241,6 +241,11 @@ SettingGroup generalSettingsSchema = SettingGroup( ), ], ), + SwitchSetting( + "Enable Custom Snooze", + (context) => AppLocalizations.of(context)!.enableCustomSnoozeSetting, + false, + ), ]), SettingPageLink( "Melodies", diff --git a/lib/timer/screens/timer_notification_screen.dart b/lib/timer/screens/timer_notification_screen.dart index 1886e3a5..54e28aeb 100644 --- a/lib/timer/screens/timer_notification_screen.dart +++ b/lib/timer/screens/timer_notification_screen.dart @@ -9,7 +9,9 @@ import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/timer/types/time_duration.dart'; import 'package:clock_app/timer/types/timer.dart'; import 'package:clock_app/timer/utils/timer_id.dart'; +import 'package:clock_app/timer/widgets/duration_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class TimerNotificationScreen extends StatefulWidget { const TimerNotificationScreen({ @@ -46,6 +48,18 @@ class _TimerNotificationScreenState extends State { AlarmDismissType.dismiss, ScheduledNotificationType.timer); } + void _customSnooze() async { + final timer = getTimerById(widget.scheduleIds.first); + if (timer == null) return; + final duration = await showDurationPicker(context); + if (duration != null) { + // Pass snoozeSeconds (total seconds, no rounding loss for sub-minute values). + dismissAlarmNotification(widget.scheduleIds[0], + AlarmDismissType.snooze, ScheduledNotificationType.timer, + snoozeSeconds: duration.inSeconds); + } + } + @override void initState() { try { @@ -121,6 +135,21 @@ class _TimerNotificationScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ actionWidget, + if (appSettings + .getGroup("General") + .getGroup("Interactions") + .getSetting("Enable Custom Snooze") + .value) + TextButton( + onPressed: _customSnooze, + child: Text( + AppLocalizations.of(context)!.customSnoozeButton, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Theme.of(context).colorScheme.primary), + ), + ), ], ), ), diff --git a/lib/timer/types/timer.dart b/lib/timer/types/timer.dart index 6cd2b9e7..91f1c444 100644 --- a/lib/timer/types/timer.dart +++ b/lib/timer/types/timer.dart @@ -152,8 +152,8 @@ class ClockTimer extends CustomizableListItem { } } - Future snooze() async { - TimeDuration addedDuration = TimeDuration(minutes: addLength.floor()); + Future snooze({TimeDuration? duration}) async { + final addedDuration = duration ?? TimeDuration(minutes: addLength.floor()); _currentDuration = addedDuration; _milliSecondsRemainingOnPause = addedDuration.inSeconds * 1000; await start();