From 716f2114c5288ecd636a8ab5b29d1337bdc92eaf Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Tue, 9 Dec 2025 12:16:23 +0530 Subject: [PATCH] feat(service): add snackbar to undo a timer reset Closes #109 --- .../nsh07/pomodoro/data/StateRepository.kt | 3 ++ .../nsh07/pomodoro/service/ServiceHelper.kt | 6 +++ .../nsh07/pomodoro/service/TimerService.kt | 26 +++++++++- .../pomodoro/service/TimerStateSnapshot.kt | 48 +++++++++++++++++++ .../pomodoro/ui/timerScreen/TimerScreen.kt | 34 ++++++++++--- .../ui/timerScreen/viewModel/TimerAction.kt | 15 +++++- app/src/main/res/values/strings.xml | 2 + 7 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/service/TimerStateSnapshot.kt diff --git a/app/src/main/java/org/nsh07/pomodoro/data/StateRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/StateRepository.kt index 410e711..bcb9262 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/StateRepository.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/StateRepository.kt @@ -20,6 +20,7 @@ package org.nsh07.pomodoro.data import androidx.compose.material3.ColorScheme import androidx.compose.material3.lightColorScheme import kotlinx.coroutines.flow.MutableStateFlow +import org.nsh07.pomodoro.service.TimerStateSnapshot import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState @@ -28,4 +29,6 @@ class StateRepository { val settingsState = MutableStateFlow(SettingsState()) var timerFrequency: Float = 60f var colorScheme: ColorScheme = lightColorScheme() + var timerStateSnapshot: TimerStateSnapshot = + TimerStateSnapshot(time = 0, timerState = TimerState()) } diff --git a/app/src/main/java/org/nsh07/pomodoro/service/ServiceHelper.kt b/app/src/main/java/org/nsh07/pomodoro/service/ServiceHelper.kt index bb7bc89..a749631 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/ServiceHelper.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/ServiceHelper.kt @@ -35,6 +35,12 @@ class ServiceHelper(private val context: Context) { context.startService(it) } + TimerAction.UndoReset -> + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.UNDO_RESET.toString() + context.startService(it) + } + is TimerAction.SkipTimer -> Intent(context, TimerService::class.java).also { it.action = TimerService.Actions.SKIP.toString() diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt index 892d5ac..88aa940 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -75,6 +75,8 @@ class TimerService : Service() { private var lastSavedDuration = 0L + private val timerStateSnapshot by lazy { stateRepository.timerStateSnapshot } + private val saveLock = Mutex() private var job = SupervisorJob() private val timerScope = CoroutineScope(Dispatchers.IO + job) @@ -134,6 +136,8 @@ class TimerService : Service() { } } + Actions.UNDO_RESET.toString() -> undoReset() + Actions.SKIP.toString() -> skipScope.launch { skipTimer(true) } Actions.STOP_ALARM.toString() -> stopAlarm() @@ -326,6 +330,16 @@ class TimerService : Service() { private suspend fun resetTimer() { val settingsState = _settingsState.value + timerStateSnapshot.save( + lastSavedDuration, + time, + cycles, + startTime, + pauseTime, + pauseDuration, + _timerState.value + ) + saveTimeToDb() lastSavedDuration = 0 time = settingsState.focusTime @@ -349,6 +363,16 @@ class TimerService : Service() { updateProgressSegments() } + private fun undoReset() { + lastSavedDuration = timerStateSnapshot.lastSavedDuration + time = timerStateSnapshot.time + cycles = timerStateSnapshot.cycles + startTime = timerStateSnapshot.startTime + pauseTime = timerStateSnapshot.pauseTime + pauseDuration = timerStateSnapshot.pauseDuration + _timerState.update { timerStateSnapshot.timerState } + } + private suspend fun skipTimer(fromButton: Boolean = false) { val settingsState = _settingsState.value saveTimeToDb() @@ -517,6 +541,6 @@ class TimerService : Service() { } enum class Actions { - TOGGLE, SKIP, RESET, STOP_ALARM, UPDATE_ALARM_TONE + TOGGLE, SKIP, RESET, UNDO_RESET, STOP_ALARM, UPDATE_ALARM_TONE } } diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerStateSnapshot.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerStateSnapshot.kt new file mode 100644 index 0000000..115b59c --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerStateSnapshot.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.service + +import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState + +data class TimerStateSnapshot( + var lastSavedDuration: Long = 0L, + var time: Long, + var cycles: Int = 0, + var startTime: Long = 0L, + var pauseTime: Long = 0L, + var pauseDuration: Long = 0L, + var timerState: TimerState +) { + fun save( + lastSavedDuration: Long, + time: Long, + cycles: Int, + startTime: Long, + pauseTime: Long, + pauseDuration: Long, + timerState: TimerState + ) { + this.lastSavedDuration = lastSavedDuration + this.time = time + this.cycles = cycles + this.startTime = startTime + this.pauseTime = pauseTime + this.pauseDuration = pauseDuration + this.timerState = timerState + } +} diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt index 359bce6..b2539c6 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt @@ -18,6 +18,7 @@ package org.nsh07.pomodoro.ui.timerScreen import android.Manifest +import android.annotation.SuppressLint import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -65,6 +66,10 @@ import androidx.compose.material3.MaterialTheme.motionScheme import androidx.compose.material3.MaterialTheme.shapes import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -74,6 +79,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally @@ -83,6 +89,7 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource @@ -94,8 +101,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation3.ui.LocalNavAnimatedContentScope +import kotlinx.coroutines.launch import org.nsh07.pomodoro.R -import org.nsh07.pomodoro.ui.mergePaddingValues import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex600 import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.TomatoTheme @@ -103,7 +110,6 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState - @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun SharedTransitionScope.TimerScreen( @@ -115,7 +121,9 @@ fun SharedTransitionScope.TimerScreen( modifier: Modifier = Modifier ) { val motionScheme = motionScheme + val scope = rememberCoroutineScope() val haptic = LocalHapticFeedback.current + val context = LocalContext.current val color by animateColorAsState( if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary @@ -139,6 +147,7 @@ fun SharedTransitionScope.TimerScreen( ) val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val snackbarHostState = remember { SnackbarHostState() } Scaffold( topBar = { @@ -215,16 +224,16 @@ fun SharedTransitionScope.TimerScreen( scrollBehavior = scrollBehavior ) }, + bottomBar = { Spacer(Modifier.height(contentPadding.calculateBottomPadding())) }, + snackbarHost = { SnackbarHost(snackbarHostState) }, modifier = modifier .nestedScroll(scrollBehavior.nestedScrollConnection) ) { innerPadding -> - val insets = mergePaddingValues(innerPadding, contentPadding) LazyColumn( verticalArrangement = Arrangement.Center, horizontalAlignment = CenterHorizontally, - contentPadding = insets, - modifier = Modifier - .fillMaxSize() + contentPadding = innerPadding, + modifier = Modifier.fillMaxSize() ) { item { Column(horizontalAlignment = CenterHorizontally) { @@ -411,6 +420,19 @@ fun SharedTransitionScope.TimerScreen( onClick = { onAction(TimerAction.ResetTimer) haptic.performHapticFeedback(HapticFeedbackType.VirtualKey) + + @SuppressLint("LocalContextGetResourceValueCall") + scope.launch { + val result = snackbarHostState.showSnackbar( + context.getString(R.string.timer_reset_message), + actionLabel = context.getString(R.string.undo), + withDismissAction = true, + duration = SnackbarDuration.Long + ) + if (result == SnackbarResult.ActionPerformed) { + onAction(TimerAction.UndoReset) + } + } }, colors = IconButtonDefaults.filledTonalIconButtonColors( containerColor = colorContainer diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerAction.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerAction.kt index 2134c1c..1b542a4 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerAction.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerAction.kt @@ -1,8 +1,18 @@ /* * Copyright (c) 2025 Nishant Mishra * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . */ package org.nsh07.pomodoro.ui.timerScreen.viewModel @@ -11,6 +21,7 @@ sealed interface TimerAction { data class SkipTimer(val fromButton: Boolean) : TimerAction data object ResetTimer : TimerAction + data object UndoReset : TimerAction data object StopAlarm : TimerAction data object ToggleTimer : TimerAction } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0c54743..e037f45 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -111,4 +111,6 @@ Auto start next timer Secure AOD Automatically lock your device after a timeout, while keeping the AOD visible + Timer reset + Undo \ No newline at end of file