Make session length slider functional

This commit is contained in:
Nishant Mishra
2025-07-08 10:03:03 +05:30
parent 40c6608d79
commit e9d30bc0f6
9 changed files with 110 additions and 52 deletions

View File

@@ -9,7 +9,7 @@ import org.nsh07.pomodoro.ui.AppScreen
import org.nsh07.pomodoro.ui.NavItem
import org.nsh07.pomodoro.ui.Screen
import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.ui.viewModel.TimerViewModel
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
class MainActivity : ComponentActivity() {

View File

@@ -4,10 +4,12 @@ interface TimerRepository {
var focusTime: Int
var shortBreakTime: Int
var longBreakTime: Int
var sessionLength: Int
}
class AppTimerRepository : TimerRepository {
override var focusTime = 25 * 60 * 1000
override var shortBreakTime = 5 * 60 * 1000
override var longBreakTime = 15 * 60 * 1000
override var sessionLength = 4
}

View File

@@ -15,6 +15,7 @@ import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.material3.NavigationItemIconPosition
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ShortNavigationBar
import androidx.compose.material3.ShortNavigationBarArrangement
import androidx.compose.material3.ShortNavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
@@ -44,7 +45,7 @@ import org.nsh07.pomodoro.MainActivity.Companion.screens
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.StatsScreen
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
import org.nsh07.pomodoro.ui.viewModel.TimerViewModel
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
@@ -52,7 +53,7 @@ fun AppScreen(
modifier: Modifier = Modifier,
viewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory)
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val uiState by viewModel.timerState.collectAsStateWithLifecycle()
val remainingTime by viewModel.time.collectAsStateWithLifecycle()
val progress by rememberUpdatedState((uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime)
@@ -78,7 +79,16 @@ fun AppScreen(
Scaffold(
bottomBar = {
ShortNavigationBar {
val wide = remember {
windowSizeClass.isWidthAtLeastBreakpoint(
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
)
}
ShortNavigationBar(
arrangement =
if (wide) ShortNavigationBarArrangement.Centered
else ShortNavigationBarArrangement.EqualWeight
) {
screens.forEach {
val selected = backStack.last() == it.route
ShortNavigationBarItem(
@@ -98,10 +108,7 @@ fun AppScreen(
}
},
iconPosition =
if (windowSizeClass.isWidthAtLeastBreakpoint(
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
)
) NavigationItemIconPosition.Start
if (wide) NavigationItemIconPosition.Start
else NavigationItemIconPosition.Top,
label = { Text(it.label) }
)
@@ -134,7 +141,7 @@ fun AppScreen(
entryProvider = entryProvider {
entry<Screen.Timer> {
TimerScreen(
uiState = uiState,
timerState = uiState,
showBrandTitle = showBrandTitle,
progress = { progress },
resetTimer = viewModel::resetTimer,

View File

@@ -25,6 +25,7 @@ import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
@@ -43,10 +44,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTitle
import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.ui.viewModel.SettingsViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreenRoot(
modifier: Modifier = Modifier,
@@ -62,10 +64,20 @@ fun SettingsScreenRoot(
viewModel.longBreakTimeTextFieldState
}
val sessionsSliderState = rememberSaveable(
saver = SliderState.Saver(
viewModel.sessionsSliderState.onValueChangeFinished,
viewModel.sessionsSliderState.valueRange
)
) {
viewModel.sessionsSliderState
}
SettingsScreen(
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
modifier = modifier
)
}
@@ -76,10 +88,10 @@ private fun SettingsScreen(
focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState,
modifier: Modifier = Modifier
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val sessionsSliderState = rememberSliderState(value = 3f, steps = 3, valueRange = 1f..5f)
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
TopAppBar(
@@ -184,11 +196,11 @@ private fun SettingsScreen(
)
},
headlineContent = {
Text("Sessions")
Text("Session length")
},
supportingContent = {
Column {
Text("${sessionsSliderState.value.toInt()} sessions before a long break")
Text("Focus intervals in one session: ${sessionsSliderState.value.toInt()}")
Slider(
state = sessionsSliderState,
modifier = Modifier.padding(vertical = 4.dp)
@@ -202,6 +214,7 @@ private fun SettingsScreen(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview(
showSystemUi = true,
device = Devices.PIXEL_9_PRO
@@ -213,6 +226,7 @@ fun SettingsScreenPreview() {
focusTimeInputFieldState = rememberTextFieldState((25 * 60 * 1000).toString()),
shortBreakTimeInputFieldState = rememberTextFieldState((5 * 60 * 1000).toString()),
longBreakTimeInputFieldState = rememberTextFieldState((15 * 60 * 1000).toString()),
sessionsSliderState = rememberSliderState(value = 3f, steps = 3, valueRange = 1f..5f),
modifier = Modifier.fillMaxSize()
)
}

View File

@@ -1,6 +1,8 @@
package org.nsh07.pomodoro.ui.viewModel
package org.nsh07.pomodoro.ui.settingsScreen.viewModel
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SliderState
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@@ -8,6 +10,7 @@ import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.AP
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
@@ -15,7 +18,7 @@ import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.data.AppPreferenceRepository
import org.nsh07.pomodoro.data.TimerRepository
@OptIn(FlowPreview::class)
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
class SettingsViewModel(
private val preferenceRepository: AppPreferenceRepository,
private val timerRepository: TimerRepository
@@ -27,8 +30,15 @@ class SettingsViewModel(
val longBreakTimeTextFieldState =
TextFieldState((timerRepository.longBreakTime / 60000).toString())
val sessionsSliderState = SliderState(
value = timerRepository.sessionLength.toFloat(),
steps = 4,
valueRange = 1f..6f,
onValueChangeFinished = ::updateSessionLength
)
init {
viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
snapshotFlow { focusTimeTextFieldState.text }
.debounce(500)
.collect {
@@ -39,6 +49,8 @@ class SettingsViewModel(
)
}
}
}
viewModelScope.launch(Dispatchers.IO) {
snapshotFlow { shortBreakTimeTextFieldState.text }
.debounce(500)
.collect {
@@ -49,6 +61,8 @@ class SettingsViewModel(
)
}
}
}
viewModelScope.launch(Dispatchers.IO) {
snapshotFlow { longBreakTimeTextFieldState.text }
.debounce(500)
.collect {
@@ -62,6 +76,15 @@ class SettingsViewModel(
}
}
private fun updateSessionLength() {
viewModelScope.launch {
timerRepository.sessionLength = preferenceRepository.saveIntPreference(
"session_length",
sessionsSliderState.value.toInt()
)
}
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {

View File

@@ -53,13 +53,13 @@ import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTitle
import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.ui.viewModel.TimerMode
import org.nsh07.pomodoro.ui.viewModel.UiState
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun TimerScreen(
uiState: UiState,
timerState: TimerState,
showBrandTitle: Boolean,
progress: () -> Float,
resetTimer: () -> Unit,
@@ -70,17 +70,17 @@ fun TimerScreen(
val motionScheme = motionScheme
val color by animateColorAsState(
if (uiState.timerMode == TimerMode.FOCUS) colorScheme.primary
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary
else colorScheme.tertiary,
animationSpec = motionScheme.slowEffectsSpec()
)
val onColor by animateColorAsState(
if (uiState.timerMode == TimerMode.FOCUS) colorScheme.onPrimary
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.onPrimary
else colorScheme.onTertiary,
animationSpec = motionScheme.slowEffectsSpec()
)
val colorContainer by animateColorAsState(
if (uiState.timerMode == TimerMode.FOCUS) colorScheme.secondaryContainer
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.secondaryContainer
else colorScheme.tertiaryContainer,
animationSpec = motionScheme.slowEffectsSpec()
)
@@ -89,7 +89,7 @@ fun TimerScreen(
TopAppBar(
title = {
AnimatedContent(
if (!showBrandTitle) uiState.timerMode else TimerMode.BRAND,
if (!showBrandTitle) timerState.timerMode else TimerMode.BRAND,
transitionSpec = {
slideInVertically(
animationSpec = motionScheme.slowSpatialSpec(),
@@ -166,7 +166,7 @@ fun TimerScreen(
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(contentAlignment = Alignment.Center) {
if (uiState.timerMode == TimerMode.FOCUS) {
if (timerState.timerMode == TimerMode.FOCUS) {
CircularProgressIndicator(
progress = progress,
modifier = Modifier
@@ -204,7 +204,7 @@ fun TimerScreen(
)
}
Text(
text = uiState.timeStr,
text = timerState.timeStr,
style = TextStyle(
fontFamily = openRundeClock,
fontWeight = FontWeight.Bold,
@@ -246,7 +246,7 @@ fun TimerScreen(
{
FilledIconToggleButton(
onCheckedChange = { toggleTimer() },
checked = uiState.timerRunning,
checked = timerState.timerRunning,
colors = IconButtonDefaults.filledIconToggleButtonColors(
checkedContainerColor = color,
checkedContentColor = onColor
@@ -257,7 +257,7 @@ fun TimerScreen(
.size(width = 128.dp, height = 96.dp)
.animateWidth(interactionSources[0])
) {
if (uiState.timerRunning) {
if (timerState.timerRunning) {
Icon(
painterResource(R.drawable.pause_large),
contentDescription = "Pause",
@@ -275,7 +275,7 @@ fun TimerScreen(
{ state ->
DropdownMenuItem(
leadingIcon = {
if (uiState.timerRunning) {
if (timerState.timerRunning) {
Icon(
painterResource(R.drawable.pause),
contentDescription = "Pause"
@@ -287,7 +287,7 @@ fun TimerScreen(
)
}
},
text = { Text(if (uiState.timerRunning) "Pause" else "Play") },
text = { Text(if (timerState.timerRunning) "Pause" else "Play") },
onClick = {
toggleTimer()
state.dismiss()
@@ -377,17 +377,17 @@ fun TimerScreen(
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Up next", style = typography.titleSmall)
Text(
uiState.nextTimeStr,
timerState.nextTimeStr,
style = TextStyle(
fontFamily = openRundeClock,
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
lineHeight = 28.sp,
color = if (uiState.nextTimerMode == TimerMode.FOCUS) colorScheme.primary else colorScheme.tertiary
color = if (timerState.nextTimerMode == TimerMode.FOCUS) colorScheme.primary else colorScheme.tertiary
)
)
Text(
when (uiState.nextTimerMode) {
when (timerState.nextTimerMode) {
TimerMode.FOCUS -> "Focus"
TimerMode.SHORT_BREAK -> "Short Break"
else -> "Long Break"
@@ -405,12 +405,12 @@ fun TimerScreen(
)
@Composable
fun TimerScreenPreview() {
val uiState = UiState(
val timerState = TimerState(
timeStr = "03:34", nextTimeStr = "5:00", timerMode = TimerMode.FOCUS, timerRunning = true
)
TomatoTheme {
TimerScreen(
uiState,
timerState,
false,
{ 0.3f },
{},

View File

@@ -1,6 +1,6 @@
package org.nsh07.pomodoro.ui.viewModel
package org.nsh07.pomodoro.ui.timerScreen.viewModel
data class UiState(
data class TimerState(
val timerMode: TimerMode = TimerMode.FOCUS,
val timeStr: String = "25:00",
val totalTime: Int = 25 * 60,

View File

@@ -1,4 +1,4 @@
package org.nsh07.pomodoro.ui.viewModel
package org.nsh07.pomodoro.ui.timerScreen.viewModel
import android.os.SystemClock
import androidx.lifecycle.ViewModel
@@ -34,19 +34,21 @@ class TimerViewModel(
?: preferenceRepository.saveIntPreference("short_break_time", timerRepository.shortBreakTime)
timerRepository.longBreakTime = preferenceRepository.getIntPreference("long_break_time")
?: preferenceRepository.saveIntPreference("long_break_time", timerRepository.longBreakTime)
timerRepository.sessionLength = preferenceRepository.getIntPreference("session_length")
?: preferenceRepository.saveIntPreference("session_length", timerRepository.sessionLength)
resetTimer()
}
}
private val _uiState = MutableStateFlow(
UiState(
private val _timerState = MutableStateFlow(
TimerState(
totalTime = timerRepository.focusTime,
timeStr = millisecondsToStr(timerRepository.focusTime),
nextTimeStr = millisecondsToStr(timerRepository.shortBreakTime)
)
)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
val timerState: StateFlow<TimerState> = _timerState.asStateFlow()
var timerJob: Job? = null
private val _time = MutableStateFlow(timerRepository.focusTime)
@@ -64,7 +66,7 @@ class TimerViewModel(
pauseTime = 0L
pauseDuration = 0L
_uiState.update { currentState ->
_timerState.update { currentState ->
currentState.copy(
timerMode = TimerMode.FOCUS,
timeStr = millisecondsToStr(time.value),
@@ -79,11 +81,11 @@ class TimerViewModel(
startTime = 0L
pauseTime = 0L
pauseDuration = 0L
cycles = (cycles + 1) % 8
cycles = (cycles + 1) % (timerRepository.sessionLength * 2)
if (cycles % 2 == 0) {
_time.update { timerRepository.focusTime }
_uiState.update { currentState ->
_timerState.update { currentState ->
currentState.copy(
timerMode = TimerMode.FOCUS,
timeStr = millisecondsToStr(time.value),
@@ -97,10 +99,10 @@ class TimerViewModel(
)
}
} else {
val long = cycles == 7
val long = cycles == (timerRepository.sessionLength * 2) - 1
_time.update { if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime }
_uiState.update { currentState ->
_timerState.update { currentState ->
currentState.copy(
timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
timeStr = millisecondsToStr(time.value),
@@ -113,23 +115,23 @@ class TimerViewModel(
}
fun toggleTimer() {
if (uiState.value.timerRunning) {
_uiState.update { currentState ->
if (timerState.value.timerRunning) {
_timerState.update { currentState ->
currentState.copy(timerRunning = false)
}
timerJob?.cancel()
pauseTime = SystemClock.elapsedRealtime()
} else {
_uiState.update { it.copy(timerRunning = true) }
_timerState.update { it.copy(timerRunning = true) }
if (pauseTime != 0L) pauseDuration += SystemClock.elapsedRealtime() - pauseTime
timerJob = viewModelScope.launch {
while (true) {
if (!uiState.value.timerRunning) break
if (!timerState.value.timerRunning) break
if (startTime == 0L) startTime = SystemClock.elapsedRealtime()
_time.update {
when (uiState.value.timerMode) {
when (timerState.value.timerMode) {
TimerMode.FOCUS ->
timerRepository.focusTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt()
@@ -144,12 +146,12 @@ class TimerViewModel(
if (time.value < 0) {
skipTimer()
_uiState.update { currentState ->
_timerState.update { currentState ->
currentState.copy(timerRunning = false)
}
timerJob?.cancel()
} else {
_uiState.update { currentState ->
_timerState.update { currentState ->
currentState.copy(
timeStr = millisecondsToStr(time.value)
)

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,680q17,0 28.5,-11.5T520,640v-160q0,-17 -11.5,-28.5T480,440q-17,0 -28.5,11.5T440,480v160q0,17 11.5,28.5T480,680ZM480,360q17,0 28.5,-11.5T520,320q0,-17 -11.5,-28.5T480,280q-17,0 -28.5,11.5T440,320q0,17 11.5,28.5T480,360ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880Z" />
</vector>