refactor: Migrate TimerViewModel from MVVM to MVI
Also fix a bug in the StatDao that prevented app from compiling
This commit is contained in:
@@ -39,7 +39,7 @@ interface StatDao {
|
|||||||
@Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat")
|
@Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat")
|
||||||
fun getAllStatsSummary(): Flow<List<StatSummary>>
|
fun getAllStatsSummary(): Flow<List<StatSummary>>
|
||||||
|
|
||||||
@Query("SELECT AVG(focusTimeQ1), AVG(focusTimeQ2), AVG(focusTimeQ3), AVG(focusTimeQ4) FROM stat")
|
@Query("SELECT AVG(focusTimeQ1) AS focusTimeQ1, AVG(focusTimeQ2) AS focusTimeQ2, AVG(focusTimeQ3) AS focusTimeQ3, AVG(focusTimeQ4) AS focusTimeQ4 FROM stat")
|
||||||
fun getAvgFocusTimes(): Flow<StatFocusTime?>
|
fun getAvgFocusTimes(): Flow<StatFocusTime?>
|
||||||
|
|
||||||
@Query("SELECT EXISTS (SELECT * FROM stat WHERE date = :date)")
|
@Query("SELECT EXISTS (SELECT * FROM stat WHERE date = :date)")
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro.ui
|
package org.nsh07.pomodoro.ui
|
||||||
|
|
||||||
import androidx.compose.animation.ContentTransform
|
import androidx.compose.animation.ContentTransform
|
||||||
@@ -22,10 +29,8 @@ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
@@ -38,9 +43,6 @@ import androidx.navigation3.runtime.entryProvider
|
|||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import androidx.window.core.layout.WindowSizeClass
|
import androidx.window.core.layout.WindowSizeClass
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.nsh07.pomodoro.MainActivity.Companion.screens
|
import org.nsh07.pomodoro.MainActivity.Companion.screens
|
||||||
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
|
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.StatsScreen
|
import org.nsh07.pomodoro.ui.statsScreen.StatsScreen
|
||||||
@@ -57,20 +59,12 @@ fun AppScreen(
|
|||||||
val remainingTime by viewModel.time.collectAsStateWithLifecycle()
|
val remainingTime by viewModel.time.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val progress by rememberUpdatedState((uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime)
|
val progress by rememberUpdatedState((uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime)
|
||||||
var showBrandTitle by remember { mutableStateOf(true) }
|
|
||||||
|
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val motionScheme = motionScheme
|
val motionScheme = motionScheme
|
||||||
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
|
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
delay(1500)
|
|
||||||
showBrandTitle = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(uiState.timerMode) {
|
LaunchedEffect(uiState.timerMode) {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
}
|
}
|
||||||
@@ -142,11 +136,8 @@ fun AppScreen(
|
|||||||
entry<Screen.Timer> {
|
entry<Screen.Timer> {
|
||||||
TimerScreen(
|
TimerScreen(
|
||||||
timerState = uiState,
|
timerState = uiState,
|
||||||
showBrandTitle = showBrandTitle,
|
|
||||||
progress = { progress },
|
progress = { progress },
|
||||||
resetTimer = viewModel::resetTimer,
|
onAction = viewModel::onAction,
|
||||||
skipTimer = viewModel::skipTimer,
|
|
||||||
toggleTimer = viewModel::toggleTimer,
|
|
||||||
modifier = modifier.padding(
|
modifier = modifier.padding(
|
||||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro.ui.timerScreen
|
package org.nsh07.pomodoro.ui.timerScreen
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
@@ -53,6 +60,7 @@ import org.nsh07.pomodoro.R
|
|||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTitle
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTitle
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||||
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
|
||||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
|
||||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
||||||
|
|
||||||
@@ -60,11 +68,8 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
|||||||
@Composable
|
@Composable
|
||||||
fun TimerScreen(
|
fun TimerScreen(
|
||||||
timerState: TimerState,
|
timerState: TimerState,
|
||||||
showBrandTitle: Boolean,
|
|
||||||
progress: () -> Float,
|
progress: () -> Float,
|
||||||
resetTimer: () -> Unit,
|
onAction: (TimerAction) -> Unit,
|
||||||
skipTimer: () -> Unit,
|
|
||||||
toggleTimer: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val motionScheme = motionScheme
|
val motionScheme = motionScheme
|
||||||
@@ -89,7 +94,7 @@ fun TimerScreen(
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
if (!showBrandTitle) timerState.timerMode else TimerMode.BRAND,
|
if (!timerState.showBrandTitle) timerState.timerMode else TimerMode.BRAND,
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
slideInVertically(
|
slideInVertically(
|
||||||
animationSpec = motionScheme.slowSpatialSpec(),
|
animationSpec = motionScheme.slowSpatialSpec(),
|
||||||
@@ -245,7 +250,7 @@ fun TimerScreen(
|
|||||||
customItem(
|
customItem(
|
||||||
{
|
{
|
||||||
FilledIconToggleButton(
|
FilledIconToggleButton(
|
||||||
onCheckedChange = { toggleTimer() },
|
onCheckedChange = { onAction(TimerAction.ToggleTimer) },
|
||||||
checked = timerState.timerRunning,
|
checked = timerState.timerRunning,
|
||||||
colors = IconButtonDefaults.filledIconToggleButtonColors(
|
colors = IconButtonDefaults.filledIconToggleButtonColors(
|
||||||
checkedContainerColor = color,
|
checkedContainerColor = color,
|
||||||
@@ -289,7 +294,7 @@ fun TimerScreen(
|
|||||||
},
|
},
|
||||||
text = { Text(if (timerState.timerRunning) "Pause" else "Play") },
|
text = { Text(if (timerState.timerRunning) "Pause" else "Play") },
|
||||||
onClick = {
|
onClick = {
|
||||||
toggleTimer()
|
onAction(TimerAction.ToggleTimer)
|
||||||
state.dismiss()
|
state.dismiss()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -299,7 +304,7 @@ fun TimerScreen(
|
|||||||
customItem(
|
customItem(
|
||||||
{
|
{
|
||||||
FilledTonalIconButton(
|
FilledTonalIconButton(
|
||||||
onClick = resetTimer,
|
onClick = { onAction(TimerAction.ResetTimer) },
|
||||||
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
||||||
containerColor = colorContainer
|
containerColor = colorContainer
|
||||||
),
|
),
|
||||||
@@ -326,7 +331,7 @@ fun TimerScreen(
|
|||||||
},
|
},
|
||||||
text = { Text("Restart") },
|
text = { Text("Restart") },
|
||||||
onClick = {
|
onClick = {
|
||||||
resetTimer()
|
onAction(TimerAction.ResetTimer)
|
||||||
state.dismiss()
|
state.dismiss()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -336,7 +341,7 @@ fun TimerScreen(
|
|||||||
customItem(
|
customItem(
|
||||||
{
|
{
|
||||||
FilledTonalIconButton(
|
FilledTonalIconButton(
|
||||||
onClick = skipTimer,
|
onClick = { onAction(TimerAction.SkipTimer) },
|
||||||
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
||||||
containerColor = colorContainer
|
containerColor = colorContainer
|
||||||
),
|
),
|
||||||
@@ -363,7 +368,7 @@ fun TimerScreen(
|
|||||||
},
|
},
|
||||||
text = { Text("Skip to next") },
|
text = { Text("Skip to next") },
|
||||||
onClick = {
|
onClick = {
|
||||||
skipTimer()
|
onAction(TimerAction.SkipTimer)
|
||||||
state.dismiss()
|
state.dismiss()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -411,10 +416,7 @@ fun TimerScreenPreview() {
|
|||||||
TomatoTheme {
|
TomatoTheme {
|
||||||
TimerScreen(
|
TimerScreen(
|
||||||
timerState,
|
timerState,
|
||||||
false,
|
|
||||||
{ 0.3f },
|
{ 0.3f },
|
||||||
{},
|
|
||||||
{},
|
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.timerScreen.viewModel
|
||||||
|
|
||||||
|
sealed interface TimerAction {
|
||||||
|
data object ResetTimer : TimerAction
|
||||||
|
data object SkipTimer : TimerAction
|
||||||
|
data object ToggleTimer : TimerAction
|
||||||
|
}
|
||||||
@@ -13,7 +13,8 @@ data class TimerState(
|
|||||||
val totalTime: Long = 25 * 60,
|
val totalTime: Long = 25 * 60,
|
||||||
val timerRunning: Boolean = false,
|
val timerRunning: Boolean = false,
|
||||||
val nextTimerMode: TimerMode = TimerMode.SHORT_BREAK,
|
val nextTimerMode: TimerMode = TimerMode.SHORT_BREAK,
|
||||||
val nextTimeStr: String = "5:00"
|
val nextTimeStr: String = "5:00",
|
||||||
|
val showBrandTitle: Boolean = true
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class TimerMode {
|
enum class TimerMode {
|
||||||
|
|||||||
@@ -82,10 +82,24 @@ class TimerViewModel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
resetTimer()
|
resetTimer()
|
||||||
|
|
||||||
|
delay(1500)
|
||||||
|
|
||||||
|
_timerState.update { currentState ->
|
||||||
|
currentState.copy(showBrandTitle = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetTimer() {
|
fun onAction(action: TimerAction) {
|
||||||
|
when (action) {
|
||||||
|
TimerAction.ResetTimer -> resetTimer()
|
||||||
|
TimerAction.SkipTimer -> skipTimer()
|
||||||
|
TimerAction.ToggleTimer -> toggleTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetTimer() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
saveTimeToDb()
|
saveTimeToDb()
|
||||||
_time.update { timerRepository.focusTime }
|
_time.update { timerRepository.focusTime }
|
||||||
@@ -106,7 +120,7 @@ class TimerViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun skipTimer() {
|
private fun skipTimer() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
saveTimeToDb()
|
saveTimeToDb()
|
||||||
startTime = 0L
|
startTime = 0L
|
||||||
@@ -147,7 +161,7 @@ class TimerViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleTimer() {
|
private fun toggleTimer() {
|
||||||
if (timerState.value.timerRunning) {
|
if (timerState.value.timerRunning) {
|
||||||
_timerState.update { currentState ->
|
_timerState.update { currentState ->
|
||||||
currentState.copy(timerRunning = false)
|
currentState.copy(timerRunning = false)
|
||||||
|
|||||||
Reference in New Issue
Block a user