diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index b2c68bd..5e919b9 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -9,11 +9,11 @@ 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.UiViewModel +import org.nsh07.pomodoro.ui.viewModel.TimerViewModel class MainActivity : ComponentActivity() { - private val viewModel: UiViewModel by viewModels(factoryProducer = { UiViewModel.Factory }) + private val viewModel: TimerViewModel by viewModels(factoryProducer = { TimerViewModel.Factory }) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt index 2101cb2..c8039cf 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt @@ -4,6 +4,7 @@ import android.content.Context interface AppContainer { val appPreferencesRepository: AppPreferenceRepository + val appTimerRepository: AppTimerRepository } class DefaultAppContainer(context: Context) : AppContainer { @@ -14,4 +15,8 @@ class DefaultAppContainer(context: Context) : AppContainer { ) } + override val appTimerRepository: AppTimerRepository by lazy { + AppTimerRepository() + } + } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt new file mode 100644 index 0000000..9bd9194 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt @@ -0,0 +1,13 @@ +package org.nsh07.pomodoro.data + +interface TimerRepository { + var focusTime: Int + var shortBreakTime: Int + var longBreakTime: Int +} + +class AppTimerRepository : TimerRepository { + override var focusTime = 25 * 60 * 1000 + override var shortBreakTime = 5 * 60 * 1000 + override var longBreakTime = 15 * 60 * 1000 +} \ 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 062ac75..e3ff33a 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.animation.scaleOut import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon @@ -25,7 +24,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -43,30 +41,20 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import org.nsh07.pomodoro.MainActivity.Companion.screens -import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreen +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.UiViewModel +import org.nsh07.pomodoro.ui.viewModel.TimerViewModel @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun AppScreen( modifier: Modifier = Modifier, - viewModel: UiViewModel = viewModel(factory = UiViewModel.Factory) + viewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory) ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val remainingTime by viewModel.time.collectAsStateWithLifecycle() - val focusTimeInputFieldState = rememberSaveable(saver = TextFieldState.Saver) { - viewModel.focusTimeTextFieldState - } - val shortBreakTimeInputFieldState = rememberSaveable(saver = TextFieldState.Saver) { - viewModel.shortBreakTimeTextFieldState - } - val longBreakTimeInputFieldState = rememberSaveable(saver = TextFieldState.Saver) { - viewModel.longBreakTimeTextFieldState - } - val progress by rememberUpdatedState((uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime) var showBrandTitle by remember { mutableStateOf(true) } @@ -149,7 +137,7 @@ fun AppScreen( uiState = uiState, showBrandTitle = showBrandTitle, progress = { progress }, - resetTimer = viewModel::updateTimerConstants, + resetTimer = viewModel::resetTimer, skipTimer = viewModel::skipTimer, toggleTimer = viewModel::toggleTimer, modifier = modifier.padding( @@ -161,12 +149,7 @@ fun AppScreen( } entry { - SettingsScreen( - focusTimeInputFieldState, - shortBreakTimeInputFieldState, - longBreakTimeInputFieldState, - viewModel::startTimeFieldsCollection, - viewModel::stopTimeFieldsCollection, + SettingsScreenRoot( modifier = modifier.padding( start = contentPadding.calculateStartPadding(layoutDirection), end = contentPadding.calculateEndPadding(layoutDirection), 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 beea291..ce9553c 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 @@ -30,7 +30,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberSliderState import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -41,27 +41,43 @@ 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 androidx.lifecycle.viewmodel.compose.viewModel import org.nsh07.pomodoro.R import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTitle import org.nsh07.pomodoro.ui.theme.TomatoTheme +import org.nsh07.pomodoro.ui.viewModel.SettingsViewModel + +@Composable +fun SettingsScreenRoot( + modifier: Modifier = Modifier, + viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory) +) { + val focusTimeInputFieldState = rememberSaveable(saver = TextFieldState.Saver) { + viewModel.focusTimeTextFieldState + } + val shortBreakTimeInputFieldState = rememberSaveable(saver = TextFieldState.Saver) { + viewModel.shortBreakTimeTextFieldState + } + val longBreakTimeInputFieldState = rememberSaveable(saver = TextFieldState.Saver) { + viewModel.longBreakTimeTextFieldState + } + + SettingsScreen( + focusTimeInputFieldState = focusTimeInputFieldState, + shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, + longBreakTimeInputFieldState = longBreakTimeInputFieldState, + modifier = modifier + ) +} @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable -fun SettingsScreen( +private fun SettingsScreen( focusTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState, - startCollectingTimeFields: () -> Unit, - stopCollectingTimeFields: () -> Unit, modifier: Modifier = Modifier ) { - DisposableEffect(Unit) { - startCollectingTimeFields() - onDispose { - stopCollectingTimeFields() - } - } - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val sessionsSliderState = rememberSliderState(value = 3f, steps = 3, valueRange = 1f..5f) @@ -197,8 +213,6 @@ fun SettingsScreenPreview() { 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/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/SettingsViewModel.kt new file mode 100644 index 0000000..4fc0570 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/SettingsViewModel.kt @@ -0,0 +1,79 @@ +package org.nsh07.pomodoro.ui.viewModel + +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.runtime.snapshotFlow +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.launch +import org.nsh07.pomodoro.TomatoApplication +import org.nsh07.pomodoro.data.AppPreferenceRepository +import org.nsh07.pomodoro.data.TimerRepository + +@OptIn(FlowPreview::class) +class SettingsViewModel( + private val preferenceRepository: AppPreferenceRepository, + private val timerRepository: TimerRepository +) : ViewModel() { + val focusTimeTextFieldState = + TextFieldState((timerRepository.focusTime / 60000).toString()) + val shortBreakTimeTextFieldState = + TextFieldState((timerRepository.shortBreakTime / 60000).toString()) + val longBreakTimeTextFieldState = + TextFieldState((timerRepository.longBreakTime / 60000).toString()) + + init { + viewModelScope.launch { + snapshotFlow { focusTimeTextFieldState.text } + .debounce(500) + .collect { + if (it.isNotEmpty()) { + timerRepository.focusTime = preferenceRepository.saveIntPreference( + "focus_time", + it.toString().toInt() * 60 * 1000 + ) + } + } + snapshotFlow { shortBreakTimeTextFieldState.text } + .debounce(500) + .collect { + if (it.isNotEmpty()) { + timerRepository.shortBreakTime = preferenceRepository.saveIntPreference( + "short_break_time", + it.toString().toInt() * 60 * 1000 + ) + } + } + snapshotFlow { longBreakTimeTextFieldState.text } + .debounce(500) + .collect { + if (it.isNotEmpty()) { + timerRepository.longBreakTime = preferenceRepository.saveIntPreference( + "long_break_time", + it.toString().toInt() * 60 * 1000 + ) + } + } + } + } + + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val application = (this[APPLICATION_KEY] as TomatoApplication) + val appPreferenceRepository = application.container.appPreferencesRepository + val appTimerRepository = application.container.appTimerRepository + + SettingsViewModel( + preferenceRepository = appPreferenceRepository, + timerRepository = appTimerRepository + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/TimerViewModel.kt similarity index 51% rename from app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiViewModel.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/viewModel/TimerViewModel.kt index 5574d86..9088491 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/viewModel/TimerViewModel.kt @@ -1,9 +1,6 @@ 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 @@ -17,44 +14,42 @@ 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 import org.nsh07.pomodoro.data.AppPreferenceRepository -import java.util.Locale -import kotlin.math.ceil +import org.nsh07.pomodoro.data.TimerRepository +import org.nsh07.pomodoro.utils.millisecondsToStr @OptIn(FlowPreview::class) -class UiViewModel( - private val preferenceRepository: AppPreferenceRepository +class TimerViewModel( + private val preferenceRepository: AppPreferenceRepository, + private val timerRepository: TimerRepository ) : ViewModel() { - var focusTime = 25 * 60 * 1000 - 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(true) + viewModelScope.launch(Dispatchers.IO) { + timerRepository.focusTime = preferenceRepository.getIntPreference("focus_time") + ?: preferenceRepository.saveIntPreference("focus_time", timerRepository.focusTime) + timerRepository.shortBreakTime = preferenceRepository.getIntPreference("short_break_time") + ?: preferenceRepository.saveIntPreference("short_break_time", timerRepository.shortBreakTime) + timerRepository.longBreakTime = preferenceRepository.getIntPreference("long_break_time") + ?: preferenceRepository.saveIntPreference("long_break_time", timerRepository.longBreakTime) + + resetTimer() + } } private val _uiState = MutableStateFlow( UiState( - totalTime = focusTime, - timeStr = millisecondsToStr(focusTime), - nextTimeStr = millisecondsToStr(shortBreakTime) + totalTime = timerRepository.focusTime, + timeStr = millisecondsToStr(timerRepository.focusTime), + nextTimeStr = millisecondsToStr(timerRepository.shortBreakTime) ) ) 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) + private val _time = MutableStateFlow(timerRepository.focusTime) val time: StateFlow = _time.asStateFlow() private var cycles = 0 @@ -63,7 +58,7 @@ class UiViewModel( private var pauseDuration = 0L fun resetTimer() { - _time.update { focusTime } + _time.update { timerRepository.focusTime } cycles = 0 startTime = 0L pauseTime = 0L @@ -75,7 +70,7 @@ class UiViewModel( timeStr = millisecondsToStr(time.value), totalTime = time.value, nextTimerMode = TimerMode.SHORT_BREAK, - nextTimeStr = millisecondsToStr(shortBreakTime) + nextTimeStr = millisecondsToStr(timerRepository.shortBreakTime) ) } } @@ -87,7 +82,7 @@ class UiViewModel( cycles = (cycles + 1) % 8 if (cycles % 2 == 0) { - _time.update { focusTime } + _time.update { timerRepository.focusTime } _uiState.update { currentState -> currentState.copy( timerMode = TimerMode.FOCUS, @@ -95,15 +90,15 @@ class UiViewModel( totalTime = time.value, nextTimerMode = if (cycles == 6) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK, nextTimeStr = if (cycles == 6) millisecondsToStr( - longBreakTime + timerRepository.longBreakTime ) else millisecondsToStr( - shortBreakTime + timerRepository.shortBreakTime ) ) } } else { val long = cycles == 7 - _time.update { if (long) longBreakTime else shortBreakTime } + _time.update { if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime } _uiState.update { currentState -> currentState.copy( @@ -111,7 +106,7 @@ class UiViewModel( timeStr = millisecondsToStr(time.value), totalTime = time.value, nextTimerMode = TimerMode.FOCUS, - nextTimeStr = millisecondsToStr(focusTime) + nextTimeStr = millisecondsToStr(timerRepository.focusTime) ) } } @@ -136,13 +131,13 @@ class UiViewModel( _time.update { when (uiState.value.timerMode) { TimerMode.FOCUS -> - focusTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt() + timerRepository.focusTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt() TimerMode.SHORT_BREAK -> - shortBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt() + timerRepository.shortBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt() else -> - longBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt() + timerRepository.longBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt() } } @@ -167,92 +162,18 @@ class UiViewModel( } } - fun updateTimerConstants(updateTextFields: Boolean = false) { - viewModelScope.launch(Dispatchers.IO) { - focusTime = preferenceRepository.getIntPreference("focus_time") - ?: preferenceRepository.saveIntPreference("focus_time", focusTime) - shortBreakTime = preferenceRepository.getIntPreference("short_break_time") - ?: preferenceRepository.saveIntPreference("short_break_time", shortBreakTime) - longBreakTime = preferenceRepository.getIntPreference("long_break_time") - ?: preferenceRepository.saveIntPreference("long_break_time", longBreakTime) - - 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()) - } - } - - resetTimer() - } - } - - fun startTimeFieldsCollection() { - focusTimeJob = viewModelScope.launch { - snapshotFlow { focusTimeTextFieldState.text } - .debounce(500) - .collect { - if (it.isNotEmpty()) { - focusTime = preferenceRepository.saveIntPreference( - "focus_time", - it.toString().toInt() * 60 * 1000 - ) - } - } - } - shortBreakTimeJob = viewModelScope.launch { - snapshotFlow { shortBreakTimeTextFieldState.text } - .debounce(500) - .collect { - if (it.isNotEmpty()) { - shortBreakTime = preferenceRepository.saveIntPreference( - "short_break_time", - it.toString().toInt() * 60 * 1000 - ) - } - } - } - longBreakTimeJob = viewModelScope.launch { - snapshotFlow { longBreakTimeTextFieldState.text } - .debounce(500) - .collect { - if (it.isNotEmpty()) { - longBreakTime = preferenceRepository.saveIntPreference( - "long_break_time", - it.toString().toInt() * 60 * 1000 - ) - } - } - } - } - - fun stopTimeFieldsCollection() { - focusTimeJob?.cancel() - shortBreakTimeJob?.cancel() - longBreakTimeJob?.cancel() - } - companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { val application = (this[APPLICATION_KEY] as TomatoApplication) val appPreferenceRepository = application.container.appPreferencesRepository - UiViewModel(preferenceRepository = appPreferenceRepository) + val appTimerRepository = application.container.appTimerRepository + + TimerViewModel( + preferenceRepository = appPreferenceRepository, + timerRepository = appTimerRepository + ) } } } -} - -fun millisecondsToStr(t: Int): String { - val min = (ceil(t / 1000.0).toInt() / 60) - val sec = (ceil(t / 1000.0).toInt() % 60) - return String.format(locale = Locale.getDefault(), "%02d:%02d", min, sec) } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt b/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt new file mode 100644 index 0000000..1bb75c2 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt @@ -0,0 +1,10 @@ +package org.nsh07.pomodoro.utils + +import java.util.Locale +import kotlin.math.ceil + +fun millisecondsToStr(t: Int): String { + val min = (ceil(t / 1000.0).toInt() / 60) + val sec = (ceil(t / 1000.0).toInt() % 60) + return String.format(locale = Locale.getDefault(), "%02d:%02d", min, sec) +} \ No newline at end of file