diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index 5e919b9..a753952 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -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() { diff --git a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt index 9bd9194..ba7b33d 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt @@ -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 } \ No newline at end of file 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 e3ff33a..883c0a7 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -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 { TimerScreen( - uiState = uiState, + timerState = uiState, showBrandTitle = showBrandTitle, progress = { progress }, resetTimer = viewModel::resetTimer, 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 ce9553c..816f9b5 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 @@ -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() ) } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt similarity index 78% rename from app/src/main/java/org/nsh07/pomodoro/ui/viewModel/SettingsViewModel.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index 4fc0570..52c9407 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -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 { diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt index d2846e5..0153c8c 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt @@ -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 }, {}, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiState.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerState.kt similarity index 81% rename from app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiState.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerState.kt index f1a330f..e100237 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiState.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerState.kt @@ -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, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt similarity index 85% rename from app/src/main/java/org/nsh07/pomodoro/ui/viewModel/TimerViewModel.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt index 9088491..b266105 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/TimerViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt @@ -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.asStateFlow() + val timerState: StateFlow = _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) ) diff --git a/app/src/main/res/drawable/info.xml b/app/src/main/res/drawable/info.xml new file mode 100644 index 0000000..bd589bd --- /dev/null +++ b/app/src/main/res/drawable/info.xml @@ -0,0 +1,10 @@ + + +