Implement changing timer durations

This commit is contained in:
Nishant Mishra
2025-07-06 19:10:21 +05:30
parent 67b1fa2ef9
commit 6e702a6010
4 changed files with 203 additions and 145 deletions

View File

@@ -55,6 +55,10 @@ fun AppScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val remainingTime by viewModel.time.collectAsStateWithLifecycle()
val focusTimeInputFieldState = viewModel.focusTimeTextFieldState
val shortBreakTimeInputFieldState = viewModel.shortBreakTimeTextFieldState
val longBreakTimeInputFieldState = viewModel.longBreakTimeTextFieldState
val progress by rememberUpdatedState((uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime)
var showBrandTitle by remember { mutableStateOf(true) }
@@ -150,12 +154,11 @@ fun AppScreen(
entry<Screen.Settings> {
SettingsScreen(
25 * 60 * 1000,
5 * 60 * 1000,
15 * 60 * 1000,
{},
{},
{},
focusTimeInputFieldState,
shortBreakTimeInputFieldState,
longBreakTimeInputFieldState,
viewModel::startTimeFieldsCollection,
viewModel::stopTimeFieldsCollection,
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),

View File

@@ -0,0 +1,70 @@
package org.nsh07.pomodoro.ui.settingsScreen
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MinuteInputField(
state: TextFieldState,
shape: Shape,
modifier: Modifier = Modifier,
imeAction: ImeAction = ImeAction.Next
) {
BasicTextField(
state = state,
lineLimits = TextFieldLineLimits.SingleLine,
inputTransformation = MinutesInputTransformation,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.NumberPassword,
imeAction = imeAction
),
textStyle = TextStyle(
fontFamily = openRundeClock,
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
letterSpacing = (-2).sp,
color = colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
),
cursorBrush = SolidColor(colorScheme.onSurface),
decorator = { innerTextField ->
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.size(112.dp, 100.dp)
.background(
animateColorAsState(
if (state.text.isNotEmpty())
colorScheme.surfaceContainer
else colorScheme.errorContainer,
motionScheme.defaultEffectsSpec()
).value,
shape
)
) { innerTextField() }
}
)
}

View File

@@ -1,73 +1,58 @@
package org.nsh07.pomodoro.ui.settingsScreen
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTitle
import org.nsh07.pomodoro.ui.theme.TomatoTheme
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SettingsScreen(
focusTime: Int,
shortBreakTime: Int,
longBreakTime: Int,
updateFocusTime: (Int) -> Unit,
updateShortBreakTime: (Int) -> Unit,
updateLongBreakTime: (Int) -> Unit,
focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState,
startCollectingTimeFields: () -> Unit,
stopCollectingTimeFields: () -> Unit,
modifier: Modifier = Modifier
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
DisposableEffect(Unit) {
startCollectingTimeFields()
onDispose {
stopCollectingTimeFields()
}
}
val focusTimeInputFieldState = rememberTextFieldState(
(focusTime / 60000).toString().padStart(2, '0')
)
val shortBreakTimeInputFieldState = rememberTextFieldState(
(shortBreakTime / 60000).toString().padStart(2, '0')
)
val longBreakTimeInputFieldState = rememberTextFieldState(
(longBreakTime / 60000).toString().padStart(2, '0')
)
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
TopAppBar(
@@ -109,41 +94,16 @@ fun SettingsScreen(
.horizontalScroll(rememberScrollState())
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(112.dp, 100.dp)
.background(
animateColorAsState(
if (focusTimeInputFieldState.text.isNotEmpty())
colorScheme.surfaceContainer
else colorScheme.errorContainer,
motionScheme.defaultEffectsSpec()
).value,
RoundedCornerShape(
topStart = 16.dp,
bottomStart = 16.dp,
topEnd = 4.dp,
bottomEnd = 4.dp
)
)
) {
BasicTextField(
state = focusTimeInputFieldState,
lineLimits = TextFieldLineLimits.SingleLine,
inputTransformation = MinutesInputTransformation,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
textStyle = TextStyle(
fontFamily = openRundeClock,
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
letterSpacing = (-2).sp,
color = colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
),
cursorBrush = SolidColor(colorScheme.onSurface)
)
}
MinuteInputField(
state = focusTimeInputFieldState,
shape = RoundedCornerShape(
topStart = 16.dp,
bottomStart = 16.dp,
topEnd = 4.dp,
bottomEnd = 4.dp
),
imeAction = ImeAction.Next
)
Text(
"Focus",
style = typography.titleSmallEmphasized,
@@ -153,36 +113,11 @@ fun SettingsScreen(
}
Spacer(Modifier.width(2.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(112.dp, 100.dp)
.background(
animateColorAsState(
if (shortBreakTimeInputFieldState.text.isNotEmpty())
colorScheme.surfaceContainer
else colorScheme.errorContainer,
motionScheme.defaultEffectsSpec()
).value,
RoundedCornerShape(4.dp)
)
) {
BasicTextField(
state = shortBreakTimeInputFieldState,
lineLimits = TextFieldLineLimits.SingleLine,
inputTransformation = MinutesInputTransformation,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
textStyle = TextStyle(
fontFamily = openRundeClock,
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
letterSpacing = (-2).sp,
color = colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
),
cursorBrush = SolidColor(colorScheme.onSurface)
)
}
MinuteInputField(
state = shortBreakTimeInputFieldState,
shape = RoundedCornerShape(4.dp),
imeAction = ImeAction.Next
)
Text(
"Short break",
style = typography.titleSmallEmphasized,
@@ -192,41 +127,16 @@ fun SettingsScreen(
}
Spacer(Modifier.width(2.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(112.dp, 100.dp)
.background(
animateColorAsState(
if (longBreakTimeInputFieldState.text.isNotEmpty())
colorScheme.surfaceContainer
else colorScheme.errorContainer,
motionScheme.defaultEffectsSpec()
).value,
RoundedCornerShape(
topStart = 4.dp,
bottomStart = 4.dp,
topEnd = 16.dp,
bottomEnd = 16.dp
)
)
) {
BasicTextField(
state = longBreakTimeInputFieldState,
lineLimits = TextFieldLineLimits.SingleLine,
inputTransformation = MinutesInputTransformation,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
textStyle = TextStyle(
fontFamily = openRundeClock,
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
letterSpacing = (-2).sp,
color = colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
),
cursorBrush = SolidColor(colorScheme.onSurface)
)
}
MinuteInputField(
state = longBreakTimeInputFieldState,
shape = RoundedCornerShape(
topStart = 4.dp,
bottomStart = 4.dp,
topEnd = 16.dp,
bottomEnd = 16.dp
),
imeAction = ImeAction.Done
)
Text(
"Long break",
style = typography.titleSmallEmphasized,
@@ -248,12 +158,11 @@ fun SettingsScreen(
fun SettingsScreenPreview() {
TomatoTheme {
SettingsScreen(
focusTime = 25 * 60 * 1000,
shortBreakTime = 5 * 60 * 1000,
longBreakTime = 15 * 60 * 1000,
updateFocusTime = {},
updateShortBreakTime = {},
updateLongBreakTime = {},
focusTimeInputFieldState = rememberTextFieldState((25 * 60 * 1000).toString()),
shortBreakTimeInputFieldState = rememberTextFieldState((5 * 60 * 1000).toString()),
longBreakTimeInputFieldState = rememberTextFieldState((15 * 60 * 1000).toString()),
startCollectingTimeFields = {},
stopCollectingTimeFields = {},
modifier = Modifier.fillMaxSize()
)
}

View File

@@ -1,6 +1,9 @@
package org.nsh07.pomodoro.ui.viewModel
import android.os.SystemClock
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.delete
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
@@ -8,11 +11,13 @@ 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.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication
@@ -20,6 +25,7 @@ import org.nsh07.pomodoro.data.AppPreferenceRepository
import java.util.Locale
import kotlin.math.ceil
@OptIn(FlowPreview::class)
class UiViewModel(
private val preferenceRepository: AppPreferenceRepository
) : ViewModel() {
@@ -27,8 +33,12 @@ class UiViewModel(
var shortBreakTime = 5 * 60 * 1000
var longBreakTime = 15 * 60 * 1000
val focusTimeTextFieldState = TextFieldState((focusTime / 60000).toString())
val shortBreakTimeTextFieldState = TextFieldState((shortBreakTime / 60000).toString())
val longBreakTimeTextFieldState = TextFieldState((longBreakTime / 60000).toString())
init {
updateTimerConstants()
updateTimerConstants(true)
}
private val _uiState = MutableStateFlow(
@@ -40,6 +50,9 @@ class UiViewModel(
)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
var timerJob: Job? = null
var focusTimeJob: Job? = null
var shortBreakTimeJob: Job? = null
var longBreakTimeJob: Job? = null
private val _time = MutableStateFlow(focusTime)
val time: StateFlow<Int> = _time.asStateFlow()
@@ -184,7 +197,7 @@ class UiViewModel(
}
}
fun updateTimerConstants() {
fun updateTimerConstants(updateTextFields: Boolean = false, restart: Boolean = true) {
viewModelScope.launch(Dispatchers.IO) {
focusTime = preferenceRepository.getIntPreference("focus_time")
?: preferenceRepository.saveIntPreference("focus_time", focusTime)
@@ -193,10 +206,73 @@ class UiViewModel(
longBreakTime = preferenceRepository.getIntPreference("long_break_time")
?: preferenceRepository.saveIntPreference("long_break_time", longBreakTime)
resetTimer()
if (updateTextFields) {
focusTimeTextFieldState.edit {
delete(0,length)
append((focusTime / 60000).toString())
}
shortBreakTimeTextFieldState.edit {
delete(0, length)
append((shortBreakTime/ 60000).toString())
}
longBreakTimeTextFieldState.edit {
delete(0, length)
append((longBreakTime / 60000).toString())
}
}
if (restart) resetTimer()
}
}
fun startTimeFieldsCollection() {
focusTimeJob = viewModelScope.launch {
snapshotFlow { focusTimeTextFieldState.text }
.debounce(500)
.collect {
if (it.isNotEmpty()) {
preferenceRepository.saveIntPreference(
"focus_time",
it.toString().toInt() * 60 * 1000
)
updateTimerConstants(restart = false)
}
}
}
shortBreakTimeJob = viewModelScope.launch {
snapshotFlow { shortBreakTimeTextFieldState.text }
.debounce(500)
.collect {
if (it.isNotEmpty()) {
preferenceRepository.saveIntPreference(
"short_break_time",
it.toString().toInt() * 60 * 1000
)
updateTimerConstants(restart = false)
}
}
}
longBreakTimeJob = viewModelScope.launch {
snapshotFlow { longBreakTimeTextFieldState.text }
.debounce(500)
.collect {
if (it.isNotEmpty()) {
preferenceRepository.saveIntPreference(
"long_break_time",
it.toString().toInt() * 60 * 1000
)
updateTimerConstants(restart = false)
}
}
}
}
fun stopTimeFieldsCollection() {
focusTimeJob?.cancel()
shortBreakTimeJob?.cancel()
longBreakTimeJob?.cancel()
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {