|
@@ -39,6 +39,8 @@ import com.example.torus.data.WorkoutRepository
|
|
|
import com.example.torus.id.UuidV7
|
|
import com.example.torus.id.UuidV7
|
|
|
import com.example.torus.model.ExerciseType
|
|
import com.example.torus.model.ExerciseType
|
|
|
import com.example.torus.model.SavedExercise
|
|
import com.example.torus.model.SavedExercise
|
|
|
|
|
+import com.example.torus.model.SessionExerciseInput
|
|
|
|
|
+import com.example.torus.model.SessionSetInput
|
|
|
import com.example.torus.model.WorkoutDetail
|
|
import com.example.torus.model.WorkoutDetail
|
|
|
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
|
|
@@ -53,18 +55,27 @@ private data class WeightSetDraft(
|
|
|
fun StartWorkoutScreen(
|
|
fun StartWorkoutScreen(
|
|
|
workoutId: String,
|
|
workoutId: String,
|
|
|
workoutRepository: WorkoutRepository,
|
|
workoutRepository: WorkoutRepository,
|
|
|
- modifier: Modifier = Modifier
|
|
|
|
|
|
|
+ modifier: Modifier = Modifier,
|
|
|
|
|
+ onCompleteWorkout: (startedAt: Long, exercises: List<SessionExerciseInput>) -> Unit = { _, _ -> }
|
|
|
) {
|
|
) {
|
|
|
var detail by remember(workoutId) { mutableStateOf<WorkoutDetail?>(null) }
|
|
var detail by remember(workoutId) { mutableStateOf<WorkoutDetail?>(null) }
|
|
|
var exerciseIndex by remember(workoutId) { mutableIntStateOf(0) }
|
|
var exerciseIndex by remember(workoutId) { mutableIntStateOf(0) }
|
|
|
|
|
+ var isCompleting by remember { mutableStateOf(false) }
|
|
|
|
|
+ val startedAt = remember(workoutId) { System.currentTimeMillis() }
|
|
|
val setsByExercise = remember(workoutId) {
|
|
val setsByExercise = remember(workoutId) {
|
|
|
mutableStateMapOf<String, SnapshotStateList<WeightSetDraft>>()
|
|
mutableStateMapOf<String, SnapshotStateList<WeightSetDraft>>()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
LaunchedEffect(workoutId) {
|
|
LaunchedEffect(workoutId) {
|
|
|
- detail = workoutRepository.getWorkoutDetail(workoutId)
|
|
|
|
|
|
|
+ val workout = workoutRepository.getWorkoutDetail(workoutId)
|
|
|
|
|
+ detail = workout
|
|
|
exerciseIndex = 0
|
|
exerciseIndex = 0
|
|
|
setsByExercise.clear()
|
|
setsByExercise.clear()
|
|
|
|
|
+ workout?.exercises?.forEach { exercise ->
|
|
|
|
|
+ if (exercise.type == ExerciseType.Weights) {
|
|
|
|
|
+ setsByExercise[exercise.id] = defaultWeightSets()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
val workout = detail ?: return
|
|
val workout = detail ?: return
|
|
@@ -72,8 +83,15 @@ fun StartWorkoutScreen(
|
|
|
workout = workout,
|
|
workout = workout,
|
|
|
exerciseIndex = exerciseIndex,
|
|
exerciseIndex = exerciseIndex,
|
|
|
setsByExercise = setsByExercise,
|
|
setsByExercise = setsByExercise,
|
|
|
|
|
+ isCompleting = isCompleting,
|
|
|
modifier = modifier,
|
|
modifier = modifier,
|
|
|
- onNextExercise = { exerciseIndex += 1 }
|
|
|
|
|
|
|
+ onNextExercise = { exerciseIndex += 1 },
|
|
|
|
|
+ onCompleteWorkout = {
|
|
|
|
|
+ if (!isCompleting) {
|
|
|
|
|
+ isCompleting = true
|
|
|
|
|
+ onCompleteWorkout(startedAt, workout.toSessionInput(setsByExercise))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -82,24 +100,12 @@ private fun StartWorkoutContent(
|
|
|
workout: WorkoutDetail,
|
|
workout: WorkoutDetail,
|
|
|
exerciseIndex: Int,
|
|
exerciseIndex: Int,
|
|
|
setsByExercise: MutableMap<String, SnapshotStateList<WeightSetDraft>>,
|
|
setsByExercise: MutableMap<String, SnapshotStateList<WeightSetDraft>>,
|
|
|
|
|
+ isCompleting: Boolean = false,
|
|
|
modifier: Modifier = Modifier,
|
|
modifier: Modifier = Modifier,
|
|
|
- onNextExercise: () -> Unit = {}
|
|
|
|
|
|
|
+ onNextExercise: () -> Unit = {},
|
|
|
|
|
+ onCompleteWorkout: () -> Unit = {}
|
|
|
) {
|
|
) {
|
|
|
val exercises = workout.exercises
|
|
val exercises = workout.exercises
|
|
|
- if (exercises.isEmpty()) {
|
|
|
|
|
- Text(
|
|
|
|
|
- text = stringResource(R.string.no_exercises),
|
|
|
|
|
- modifier = modifier.padding(16.dp),
|
|
|
|
|
- color = MaterialTheme.colorScheme.onBackground,
|
|
|
|
|
- style = MaterialTheme.typography.bodyLarge
|
|
|
|
|
- )
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- val currentIndex = exerciseIndex.coerceIn(0, exercises.lastIndex)
|
|
|
|
|
- val exercise = exercises[currentIndex]
|
|
|
|
|
- val isLastExercise = currentIndex == exercises.lastIndex
|
|
|
|
|
-
|
|
|
|
|
Column(modifier = modifier.fillMaxSize()) {
|
|
Column(modifier = modifier.fillMaxSize()) {
|
|
|
Column(
|
|
Column(
|
|
|
modifier = Modifier
|
|
modifier = Modifier
|
|
@@ -114,57 +120,85 @@ private fun StartWorkoutContent(
|
|
|
color = Sand,
|
|
color = Sand,
|
|
|
style = MaterialTheme.typography.headlineMedium
|
|
style = MaterialTheme.typography.headlineMedium
|
|
|
)
|
|
)
|
|
|
- Text(
|
|
|
|
|
- text = stringResource(
|
|
|
|
|
- R.string.exercise_progress,
|
|
|
|
|
- currentIndex + 1,
|
|
|
|
|
- exercises.size
|
|
|
|
|
- ),
|
|
|
|
|
- color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
|
|
|
- style = MaterialTheme.typography.titleSmall
|
|
|
|
|
- )
|
|
|
|
|
- Text(
|
|
|
|
|
- text = exercise.name.ifBlank { stringResource(R.string.untitled_exercise) },
|
|
|
|
|
- color = MaterialTheme.colorScheme.onBackground,
|
|
|
|
|
- style = MaterialTheme.typography.headlineSmall
|
|
|
|
|
- )
|
|
|
|
|
- Text(
|
|
|
|
|
- text = stringResource(exercise.type.labelRes),
|
|
|
|
|
- color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
|
|
|
- style = MaterialTheme.typography.labelLarge
|
|
|
|
|
- )
|
|
|
|
|
- if (exercise.type == ExerciseType.Weights) {
|
|
|
|
|
- val sets = setsByExercise.getOrPut(exercise.id) { defaultWeightSets() }
|
|
|
|
|
- WeightSetsEditor(
|
|
|
|
|
- sets = sets,
|
|
|
|
|
- onAddSet = { sets += WeightSetDraft(id = UuidV7.generateString()) },
|
|
|
|
|
- onRemoveSet = { setId ->
|
|
|
|
|
- if (sets.size > 1) {
|
|
|
|
|
- sets.removeAll { it.id == setId }
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- onWeightChange = { setId, weight ->
|
|
|
|
|
- updateSet(sets, setId) { it.copy(weight = weight) }
|
|
|
|
|
- },
|
|
|
|
|
- onRepsChange = { setId, reps ->
|
|
|
|
|
- updateSet(sets, setId) { it.copy(reps = reps) }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (exercises.isEmpty()) {
|
|
|
|
|
+ Text(
|
|
|
|
|
+ text = stringResource(R.string.no_exercises),
|
|
|
|
|
+ color = MaterialTheme.colorScheme.onBackground,
|
|
|
|
|
+ style = MaterialTheme.typography.bodyLarge
|
|
|
)
|
|
)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ val currentIndex = exerciseIndex.coerceIn(0, exercises.lastIndex)
|
|
|
|
|
+ val exercise = exercises[currentIndex]
|
|
|
|
|
+ Text(
|
|
|
|
|
+ text = stringResource(
|
|
|
|
|
+ R.string.exercise_progress,
|
|
|
|
|
+ currentIndex + 1,
|
|
|
|
|
+ exercises.size
|
|
|
|
|
+ ),
|
|
|
|
|
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
|
|
|
+ style = MaterialTheme.typography.titleSmall
|
|
|
|
|
+ )
|
|
|
|
|
+ Text(
|
|
|
|
|
+ text = exercise.name.ifBlank { stringResource(R.string.untitled_exercise) },
|
|
|
|
|
+ color = MaterialTheme.colorScheme.onBackground,
|
|
|
|
|
+ style = MaterialTheme.typography.headlineSmall
|
|
|
|
|
+ )
|
|
|
|
|
+ Text(
|
|
|
|
|
+ text = stringResource(exercise.type.labelRes),
|
|
|
|
|
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
|
|
|
+ style = MaterialTheme.typography.labelLarge
|
|
|
|
|
+ )
|
|
|
|
|
+ if (exercise.type == ExerciseType.Weights) {
|
|
|
|
|
+ val sets = setsByExercise.getOrPut(exercise.id) { defaultWeightSets() }
|
|
|
|
|
+ WeightSetsEditor(
|
|
|
|
|
+ sets = sets,
|
|
|
|
|
+ onAddSet = { sets += WeightSetDraft(id = UuidV7.generateString()) },
|
|
|
|
|
+ onRemoveSet = { setId ->
|
|
|
|
|
+ if (sets.size > 1) {
|
|
|
|
|
+ sets.removeAll { it.id == setId }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ onWeightChange = { setId, weight ->
|
|
|
|
|
+ updateSet(sets, setId) { it.copy(weight = weight) }
|
|
|
|
|
+ },
|
|
|
|
|
+ onRepsChange = { setId, reps ->
|
|
|
|
|
+ updateSet(sets, setId) { it.copy(reps = reps) }
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- if (!isLastExercise) {
|
|
|
|
|
|
|
+ val isLastExercise = exercises.isEmpty() ||
|
|
|
|
|
+ exerciseIndex.coerceIn(0, exercises.lastIndex) == exercises.lastIndex
|
|
|
|
|
+ Column(
|
|
|
|
|
+ modifier = Modifier
|
|
|
|
|
+ .fillMaxWidth()
|
|
|
|
|
+ .padding(horizontal = 16.dp, vertical = 12.dp),
|
|
|
|
|
+ verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
|
|
|
+ ) {
|
|
|
|
|
+ if (!isLastExercise) {
|
|
|
|
|
+ OutlinedButton(
|
|
|
|
|
+ onClick = onNextExercise,
|
|
|
|
|
+ modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
+ shape = RoundedCornerShape(50),
|
|
|
|
|
+ colors = ButtonDefaults.outlinedButtonColors(
|
|
|
|
|
+ contentColor = MaterialTheme.colorScheme.onBackground
|
|
|
|
|
+ )
|
|
|
|
|
+ ) {
|
|
|
|
|
+ Text(text = stringResource(R.string.next_exercise))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
Button(
|
|
Button(
|
|
|
- onClick = onNextExercise,
|
|
|
|
|
- modifier = Modifier
|
|
|
|
|
- .fillMaxWidth()
|
|
|
|
|
- .padding(horizontal = 16.dp, vertical = 12.dp),
|
|
|
|
|
|
|
+ onClick = onCompleteWorkout,
|
|
|
|
|
+ enabled = !isCompleting,
|
|
|
|
|
+ modifier = Modifier.fillMaxWidth(),
|
|
|
shape = RoundedCornerShape(50),
|
|
shape = RoundedCornerShape(50),
|
|
|
colors = ButtonDefaults.buttonColors(
|
|
colors = ButtonDefaults.buttonColors(
|
|
|
containerColor = MaterialTheme.colorScheme.primary,
|
|
containerColor = MaterialTheme.colorScheme.primary,
|
|
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
|
|
)
|
|
)
|
|
|
) {
|
|
) {
|
|
|
- Text(text = stringResource(R.string.next_exercise))
|
|
|
|
|
|
|
+ Text(text = stringResource(R.string.complete_workout))
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -276,6 +310,26 @@ private fun updateSet(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+private fun WorkoutDetail.toSessionInput(
|
|
|
|
|
+ setsByExercise: Map<String, List<WeightSetDraft>>
|
|
|
|
|
+): List<SessionExerciseInput> {
|
|
|
|
|
+ return exercises.map { exercise ->
|
|
|
|
|
+ val sets = if (exercise.type == ExerciseType.Weights) {
|
|
|
|
|
+ (setsByExercise[exercise.id] ?: defaultWeightSets()).map { set ->
|
|
|
|
|
+ SessionSetInput(weight = set.weight, reps = set.reps)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ emptyList()
|
|
|
|
|
+ }
|
|
|
|
|
+ SessionExerciseInput(
|
|
|
|
|
+ workoutExerciseId = exercise.id,
|
|
|
|
|
+ name = exercise.name,
|
|
|
|
|
+ type = exercise.type,
|
|
|
|
|
+ sets = sets
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
private fun filterDecimal(value: String): String {
|
|
private fun filterDecimal(value: String): String {
|
|
|
val filtered = value.filter { it.isDigit() || it == '.' }
|
|
val filtered = value.filter { it.isDigit() || it == '.' }
|
|
|
val firstDot = filtered.indexOf('.')
|
|
val firstDot = filtered.indexOf('.')
|