diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt index fcb4ade..583c9b5 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -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 { 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), diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/MinuteInputField.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/MinuteInputField.kt new file mode 100644 index 0000000..9e497bf --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/MinuteInputField.kt @@ -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() } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index 8ef9db8..9db39ce 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -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() ) } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiViewModel.kt index 075c072..7ab6d64 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiViewModel.kt @@ -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.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 = _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 {