Implement changing timer durations
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user