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)
+ }
+}