From 6955e3afeb27e3c5e27d5f460001d9847003b1cc Mon Sep 17 00:00:00 2001 From: Devil Date: Fri, 10 Jul 2026 14:12:25 -0400 Subject: [PATCH] feat(#646): label set types in workout UI - Pure-Kotlin setTypeLabel() helper returns sealed Calibration | Warmup(n) | Working off existing repCount.warmupReps/isWarmupComplete + coordinator.currentWarmupSetIndex - WorkoutHud ExecutionPage renders label inline under the existing 'Set X / Y' text; rides the existing !isJustLift guard - SetReadyScreen header renders warmup/working label inline with the existing mode picker; calibration label suppressed - 6 locale strings.xml: set_type_calibration | set_type_warmup | set_type_working - WorkoutTab forwards currentWarmupSetIndex from state through to WorkoutHud - 4 pure-Kotlin helper tests; existing 2431-test suite still green Fixes #646 --- .../composeResources/values-de/strings.xml | 4 ++ .../composeResources/values-es/strings.xml | 4 ++ .../composeResources/values-fr/strings.xml | 4 ++ .../composeResources/values-it/strings.xml | 4 ++ .../composeResources/values-nl/strings.xml | 4 ++ .../composeResources/values/strings.xml | 4 ++ .../presentation/screen/SetReadyScreen.kt | 23 ++++++++++ .../presentation/screen/WorkoutHud.kt | 19 +++++++++ .../presentation/screen/WorkoutTab.kt | 4 ++ .../presentation/util/SetTypeLabel.kt | 39 +++++++++++++++++ .../presentation/SetTypeLabelTest.kt | 42 +++++++++++++++++++ 11 files changed, 151 insertions(+) create mode 100644 shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/util/SetTypeLabel.kt create mode 100644 shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/SetTypeLabelTest.kt diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml index de02982dd..48ab26876 100644 --- a/shared/src/commonMain/composeResources/values-de/strings.xml +++ b/shared/src/commonMain/composeResources/values-de/strings.xml @@ -700,5 +700,9 @@ Equipment-Rack Inklusive: %1$s +%1$d weitere + + Calibration Reps + Warm-up Set %1$d + Working Set diff --git a/shared/src/commonMain/composeResources/values-es/strings.xml b/shared/src/commonMain/composeResources/values-es/strings.xml index 97507b74a..32067b2a3 100644 --- a/shared/src/commonMain/composeResources/values-es/strings.xml +++ b/shared/src/commonMain/composeResources/values-es/strings.xml @@ -700,5 +700,9 @@ Rack de equipamiento Incluye: %1$s +%1$d más + + Calibration Reps + Warm-up Set %1$d + Working Set diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml index 35dc22e46..94a421157 100644 --- a/shared/src/commonMain/composeResources/values-fr/strings.xml +++ b/shared/src/commonMain/composeResources/values-fr/strings.xml @@ -700,5 +700,9 @@ Rack d\'équipement Inclus : %1$s +%1$d de plus + + Calibration Reps + Warm-up Set %1$d + Working Set diff --git a/shared/src/commonMain/composeResources/values-it/strings.xml b/shared/src/commonMain/composeResources/values-it/strings.xml index d8fa81481..ab7068fc5 100644 --- a/shared/src/commonMain/composeResources/values-it/strings.xml +++ b/shared/src/commonMain/composeResources/values-it/strings.xml @@ -67,5 +67,9 @@ Attivato Disattivato Ha effetto alla prossima connessione. Riconnettiti al trainer dopo la modifica. + + Calibration Reps + Warm-up Set %1$d + Working Set diff --git a/shared/src/commonMain/composeResources/values-nl/strings.xml b/shared/src/commonMain/composeResources/values-nl/strings.xml index d41107db4..211bc6e7d 100644 --- a/shared/src/commonMain/composeResources/values-nl/strings.xml +++ b/shared/src/commonMain/composeResources/values-nl/strings.xml @@ -679,5 +679,9 @@ Equipment rack Inclusief: %1$s +%1$d meer + + Calibration Reps + Warm-up Set %1$d + Working Set diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index 25acd79d7..d2ee0ed4d 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -765,6 +765,10 @@ %1$d selected Rack: %1$s Detailed metrics were not captured for this set + + Calibration Reps + Warm-up Set %1$d + Working Set Weighted vest Dip belt Chains diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SetReadyScreen.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SetReadyScreen.kt index 17f93095c..37eabed3a 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SetReadyScreen.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SetReadyScreen.kt @@ -51,6 +51,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import com.devil.phoenixproject.domain.model.RepCount +import com.devil.phoenixproject.presentation.util.SetTypeLabel +import com.devil.phoenixproject.presentation.util.setTypeLabel import com.devil.phoenixproject.ui.theme.screenBackgroundBrush import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role @@ -106,6 +109,8 @@ import vitruvianprojectphoenix.shared.generated.resources.equipment_rack_save_ov import vitruvianprojectphoenix.shared.generated.resources.equipment_rack_save_override_title import vitruvianprojectphoenix.shared.generated.resources.exit_routine_message import vitruvianprojectphoenix.shared.generated.resources.exit_routine_title +import vitruvianprojectphoenix.shared.generated.resources.set_type_warmup +import vitruvianprojectphoenix.shared.generated.resources.set_type_working import vitruvianprojectphoenix.shared.generated.resources.target_reps /** @@ -124,6 +129,9 @@ fun SetReadyScreen(navController: NavController, viewModel: MainViewModel, exerc val weightRecommendation by viewModel.weightAdjustmentRecommendation.collectAsState() val rackItems by viewModel.rackItems.collectAsState() val activeRackItemIds by viewModel.activeRackItemIds.collectAsState() + // Issue #646: drive set-type badge in SetReady header + val currentWarmupSetIndex by viewModel.currentWarmupSetIndex.collectAsState() + val repCount by viewModel.repCount.collectAsState() // Get current state val setReadyState = routineFlowState as? RoutineFlowState.SetReady @@ -435,6 +443,21 @@ fun SetReadyScreen(navController: NavController, viewModel: MainViewModel, exerc style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.72f), ) + // Issue #646: set-type badge — calibration label suppressed at SetReady + // (calibration reps run during the active set, not at this screen) + val setType = setTypeLabel(repCount, currentWarmupSetIndex, showCalibrationLabel = false) + val setTypeText = when (setType) { + is SetTypeLabel.Warmup -> stringResource(Res.string.set_type_warmup, setType.setNumber) + SetTypeLabel.Working -> stringResource(Res.string.set_type_working) + SetTypeLabel.Calibration -> "" // unreachable; showCalibrationLabel=false + } + if (setTypeText.isNotEmpty()) { + Text( + text = setTypeText, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.72f), + ) + } } } } diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutHud.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutHud.kt index 34f4525a6..4a537ee16 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutHud.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutHud.kt @@ -33,8 +33,10 @@ import com.devil.phoenixproject.presentation.components.ForceCurveMiniGraph import com.devil.phoenixproject.presentation.components.StableRepProgress import com.devil.phoenixproject.presentation.util.LocalWindowSizeClass import com.devil.phoenixproject.presentation.util.ResponsiveDimensions +import com.devil.phoenixproject.presentation.util.SetTypeLabel import com.devil.phoenixproject.presentation.util.WindowWidthSizeClass import com.devil.phoenixproject.presentation.util.isCompactAccessibilityLayout +import com.devil.phoenixproject.presentation.util.setTypeLabel import com.devil.phoenixproject.ui.theme.AccessibilityTheme import com.devil.phoenixproject.ui.theme.velocityZoneColor import com.devil.phoenixproject.ui.theme.velocityZoneLabel @@ -82,6 +84,7 @@ fun WorkoutHud( onResetExerciseTimer: () -> Unit = {}, velocityLossThresholdPercent: Int = 20, rackLoadAdjustment: RackLoadAdjustment = RackLoadAdjustment(), + currentWarmupSetIndex: Int = -1, // Issue #646: -1 = no warm-up phase modifier: Modifier = Modifier, ) { // Determine if we're in Echo mode @@ -172,6 +175,7 @@ fun WorkoutHud( onPauseExerciseTimer = onPauseExerciseTimer, onResumeExerciseTimer = onResumeExerciseTimer, onResetExerciseTimer = onResetExerciseTimer, + currentWarmupSetIndex = currentWarmupSetIndex, ) } @@ -471,6 +475,7 @@ private fun ExecutionPage( onPauseExerciseTimer: () -> Unit = {}, onResumeExerciseTimer: () -> Unit = {}, onResetExerciseTimer: () -> Unit = {}, + currentWarmupSetIndex: Int = -1, // Issue #646: -1 = no warm-up phase ) { // Issue #192: Check if this is a timed exercise val isTimedExercise = timedExerciseRemainingSeconds != null @@ -506,6 +511,20 @@ private fun ExecutionPage( ) } + // Issue #646: set-type badge (Calibration Reps / Warm-up Set N / Working Set) + val setType = setTypeLabel(repCount, currentWarmupSetIndex, showCalibrationLabel = true) + val setTypeText = when (setType) { + SetTypeLabel.Calibration -> stringResource(Res.string.set_type_calibration) + is SetTypeLabel.Warmup -> stringResource(Res.string.set_type_warmup, setType.setNumber) + SetTypeLabel.Working -> stringResource(Res.string.set_type_working) + } + Text( + text = setTypeText, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.85f), + ) + Spacer(modifier = Modifier.height(24.dp)) } diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutTab.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutTab.kt index 044f9b767..cfcca417e 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutTab.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutTab.kt @@ -224,6 +224,7 @@ fun WorkoutTab( velocityLossThresholdPercent = state.velocityLossThresholdPercent, weightStepKg = state.weightStepKg, rackLoadAdjustment = state.rackLoadAdjustment, + currentWarmupSetIndex = state.currentWarmupSetIndex, ) } @@ -305,6 +306,8 @@ fun WorkoutTab( // Issue #266/#410: Configurable weight step from user preferences (kg) weightStepKg: Float = 0.25f, rackLoadAdjustment: RackLoadAdjustment = RackLoadAdjustment(), + // Issue #646: -1 means not currently in variable warm-up phase + currentWarmupSetIndex: Int = -1, ) { // Note: HapticFeedbackEffect is now global in EnhancedMainScreen // No need for local haptic effect here @@ -342,6 +345,7 @@ fun WorkoutTab( onResetExerciseTimer = onResetExerciseTimer, velocityLossThresholdPercent = velocityLossThresholdPercent, rackLoadAdjustment = rackLoadAdjustment, + currentWarmupSetIndex = currentWarmupSetIndex, modifier = Modifier.fillMaxSize(), ) diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/util/SetTypeLabel.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/util/SetTypeLabel.kt new file mode 100644 index 000000000..eece6a490 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/util/SetTypeLabel.kt @@ -0,0 +1,39 @@ +package com.devil.phoenixproject.presentation.util + +import com.devil.phoenixproject.domain.model.RepCount + +/** + * Issue #646: pure-Kotlin helper for the workout set-type badge. + * + * Decides whether the active workout header should read + * "Calibration Reps" / "Warm-up Set N" / "Working Set". + * + * Kept free of `@Composable` and string resources so it stays trivially + * testable as plain JVM code. Call sites resolve the sealed label to a + * locale string via [com.devil.phoenixproject.presentation.R] mappings. + * + * `showCalibrationLabel` is `false` at Set Ready because the firmware + * calibration buffer runs during the active set, not at Set Ready. + */ +sealed class SetTypeLabel { + data object Calibration : SetTypeLabel() + data class Warmup(val setNumber: Int) : SetTypeLabel() + data object Working : SetTypeLabel() +} + +// ponytail: 3 mirrors Constants.DEFAULT_WARMUP_REPS; revisit if firmware +// calibration buffer size ever changes. +private const val CALIBRATION_BUFFER_REPS = 3 + +fun setTypeLabel( + repCount: RepCount, + currentWarmupSetIndex: Int, + showCalibrationLabel: Boolean, +): SetTypeLabel = when { + showCalibrationLabel && repCount.warmupReps < CALIBRATION_BUFFER_REPS && !repCount.isWarmupComplete -> + SetTypeLabel.Calibration + currentWarmupSetIndex >= 0 -> + SetTypeLabel.Warmup(currentWarmupSetIndex + 1) + else -> + SetTypeLabel.Working +} diff --git a/shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/SetTypeLabelTest.kt b/shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/SetTypeLabelTest.kt new file mode 100644 index 000000000..a2ca0411b --- /dev/null +++ b/shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/SetTypeLabelTest.kt @@ -0,0 +1,42 @@ +package com.devil.phoenixproject.presentation + +import com.devil.phoenixproject.domain.model.RepCount +import com.devil.phoenixproject.presentation.util.SetTypeLabel +import com.devil.phoenixproject.presentation.util.setTypeLabel +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Issue #646: pin the helper that drives the active-workout set-type badge. + * Pure Kotlin, no Compose runtime. + */ +class SetTypeLabelTest { + + @Test + fun calibration_when_buffer_incomplete() { + val rc = RepCount(warmupReps = 0, isWarmupComplete = false) + val label = setTypeLabel(rc, currentWarmupSetIndex = -1, showCalibrationLabel = true) + assertEquals(SetTypeLabel.Calibration, label) + } + + @Test + fun working_when_buffer_complete() { + val rc = RepCount(warmupReps = 3, isWarmupComplete = true) + val label = setTypeLabel(rc, currentWarmupSetIndex = -1, showCalibrationLabel = true) + assertEquals(SetTypeLabel.Working, label) + } + + @Test + fun warmup_during_variable_warmup_set_1() { + val rc = RepCount(warmupReps = 0, isWarmupComplete = false) + val label = setTypeLabel(rc, currentWarmupSetIndex = 0, showCalibrationLabel = false) + assertEquals(SetTypeLabel.Warmup(setNumber = 1), label) + } + + @Test + fun working_when_no_warmup_index_at_setready() { + val rc = RepCount(warmupReps = 0, isWarmupComplete = false) + val label = setTypeLabel(rc, currentWarmupSetIndex = -1, showCalibrationLabel = false) + assertEquals(SetTypeLabel.Working, label) + } +}