@@ -20,6 +20,7 @@ package org.nsh07.pomodoro.data
|
|||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.nsh07.pomodoro.service.TimerStateSnapshot
|
||||||
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
|
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
|
||||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
||||||
|
|
||||||
@@ -28,4 +29,6 @@ class StateRepository {
|
|||||||
val settingsState = MutableStateFlow(SettingsState())
|
val settingsState = MutableStateFlow(SettingsState())
|
||||||
var timerFrequency: Float = 60f
|
var timerFrequency: Float = 60f
|
||||||
var colorScheme: ColorScheme = lightColorScheme()
|
var colorScheme: ColorScheme = lightColorScheme()
|
||||||
|
var timerStateSnapshot: TimerStateSnapshot =
|
||||||
|
TimerStateSnapshot(time = 0, timerState = TimerState())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ class ServiceHelper(private val context: Context) {
|
|||||||
context.startService(it)
|
context.startService(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TimerAction.UndoReset ->
|
||||||
|
Intent(context, TimerService::class.java).also {
|
||||||
|
it.action = TimerService.Actions.UNDO_RESET.toString()
|
||||||
|
context.startService(it)
|
||||||
|
}
|
||||||
|
|
||||||
is TimerAction.SkipTimer ->
|
is TimerAction.SkipTimer ->
|
||||||
Intent(context, TimerService::class.java).also {
|
Intent(context, TimerService::class.java).also {
|
||||||
it.action = TimerService.Actions.SKIP.toString()
|
it.action = TimerService.Actions.SKIP.toString()
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ class TimerService : Service() {
|
|||||||
|
|
||||||
private var lastSavedDuration = 0L
|
private var lastSavedDuration = 0L
|
||||||
|
|
||||||
|
private val timerStateSnapshot by lazy { stateRepository.timerStateSnapshot }
|
||||||
|
|
||||||
private val saveLock = Mutex()
|
private val saveLock = Mutex()
|
||||||
private var job = SupervisorJob()
|
private var job = SupervisorJob()
|
||||||
private val timerScope = CoroutineScope(Dispatchers.IO + job)
|
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.SKIP.toString() -> skipScope.launch { skipTimer(true) }
|
||||||
|
|
||||||
Actions.STOP_ALARM.toString() -> stopAlarm()
|
Actions.STOP_ALARM.toString() -> stopAlarm()
|
||||||
@@ -326,6 +330,16 @@ class TimerService : Service() {
|
|||||||
private suspend fun resetTimer() {
|
private suspend fun resetTimer() {
|
||||||
val settingsState = _settingsState.value
|
val settingsState = _settingsState.value
|
||||||
|
|
||||||
|
timerStateSnapshot.save(
|
||||||
|
lastSavedDuration,
|
||||||
|
time,
|
||||||
|
cycles,
|
||||||
|
startTime,
|
||||||
|
pauseTime,
|
||||||
|
pauseDuration,
|
||||||
|
_timerState.value
|
||||||
|
)
|
||||||
|
|
||||||
saveTimeToDb()
|
saveTimeToDb()
|
||||||
lastSavedDuration = 0
|
lastSavedDuration = 0
|
||||||
time = settingsState.focusTime
|
time = settingsState.focusTime
|
||||||
@@ -349,6 +363,16 @@ class TimerService : Service() {
|
|||||||
updateProgressSegments()
|
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) {
|
private suspend fun skipTimer(fromButton: Boolean = false) {
|
||||||
val settingsState = _settingsState.value
|
val settingsState = _settingsState.value
|
||||||
saveTimeToDb()
|
saveTimeToDb()
|
||||||
@@ -517,6 +541,6 @@ class TimerService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class Actions {
|
enum class Actions {
|
||||||
TOGGLE, SKIP, RESET, STOP_ALARM, UPDATE_ALARM_TONE
|
TOGGLE, SKIP, RESET, UNDO_RESET, STOP_ALARM, UPDATE_ALARM_TONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
package org.nsh07.pomodoro.ui.timerScreen
|
package org.nsh07.pomodoro.ui.timerScreen
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
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.shapes
|
||||||
import androidx.compose.material3.MaterialTheme.typography
|
import androidx.compose.material3.MaterialTheme.typography
|
||||||
import androidx.compose.material3.Scaffold
|
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.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -74,6 +79,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
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.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.painterResource
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.nsh07.pomodoro.R
|
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.googleFlex600
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
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.TimerMode
|
||||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SharedTransitionScope.TimerScreen(
|
fun SharedTransitionScope.TimerScreen(
|
||||||
@@ -115,7 +121,9 @@ fun SharedTransitionScope.TimerScreen(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val motionScheme = motionScheme
|
val motionScheme = motionScheme
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val color by animateColorAsState(
|
val color by animateColorAsState(
|
||||||
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary
|
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary
|
||||||
@@ -139,6 +147,7 @@ fun SharedTransitionScope.TimerScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -215,16 +224,16 @@ fun SharedTransitionScope.TimerScreen(
|
|||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
bottomBar = { Spacer(Modifier.height(contentPadding.calculateBottomPadding())) },
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
val insets = mergePaddingValues(innerPadding, contentPadding)
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = CenterHorizontally,
|
horizontalAlignment = CenterHorizontally,
|
||||||
contentPadding = insets,
|
contentPadding = innerPadding,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize()
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Column(horizontalAlignment = CenterHorizontally) {
|
Column(horizontalAlignment = CenterHorizontally) {
|
||||||
@@ -411,6 +420,19 @@ fun SharedTransitionScope.TimerScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
onAction(TimerAction.ResetTimer)
|
onAction(TimerAction.ResetTimer)
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)
|
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(
|
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
||||||
containerColor = colorContainer
|
containerColor = colorContainer
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*
|
||||||
|
* 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
|
package org.nsh07.pomodoro.ui.timerScreen.viewModel
|
||||||
@@ -11,6 +21,7 @@ sealed interface TimerAction {
|
|||||||
data class SkipTimer(val fromButton: Boolean) : TimerAction
|
data class SkipTimer(val fromButton: Boolean) : TimerAction
|
||||||
|
|
||||||
data object ResetTimer : TimerAction
|
data object ResetTimer : TimerAction
|
||||||
|
data object UndoReset : TimerAction
|
||||||
data object StopAlarm : TimerAction
|
data object StopAlarm : TimerAction
|
||||||
data object ToggleTimer : TimerAction
|
data object ToggleTimer : TimerAction
|
||||||
}
|
}
|
||||||
@@ -111,4 +111,6 @@
|
|||||||
<string name="auto_start_next_timer">Auto start next timer</string>
|
<string name="auto_start_next_timer">Auto start next timer</string>
|
||||||
<string name="secure_aod">Secure AOD</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="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>
|
</resources>
|
||||||
Reference in New Issue
Block a user