|
@@ -1,6 +1,8 @@
|
|
|
package com.example.torus.ui
|
|
package com.example.torus.ui
|
|
|
|
|
|
|
|
|
|
+import androidx.compose.foundation.background
|
|
|
import androidx.compose.foundation.border
|
|
import androidx.compose.foundation.border
|
|
|
|
|
+import androidx.compose.foundation.clickable
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.Column
|
|
|
import androidx.compose.foundation.layout.Row
|
|
import androidx.compose.foundation.layout.Row
|
|
@@ -13,7 +15,9 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|
|
import androidx.compose.foundation.verticalScroll
|
|
import androidx.compose.foundation.verticalScroll
|
|
|
import androidx.compose.material3.Button
|
|
import androidx.compose.material3.Button
|
|
|
import androidx.compose.material3.ButtonDefaults
|
|
import androidx.compose.material3.ButtonDefaults
|
|
|
|
|
+import androidx.compose.material3.ExperimentalMaterial3Api
|
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.MaterialTheme
|
|
|
|
|
+import androidx.compose.material3.ModalBottomSheet
|
|
|
import androidx.compose.material3.OutlinedButton
|
|
import androidx.compose.material3.OutlinedButton
|
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.Text
|
|
|
import androidx.compose.material3.TextButton
|
|
import androidx.compose.material3.TextButton
|
|
@@ -42,8 +46,10 @@ import com.example.torus.model.SavedExercise
|
|
|
import com.example.torus.model.SessionExerciseInput
|
|
import com.example.torus.model.SessionExerciseInput
|
|
|
import com.example.torus.model.SessionSetInput
|
|
import com.example.torus.model.SessionSetInput
|
|
|
import com.example.torus.model.WorkoutDetail
|
|
import com.example.torus.model.WorkoutDetail
|
|
|
|
|
+import com.example.torus.ui.theme.Ok
|
|
|
import com.example.torus.ui.theme.Sand
|
|
import com.example.torus.ui.theme.Sand
|
|
|
import com.example.torus.ui.theme.TorusTheme
|
|
import com.example.torus.ui.theme.TorusTheme
|
|
|
|
|
+import com.example.torus.ui.theme.Warn
|
|
|
|
|
|
|
|
private data class WeightSetDraft(
|
|
private data class WeightSetDraft(
|
|
|
val id: String,
|
|
val id: String,
|
|
@@ -86,6 +92,7 @@ fun StartWorkoutScreen(
|
|
|
isCompleting = isCompleting,
|
|
isCompleting = isCompleting,
|
|
|
modifier = modifier,
|
|
modifier = modifier,
|
|
|
onNextExercise = { exerciseIndex += 1 },
|
|
onNextExercise = { exerciseIndex += 1 },
|
|
|
|
|
+ onJumpToExercise = { exerciseIndex = it },
|
|
|
onCompleteWorkout = {
|
|
onCompleteWorkout = {
|
|
|
if (!isCompleting) {
|
|
if (!isCompleting) {
|
|
|
isCompleting = true
|
|
isCompleting = true
|
|
@@ -103,8 +110,10 @@ private fun StartWorkoutContent(
|
|
|
isCompleting: Boolean = false,
|
|
isCompleting: Boolean = false,
|
|
|
modifier: Modifier = Modifier,
|
|
modifier: Modifier = Modifier,
|
|
|
onNextExercise: () -> Unit = {},
|
|
onNextExercise: () -> Unit = {},
|
|
|
|
|
+ onJumpToExercise: (Int) -> Unit = {},
|
|
|
onCompleteWorkout: () -> Unit = {}
|
|
onCompleteWorkout: () -> Unit = {}
|
|
|
) {
|
|
) {
|
|
|
|
|
+ var showExerciseMenu by remember { mutableStateOf(false) }
|
|
|
val exercises = workout.exercises
|
|
val exercises = workout.exercises
|
|
|
Column(modifier = modifier.fillMaxSize()) {
|
|
Column(modifier = modifier.fillMaxSize()) {
|
|
|
Column(
|
|
Column(
|
|
@@ -120,6 +129,18 @@ private fun StartWorkoutContent(
|
|
|
color = Sand,
|
|
color = Sand,
|
|
|
style = MaterialTheme.typography.headlineMedium
|
|
style = MaterialTheme.typography.headlineMedium
|
|
|
)
|
|
)
|
|
|
|
|
+ if (exercises.isNotEmpty()) {
|
|
|
|
|
+ OutlinedButton(
|
|
|
|
|
+ onClick = { showExerciseMenu = true },
|
|
|
|
|
+ modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
+ shape = RoundedCornerShape(50),
|
|
|
|
|
+ colors = ButtonDefaults.outlinedButtonColors(
|
|
|
|
|
+ contentColor = MaterialTheme.colorScheme.onBackground
|
|
|
|
|
+ )
|
|
|
|
|
+ ) {
|
|
|
|
|
+ Text(text = stringResource(R.string.exercises_menu))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
if (exercises.isEmpty()) {
|
|
if (exercises.isEmpty()) {
|
|
|
Text(
|
|
Text(
|
|
|
text = stringResource(R.string.no_exercises),
|
|
text = stringResource(R.string.no_exercises),
|
|
@@ -202,6 +223,18 @@ private fun StartWorkoutContent(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ if (showExerciseMenu && exercises.isNotEmpty()) {
|
|
|
|
|
+ ExerciseJumpMenu(
|
|
|
|
|
+ exercises = exercises,
|
|
|
|
|
+ currentIndex = exerciseIndex.coerceIn(0, exercises.lastIndex),
|
|
|
|
|
+ setsByExercise = setsByExercise,
|
|
|
|
|
+ onSelect = { index ->
|
|
|
|
|
+ onJumpToExercise(index)
|
|
|
|
|
+ showExerciseMenu = false
|
|
|
|
|
+ },
|
|
|
|
|
+ onDismiss = { showExerciseMenu = false }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
@Composable
|
|
@@ -291,6 +324,106 @@ private fun WeightSetCard(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+@OptIn(ExperimentalMaterial3Api::class)
|
|
|
|
|
+@Composable
|
|
|
|
|
+private fun ExerciseJumpMenu(
|
|
|
|
|
+ exercises: List<SavedExercise>,
|
|
|
|
|
+ currentIndex: Int,
|
|
|
|
|
+ setsByExercise: Map<String, List<WeightSetDraft>>,
|
|
|
|
|
+ onSelect: (Int) -> Unit,
|
|
|
|
|
+ onDismiss: () -> Unit
|
|
|
|
|
+) {
|
|
|
|
|
+ ModalBottomSheet(
|
|
|
|
|
+ onDismissRequest = onDismiss,
|
|
|
|
|
+ containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
|
|
|
|
+ contentColor = MaterialTheme.colorScheme.onBackground
|
|
|
|
|
+ ) {
|
|
|
|
|
+ Column(
|
|
|
|
|
+ modifier = Modifier
|
|
|
|
|
+ .fillMaxWidth()
|
|
|
|
|
+ .verticalScroll(rememberScrollState())
|
|
|
|
|
+ .padding(horizontal = 16.dp)
|
|
|
|
|
+ .padding(bottom = 24.dp),
|
|
|
|
|
+ verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
|
|
|
+ ) {
|
|
|
|
|
+ Text(
|
|
|
|
|
+ text = stringResource(R.string.exercises_menu),
|
|
|
|
|
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
|
|
|
+ style = MaterialTheme.typography.titleSmall
|
|
|
|
|
+ )
|
|
|
|
|
+ exercises.forEachIndexed { index, exercise ->
|
|
|
|
|
+ val selected = index == currentIndex
|
|
|
|
|
+ val completed = exercise.hasCompletedSets(setsByExercise)
|
|
|
|
|
+ val shape = RoundedCornerShape(14.dp)
|
|
|
|
|
+ Row(
|
|
|
|
|
+ modifier = Modifier
|
|
|
|
|
+ .fillMaxWidth()
|
|
|
|
|
+ .background(MaterialTheme.colorScheme.surface, shape)
|
|
|
|
|
+ .border(
|
|
|
|
|
+ width = 1.dp,
|
|
|
|
|
+ color = if (selected) {
|
|
|
|
|
+ MaterialTheme.colorScheme.primary
|
|
|
|
|
+ } else {
|
|
|
|
|
+ MaterialTheme.colorScheme.outline
|
|
|
|
|
+ },
|
|
|
|
|
+ shape = shape
|
|
|
|
|
+ )
|
|
|
|
|
+ .clickable { onSelect(index) }
|
|
|
|
|
+ .padding(16.dp),
|
|
|
|
|
+ horizontalArrangement = Arrangement.SpaceBetween,
|
|
|
|
|
+ verticalAlignment = Alignment.CenterVertically
|
|
|
|
|
+ ) {
|
|
|
|
|
+ Column(
|
|
|
|
|
+ modifier = Modifier.weight(1f),
|
|
|
|
|
+ verticalArrangement = Arrangement.spacedBy(4.dp)
|
|
|
|
|
+ ) {
|
|
|
|
|
+ Text(
|
|
|
|
|
+ text = (index + 1).toString(),
|
|
|
|
|
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
|
|
|
+ style = MaterialTheme.typography.labelSmall
|
|
|
|
|
+ )
|
|
|
|
|
+ Text(
|
|
|
|
|
+ text = exercise.name.ifBlank {
|
|
|
|
|
+ stringResource(R.string.untitled_exercise)
|
|
|
|
|
+ },
|
|
|
|
|
+ color = MaterialTheme.colorScheme.onBackground,
|
|
|
|
|
+ style = MaterialTheme.typography.bodyLarge
|
|
|
|
|
+ )
|
|
|
|
|
+ Text(
|
|
|
|
|
+ text = stringResource(exercise.type.labelRes),
|
|
|
|
|
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
|
|
|
+ style = MaterialTheme.typography.labelLarge
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ Text(
|
|
|
|
|
+ text = stringResource(
|
|
|
|
|
+ if (completed) {
|
|
|
|
|
+ R.string.exercise_complete
|
|
|
|
|
+ } else {
|
|
|
|
|
+ R.string.exercise_incomplete
|
|
|
|
|
+ }
|
|
|
|
|
+ ),
|
|
|
|
|
+ color = if (completed) Ok else Warn,
|
|
|
|
|
+ style = MaterialTheme.typography.labelLarge
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private fun SavedExercise.hasCompletedSets(
|
|
|
|
|
+ setsByExercise: Map<String, List<WeightSetDraft>>
|
|
|
|
|
+): Boolean {
|
|
|
|
|
+ if (type != ExerciseType.Weights) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ val sets = setsByExercise[id].orEmpty()
|
|
|
|
|
+ return sets.isNotEmpty() && sets.all { set ->
|
|
|
|
|
+ set.weight.isNotBlank() && set.reps.isNotBlank()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
private fun defaultWeightSets(): SnapshotStateList<WeightSetDraft> {
|
|
private fun defaultWeightSets(): SnapshotStateList<WeightSetDraft> {
|
|
|
return mutableStateListOf(
|
|
return mutableStateListOf(
|
|
|
WeightSetDraft(id = UuidV7.generateString()),
|
|
WeightSetDraft(id = UuidV7.generateString()),
|