Skip to content
1 change: 1 addition & 0 deletions lib/alarm/data/alarm_settings_schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ SettingGroup alarmSettingsSchema = SettingGroup(
30,
5,
unit: "minutes",
snapLength: 1,
enableConditions: [
ValueCondition(["Enabled"], (value) => value == true)
]),
Expand Down
45 changes: 34 additions & 11 deletions lib/alarm/logic/alarm_isolate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -70,14 +71,18 @@ void stopScheduledNotification(List<dynamic> 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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
28 changes: 28 additions & 0 deletions lib/alarm/screens/alarm_notification_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -100,6 +102,16 @@ class _AlarmNotificationScreenState extends State<AlarmNotificationScreen> {
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(
Expand Down Expand Up @@ -153,6 +165,22 @@ class _AlarmNotificationScreenState extends State<AlarmNotificationScreen> {
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),
),
),
],
),
),
Expand Down
17 changes: 11 additions & 6 deletions lib/alarm/types/alarm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -215,25 +218,27 @@ class Alarm extends CustomizableListItem {
}
}

Future<void> snooze() async {
Future<void> 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<void> _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",
);
}

Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 11 additions & 6 deletions lib/notifications/logic/alarm_notifications.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,10 @@ Future<void> closeAlarmNotification(ScheduledNotificationType type) async {
logger.t("[closeAlarmNotification] Notification closed");
}

Future<void> snoozeAlarm(int scheduleId, ScheduledNotificationType type) async {
await stopAlarm(scheduleId, type, AlarmStopAction.snooze);
Future<void> snoozeAlarm(int scheduleId, ScheduledNotificationType type,
{int? snoozeSeconds}) async {
await stopAlarm(scheduleId, type, AlarmStopAction.snooze,
snoozeSeconds: snoozeSeconds);
}

Future<void> dismissAlarm(
Expand All @@ -138,15 +140,18 @@ Future<void> dismissAlarm(
}

Future<void> 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<void> dismissAlarmNotification(int scheduleId,
AlarmDismissType dismissType, ScheduledNotificationType type) async {
AlarmDismissType dismissType, ScheduledNotificationType type,
{int? snoozeSeconds}) async {
logger.t("[dismissAlarmNotification]");

switch (dismissType) {
Expand All @@ -159,7 +164,7 @@ Future<void> dismissAlarmNotification(int scheduleId,
});
break;
case AlarmDismissType.snooze:
await snoozeAlarm(scheduleId, type);
await snoozeAlarm(scheduleId, type, snoozeSeconds: snoozeSeconds);
break;

case AlarmDismissType.unsnooze:
Expand Down
5 changes: 5 additions & 0 deletions lib/settings/data/general_settings_schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@ SettingGroup generalSettingsSchema = SettingGroup(
),
],
),
SwitchSetting(
"Enable Custom Snooze",
(context) => AppLocalizations.of(context)!.enableCustomSnoozeSetting,
false,
),
]),
SettingPageLink(
"Melodies",
Expand Down
29 changes: 29 additions & 0 deletions lib/timer/screens/timer_notification_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -46,6 +48,18 @@ class _TimerNotificationScreenState extends State<TimerNotificationScreen> {
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 {
Expand Down Expand Up @@ -121,6 +135,21 @@ class _TimerNotificationScreenState extends State<TimerNotificationScreen> {
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),
),
),
],
),
),
Expand Down
4 changes: 2 additions & 2 deletions lib/timer/types/timer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ class ClockTimer extends CustomizableListItem {
}
}

Future<void> snooze() async {
TimeDuration addedDuration = TimeDuration(minutes: addLength.floor());
Future<void> snooze({TimeDuration? duration}) async {
final addedDuration = duration ?? TimeDuration(minutes: addLength.floor());
_currentDuration = addedDuration;
_milliSecondsRemainingOnPause = addedDuration.inSeconds * 1000;
await start();
Expand Down