diff --git a/composeApp/src/commonMain/kotlin/com/majotyler/hiittimer/presentation/playWorkoutScreen/PlayWorkoutScreen.kt b/composeApp/src/commonMain/kotlin/com/majotyler/hiittimer/presentation/playWorkoutScreen/PlayWorkoutScreen.kt index 9f7b018..50ca548 100644 --- a/composeApp/src/commonMain/kotlin/com/majotyler/hiittimer/presentation/playWorkoutScreen/PlayWorkoutScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/majotyler/hiittimer/presentation/playWorkoutScreen/PlayWorkoutScreen.kt @@ -18,11 +18,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.majotyler.hiittimer.presentation.common.composables.ConfirmationDialog import com.majotyler.hiittimer.presentation.common.expect.BackHandler +private val RestYellow = Color(0xFFFFCC00) + @Composable fun PlayWorkoutScreen( viewModel: PlayWorkoutVIewModel, @@ -40,6 +43,10 @@ fun PlayWorkoutScreen( PlayWorkoutContent( confirmationDialogVisible = state.confirmationDialogVisible, + intervalName = state.intervalName, + intervalNumber = state.intervalNumber, + intervalTotal = state.intervalTotal, + isResting = state.isResting, progressDisplay = state.progressDisplay, progress = state.progress, onClickedDialogCancel = { @@ -51,11 +58,7 @@ fun PlayWorkoutScreen( onClickedPlay = { viewModel.onEvent(event = PlayWorkoutViewEvent.ClickedPlay) }, text = text, play = play, - onClickedPause = { - viewModel.onEvent( - PlayWorkoutViewEvent.ClickedPause - ) - }, + onClickedPause = { viewModel.onEvent(PlayWorkoutViewEvent.ClickedPause) }, enabled = enabled, ) } @@ -63,6 +66,10 @@ fun PlayWorkoutScreen( @Composable private fun PlayWorkoutContent( confirmationDialogVisible: Boolean, + intervalName: String, + intervalNumber: Int, + intervalTotal: Int, + isResting: Boolean, progressDisplay: String, progress: Float, onClickedPlay: () -> Unit, @@ -82,47 +89,77 @@ private fun PlayWorkoutContent( ) } + val accentColor = if (isResting) RestYellow else Color.Green + val progressTrackColor = MaterialTheme.colorScheme.secondary + Column( - modifier = Modifier.fillMaxSize().background(color = MaterialTheme.colorScheme.background), + modifier = Modifier + .fillMaxSize() + .background(color = MaterialTheme.colorScheme.background), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Center, ) { - val progressTrackColor = MaterialTheme.colorScheme.secondary + Text( + text = if (isResting) "Rest" else "Exercise", + style = MaterialTheme.typography.labelLarge, + color = accentColor, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = intervalName, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onBackground, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "Interval $intervalNumber of $intervalTotal", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f), + ) + + Spacer(modifier = Modifier.height(32.dp)) Box( - Modifier.size(200.dp).drawBehind { - drawArc( - startAngle = 180F, sweepAngle = 360F, useCenter = false, - style = Stroke(width = 10f), color = progressTrackColor, - ) - - drawArc( - startAngle = 180F, - sweepAngle = 360f * progress, - useCenter = false, - style = Stroke(width = 12f), - color = Color.Green - ) - }, - contentAlignment = Alignment.Center + modifier = Modifier + .size(200.dp) + .drawBehind { + drawArc( + startAngle = 180F, + sweepAngle = 360F, + useCenter = false, + style = Stroke(width = 10f), + color = progressTrackColor, + ) + drawArc( + startAngle = 180F, + sweepAngle = 360f * progress, + useCenter = false, + style = Stroke(width = 12f), + color = accentColor, + ) + }, + contentAlignment = Alignment.Center, ) { Text( style = MaterialTheme.typography.titleLarge, - text = progressDisplay, color = MaterialTheme.colorScheme.primary + text = progressDisplay, + color = MaterialTheme.colorScheme.primary, ) } - Spacer(modifier = Modifier.height(12.dp)) - Button(onClick = { - if (play) { - onClickedPause() - } else { - onClickedPlay() - } - }, enabled = enabled) { - Text(text) - } + Spacer(modifier = Modifier.height(32.dp)) + Button( + onClick = { if (play) onClickedPause() else onClickedPlay() }, + enabled = enabled, + ) { + Text(text) + } } - } diff --git a/composeApp/src/commonMain/kotlin/com/majotyler/hiittimer/presentation/playWorkoutScreen/PlayWorkoutVIewModel.kt b/composeApp/src/commonMain/kotlin/com/majotyler/hiittimer/presentation/playWorkoutScreen/PlayWorkoutVIewModel.kt index addbd5f..17e0dd6 100644 --- a/composeApp/src/commonMain/kotlin/com/majotyler/hiittimer/presentation/playWorkoutScreen/PlayWorkoutVIewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/majotyler/hiittimer/presentation/playWorkoutScreen/PlayWorkoutVIewModel.kt @@ -20,7 +20,7 @@ class PlayWorkoutVIewModel( private val workouts: List, ) : ViewModel() { private val _state = MutableStateFlow( - value = PlayWorkoutViewState( + value = buildState( confirmationDialogVisible = false, progressDisplay = formatMs(ms = 0L), progress = 0F, @@ -29,8 +29,8 @@ class PlayWorkoutVIewModel( val state = _state.asStateFlow() private val _play = MutableStateFlow(false) - val play = _play.asStateFlow() + private val _text = MutableStateFlow("Start") val text = _text.asStateFlow() @@ -74,16 +74,11 @@ class PlayWorkoutVIewModel( } private fun onClickedDialogCancel() { - _state.update { - it.copy(confirmationDialogVisible = false) - } + _state.update { it.copy(confirmationDialogVisible = false) } } private fun onClickedDialogConfirm() { - _state.update { - it.copy(confirmationDialogVisible = false) - } - + _state.update { it.copy(confirmationDialogVisible = false) } router.routeTo(destination = PlayWorkoutDestination.NavigateUp) } @@ -112,22 +107,7 @@ class PlayWorkoutVIewModel( private fun onClickedSystemBack() { pauseWorkout() - - _state.update { - it.copy( - confirmationDialogVisible = true, - ) - } - } - - private fun formatMs(ms: Long): String { - val minutes = ms / MILLISECONDS_IN_MINUTE - val seconds = (ms % MILLISECONDS_IN_MINUTE) / MILLISECONDS_IN_SECOND - val centiseconds = (ms % MILLISECONDS_IN_SECOND) / MILLISECONDS_IN_CENTISECOND - - return minutes.toString().padStart(2, '0') + - ":${seconds.toString().padStart(2, '0')}" + - ":${centiseconds.toString().padStart(2, '0')}" + _state.update { it.copy(confirmationDialogVisible = true) } } private fun pauseWorkout() { @@ -147,17 +127,15 @@ class PlayWorkoutVIewModel( val progress: Float = (currentStepProgressMs / currentStepTotalMs.toFloat()) - _state.update { oldState -> - oldState.copy( + _state.update { _ -> + buildState( + confirmationDialogVisible = _state.value.confirmationDialogVisible, progressDisplay = formatMs(ms = currentStepProgressMs), progress = progress, ).also { newState -> - runningMark = TimeSource.Monotonic.markNow() - val hasReachedEndOfCurrentStep: Boolean = newState.progress >= 1F - - if (hasReachedEndOfCurrentStep) { + if (newState.progress >= 1F) { val lastIndexOfIntervalsInCurrentWorkout = currentWorkout?.intervals?.lastIndex val isResting = @@ -171,7 +149,6 @@ class PlayWorkoutVIewModel( _text.value = "Start" _enabled.value = false pollProgressJob?.cancel() - } else { nextStep() } @@ -183,24 +160,48 @@ class PlayWorkoutVIewModel( private fun nextStep() { when (currentWorkoutIntervalState) { - PlayWorkoutStateOfInterval.EXERCISING -> { - currentWorkoutIntervalState = - PlayWorkoutStateOfInterval.RESTING - } + PlayWorkoutStateOfInterval.EXERCISING -> + currentWorkoutIntervalState = PlayWorkoutStateOfInterval.RESTING PlayWorkoutStateOfInterval.RESTING -> { currentWorkoutIntervalIndex++ - currentWorkoutIntervalState = - PlayWorkoutStateOfInterval.EXERCISING + currentWorkoutIntervalState = PlayWorkoutStateOfInterval.EXERCISING } } currentStepProgressMs = 0L } + private fun buildState( + confirmationDialogVisible: Boolean, + progressDisplay: String, + progress: Float, + ): PlayWorkoutViewState { + val totalIntervals = currentWorkout?.intervals?.size ?: 0 + return PlayWorkoutViewState( + confirmationDialogVisible = confirmationDialogVisible, + intervalName = currentWorkoutInterval?.name ?: "", + intervalNumber = currentWorkoutIntervalIndex + 1, + intervalTotal = totalIntervals, + isResting = currentWorkoutIntervalState == PlayWorkoutStateOfInterval.RESTING, + progressDisplay = progressDisplay, + progress = progress, + ) + } + companion object { private const val UPDATE_DELAY_MS = 16L private const val MILLISECONDS_IN_CENTISECOND = 10L private const val MILLISECONDS_IN_MINUTE = 60_000L private const val MILLISECONDS_IN_SECOND = 1_000L + + private fun formatMs(ms: Long): String { + val minutes = ms / MILLISECONDS_IN_MINUTE + val seconds = (ms % MILLISECONDS_IN_MINUTE) / MILLISECONDS_IN_SECOND + val centiseconds = (ms % MILLISECONDS_IN_SECOND) / MILLISECONDS_IN_CENTISECOND + + return minutes.toString().padStart(2, '0') + + ":${seconds.toString().padStart(2, '0')}" + + ":${centiseconds.toString().padStart(2, '0')}" + } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/majotyler/hiittimer/presentation/playWorkoutScreen/PlayWorkoutViewState.kt b/composeApp/src/commonMain/kotlin/com/majotyler/hiittimer/presentation/playWorkoutScreen/PlayWorkoutViewState.kt index 7b2f540..11cd10b 100644 --- a/composeApp/src/commonMain/kotlin/com/majotyler/hiittimer/presentation/playWorkoutScreen/PlayWorkoutViewState.kt +++ b/composeApp/src/commonMain/kotlin/com/majotyler/hiittimer/presentation/playWorkoutScreen/PlayWorkoutViewState.kt @@ -1,14 +1,11 @@ package com.majotyler.hiittimer.presentation.playWorkoutScreen -/** - * An encapsulation of the UI state required to render a timer which may be incremented. - * - * @param progressDisplay Display text representing the [progress] value. - * @param progress A ratio between 0F and 1F representing the progress of the current step of the - * workout. - */ data class PlayWorkoutViewState( val confirmationDialogVisible: Boolean, + val intervalName: String, + val intervalNumber: Int, + val intervalTotal: Int, + val isResting: Boolean, val progressDisplay: String, val progress: Float, -) \ No newline at end of file +)