Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = {
Expand All @@ -51,18 +58,18 @@ fun PlayWorkoutScreen(
onClickedPlay = { viewModel.onEvent(event = PlayWorkoutViewEvent.ClickedPlay) },
text = text,
play = play,
onClickedPause = {
viewModel.onEvent(
PlayWorkoutViewEvent.ClickedPause
)
},
onClickedPause = { viewModel.onEvent(PlayWorkoutViewEvent.ClickedPause) },
enabled = enabled,
)
}

@Composable
private fun PlayWorkoutContent(
confirmationDialogVisible: Boolean,
intervalName: String,
intervalNumber: Int,
intervalTotal: Int,
isResting: Boolean,
progressDisplay: String,
progress: Float,
onClickedPlay: () -> Unit,
Expand All @@ -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)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class PlayWorkoutVIewModel(
private val workouts: List<Workout>,
) : ViewModel() {
private val _state = MutableStateFlow(
value = PlayWorkoutViewState(
value = buildState(
confirmationDialogVisible = false,
progressDisplay = formatMs(ms = 0L),
progress = 0F,
Expand All @@ -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()

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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() {
Expand All @@ -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 =
Expand All @@ -171,7 +149,6 @@ class PlayWorkoutVIewModel(
_text.value = "Start"
_enabled.value = false
pollProgressJob?.cancel()

} else {
nextStep()
}
Expand All @@ -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')}"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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,
)
)