feat(service): add snackbar to undo a timer reset

Closes #109
This commit is contained in:
Nishant Mishra
2025-12-09 12:16:23 +05:30
parent 24e0f083ee
commit 716f2114c5
7 changed files with 125 additions and 9 deletions

View File

@@ -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())
}

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}

View File

@@ -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

View File

@@ -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 <https://www.gnu.org/licenses/>.
* 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 <https://www.gnu.org/licenses/>.
*/
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
}

View File

@@ -111,4 +111,6 @@
<string name="auto_start_next_timer">Auto start next timer</string>
<string name="secure_aod">Secure AOD</string>
<string name="secure_aod_desc">Automatically lock your device after a timeout, while keeping the AOD visible</string>
<string name="timer_reset_message">Timer reset</string>
<string name="undo">Undo</string>
</resources>