From 9c3f8a9092250a6f4016ba18e30d02a9216ad918 Mon Sep 17 00:00:00 2001 From: "Jonathan K. Chang" Date: Fri, 22 May 2026 00:56:20 -0400 Subject: [PATCH 1/8] feat(settings): add Enable Custom Snooze switch setting - Add a new toggle in General > Interactions settings to enable/disable custom snooze duration for alarms and timers. - Add 'Enable Custom Snooze' and 'Custom Snooze...' labels for the new custom snooze feature in alarm and timer notification screens. --- lib/l10n/app_en.arb | 4 ++++ lib/settings/data/general_settings_schema.dart | 5 +++++ 2 files changed, 9 insertions(+) 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/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", From 0f07556081181722e5cb29ca13e298fea0ef7145 Mon Sep 17 00:00:00 2001 From: "Jonathan K. Chang" Date: Fri, 22 May 2026 00:56:56 -0400 Subject: [PATCH 2/8] feat(alarm): support custom snooze duration in Alarm model Add _snoozeDuration field and update snooze() to accept optional minutes or seconds parameters. Uses custom duration when provided, falls back to configured snoozeLength otherwise. --- lib/alarm/types/alarm.dart | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) 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", ); } From b5c9db0d333cc61626f4f7f3ee303a972861008a Mon Sep 17 00:00:00 2001 From: "Jonathan K. Chang" Date: Fri, 22 May 2026 00:57:32 -0400 Subject: [PATCH 3/8] feat(timer): accept optional TimeDuration in Timer.snooze() Update snooze() to accept an optional TimeDuration parameter. When provided, uses the custom duration instead of the default addLength. --- lib/timer/types/timer.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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(); From 5811730c37742cddc3de84ae7c249cffd6525fd3 Mon Sep 17 00:00:00 2001 From: "Jonathan K. Chang" Date: Fri, 22 May 2026 00:57:51 -0400 Subject: [PATCH 4/8] feat(notifications): propagate custom snooze seconds through notification layer Update stopAlarm(), dismissAlarmNotification(), and snoozeAlarm() to accept and forward optional snoozeSeconds through the isolate message channel. --- .../logic/alarm_notifications.dart | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) 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: From a02acd8fafd7325d663652406966e33c96ea4e58 Mon Sep 17 00:00:00 2001 From: "Jonathan K. Chang" Date: Fri, 22 May 2026 00:58:12 -0400 Subject: [PATCH 5/8] feat(alarm): handle custom snooze duration in alarm isolate Parse optional snoozeSeconds from isolate message and pass to stopAlarm/stopTimer. Use custom duration when provided, otherwise fall back to configured snoozeLength for alarms or default snooze for timers. --- lib/alarm/logic/alarm_isolate.dart | 34 +++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index 62f868d7..00211b12 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,13 +245,18 @@ 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(); + if (snoozeSeconds != null) { + await timer.snooze(duration: TimeDuration(seconds: snoozeSeconds)); + } else { + await timer.snooze(); + } }); } else if (action == AlarmStopAction.dismiss) { // If there was an alarm already ringing when the timer was triggered, we From 9c20f89484720bd825144ddea3541400b168c7a1 Mon Sep 17 00:00:00 2001 From: "Jonathan K. Chang" Date: Fri, 22 May 2026 00:58:33 -0400 Subject: [PATCH 6/8] feat(ui): add custom snooze button to alarm and timer notifications Add a 'Custom Snooze...' button to both alarm and timer notification screens when the feature is enabled. Opens the duration picker and passes the selected duration through to the snooze action. --- .../screens/alarm_notification_screen.dart | 28 ++++++++++++++++++ .../screens/timer_notification_screen.dart | 29 +++++++++++++++++++ 2 files changed, 57 insertions(+) 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/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), + ), + ), ], ), ), From 8a2af39e2c743fe7edaa34ec3e411c790eae417b Mon Sep 17 00:00:00 2001 From: "Jonathan K. Chang" Date: Fri, 22 May 2026 02:33:10 -0400 Subject: [PATCH 7/8] fix(alarm): snap snooze length slider to whole minutes Add snapLength: 1 to the snooze Length setting so the preset snooze duration only accepts whole minutes. Custom snooze via the duration picker is unaffected and still allows sub-minute values. --- lib/alarm/data/alarm_settings_schema.dart | 1 + 1 file changed, 1 insertion(+) 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) ]), From e724ee415d73987f83ef45360ea2b65e4398103a Mon Sep 17 00:00:00 2001 From: "Jonathan K. Chang" Date: Sun, 24 May 2026 17:21:55 -0400 Subject: [PATCH 8/8] fix(alarm): treat zero-duration snooze as dismiss to ensure dismiss logic When custom snooze is set to 0, convert the action to dismiss instead of entering the snooze code path. This ensures alarms marked for "Delete After Dismissed" are properly removed when a 0-second snooze is selected. --- lib/alarm/logic/alarm_isolate.dart | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index 00211b12..805e4a01 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -251,14 +251,19 @@ void stopTimer(int scheduleId, AlarmStopAction action, ClockTimer? timer = getTimerById(scheduleId); if (timer == null) return; if (action == AlarmStopAction.snooze) { - updateTimerById(scheduleId, (timer) async { - if (snoozeSeconds != null) { - await timer.snooze(duration: TimeDuration(seconds: snoozeSeconds)); - } else { + if (snoozeSeconds == 0) { + action = AlarmStopAction.dismiss; + } else if (snoozeSeconds == null) { + updateTimerById(scheduleId, (timer) async { await timer.snooze(); - } - }); - } else if (action == AlarmStopAction.dismiss) { + }); + } 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) {