Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
f4fa9ed
build(ios): add Apple Silicon simulator target
Jul 16, 2026
7a8381a
test(ios): restore simulator-only Phantom trainer
Jul 16, 2026
398f056
fix(ios): complete Phantom repository lifecycle contract
Jul 16, 2026
1ec4ae0
fix(ios): harden Phantom cancellation lifecycle
Jul 16, 2026
d403059
fix(ios): close Phantom terminal lifecycle gate
Jul 16, 2026
964c6f7
fix(ios): guard Phantom terminal state mutations
Jul 16, 2026
c59424a
fix(ios): close remaining Phantom post-shutdown mutations
Jul 16, 2026
269dc13
fix(ios): seal Phantom terminal control paths
Jul 16, 2026
1bcdc6f
fix(ios): gate Phantom scheduled emissions by lifecycle
Jul 16, 2026
2883acc
fix(ios): close Phantom logging race windows
Jul 16, 2026
18177ca
fix(ios): close Phantom device-found logging race
Jul 16, 2026
5d9c2de
fix(ios): harden Phantom producer generations
Jul 17, 2026
e787444
fix(ios): honor Phantom stop and cancellation contracts
Jul 17, 2026
692a40e
fix(ios): stop Phantom polling with workout
Jul 17, 2026
4145174
fix(ios): preserve Phantom rep completion state
Jul 17, 2026
0911004
fix(ios): initialize Phantom connected state synchronously
Jul 17, 2026
974a1cc
fix(ios): preserve Phantom scan-connect operation generation
Jul 17, 2026
5fa60f9
fix(ios): close final Phantom race boundaries
Jul 17, 2026
78b58f3
fix(ios): publish Phantom rep completion atomically
Jul 17, 2026
7b23c5b
fix(ios): harden Phantom reentrant lifecycle boundaries
Jul 17, 2026
2ec92a4
fix(ios): close Phantom post-publication reentrancy gaps
Jul 17, 2026
97fde7f
fix(ios): serialize final Phantom cleanup boundaries
Jul 17, 2026
d8d04db
fix(ios): guard Phantom control generations
Jul 17, 2026
bc2e7c1
fix(ios): guard Phantom command generations
Jul 17, 2026
ed890a7
fix(ios): preserve Phantom connection generation
Jul 17, 2026
920fc15
fix(ios): gate Phantom reps by connection generation
Jul 17, 2026
9634100
fix(ios): thread Phantom lifecycle tokens completely
Jul 17, 2026
b8577ee
fix(ios): preserve Phantom callback tokens
Jul 17, 2026
aea975a
fix(ios): close Phantom begin and completion races
Jul 17, 2026
139e14a
fix(ios): stabilize Phantom reentrant workout handoff
Jul 17, 2026
2081143
fix(ios): preserve Phantom handle handoff
Jul 17, 2026
c91f4f8
fix(ios): order Phantom connected state publications
Jul 17, 2026
63bef4c
fix(ios): own Phantom teardown lifecycle
Jul 17, 2026
9672738
fix(ios): seal Phantom cleanup entrypoints
Jul 17, 2026
1be5b46
fix(ios): seal Phantom teardown controls
Jul 17, 2026
fb5c0eb
fix(ios): invalidate Phantom workout handoffs
Jul 17, 2026
92ea819
fix(ios): guard Phantom handle reset generation
Jul 17, 2026
f200f61
fix(ios): preserve Phantom handle ownership
Jul 17, 2026
298a957
test(ios): stabilize Phantom stop handoff assertion
Jul 17, 2026
ca4861e
fix(ios): register Phantom active polling ownership
Jul 17, 2026
ee94b97
fix(ios): reconcile Phantom active polling handoff
Jul 17, 2026
316c9e4
fix(ios): reconcile Phantom final lifecycle handoffs
Jul 17, 2026
cab4dda
fix(ios): close Phantom workout ownership gaps
Jul 17, 2026
ceafc9d
fix(ios): close Phantom reservation cleanup races
Jul 17, 2026
365502d
fix(ios): seal Phantom scan cancellation cleanup
Jul 17, 2026
43d9233
fix(ios): close final Phantom cancellation handoffs
Jul 17, 2026
f86e103
fix(ios): close final Phantom ownership races
Jul 17, 2026
9ef4c45
fix(ios): close Phantom control publication races
Jul 17, 2026
3e3bb1a
fix(ios): close Phantom control publication races
Jul 17, 2026
6fa1a98
feat(ios): add simulator Phantom support
Jul 17, 2026
25ab6a4
feat(ios): decode workout commands in Phantom trainer
Jul 17, 2026
a78e526
fix(ios): validate Phantom program opcodes exactly
Jul 17, 2026
de92936
feat(ios): merge Task 3 Phantom packet decoding
Jul 17, 2026
28355e4
feat(#674): add eccentric load option for Old School mode
Jul 25, 2026
ad17b6d
fix(#674): update SetReadyScreenScrollWiringTest for Old School setti…
Jul 25, 2026
b5097ad
fix(#674): gate hasEccentricOverload on OldSchool mode; preserve port…
Jul 25, 2026
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
19 changes: 18 additions & 1 deletion shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ kotlin {
withHostTest {}
}

// iOS target (iosArm64 only - physical devices for distribution)
// iOS targets
val xcf = XCFramework()
iosArm64 {
binaries.framework {
Expand All @@ -52,6 +52,19 @@ kotlin {
freeCompilerArgs += listOf("-Xadd-light-debug=enable")
}
}
iosSimulatorArm64 {
binaries.framework {
baseName = "shared"
isStatic = true
xcf.add(this)
// Link system frameworks required by shared module
linkerOpts("-framework", "HealthKit")
linkerOpts("-framework", "Speech")
}
binaries.all {
freeCompilerArgs += listOf("-Xadd-light-debug=enable")
}
}

sourceSets {
val commonMain by getting {
Expand Down Expand Up @@ -179,11 +192,14 @@ kotlin {

val iosArm64Main by getting
val iosArm64Test by getting
val iosSimulatorArm64Main by getting
val iosSimulatorArm64Test by getting

@Suppress("UNUSED_VARIABLE")
val iosMain by creating {
dependsOn(commonMain)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)

dependencies {
// SQLDelight Native Driver
Expand All @@ -198,6 +214,7 @@ kotlin {
val iosTest by creating {
dependsOn(commonTest)
iosArm64Test.dependsOn(this)
iosSimulatorArm64Test.dependsOn(this)

dependencies {
implementation(libs.sqldelight.native.driver)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,8 @@ object PortalSyncAdapter {
repCountTiming = ex.repCountTiming.name,
stopAtPosition = if (ex.stopAtTop) "TOP" else null,
stallDetection = ex.stallDetectionEnabled,
eccentricLoad = if (ex.programMode == ProgramMode.Echo) {
eccentricLoad = if (ex.programMode == ProgramMode.Echo ||
(ex.programMode == ProgramMode.OldSchool && ex.eccentricLoad.percentage > 100)) {
ex.eccentricLoad.name
} else {
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,11 @@ data class WorkoutParameters(
) {
/** True if this is an Echo workout */
val isEchoMode: Boolean get() = programMode == ProgramMode.Echo

/** True if Old School mode has eccentric load > 100% (triggers 0x4E Echo packet dispatch).
* Gated on ProgramMode.OldSchool so switching to Pump/TUT/TUTBeast/EccentricOnly
* doesn't accidentally dispatch the 0x4E packet from a stale eccentric value. */
val hasEccentricOverload: Boolean get() = programMode == ProgramMode.OldSchool && eccentricLoad.percentage > 100
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.devil.phoenixproject.domain.model.BiomechanicsSetSummary
import com.devil.phoenixproject.domain.model.BodyweightVariantOption
import com.devil.phoenixproject.domain.model.CompletedSet
import com.devil.phoenixproject.domain.model.ConnectionStatus
import com.devil.phoenixproject.domain.model.EchoLevel
import com.devil.phoenixproject.domain.model.FiveThreeOneRoutineDetector
import com.devil.phoenixproject.domain.model.HapticEvent
import com.devil.phoenixproject.domain.model.IntegrationProvider
Expand Down Expand Up @@ -430,8 +431,8 @@ class ActiveSessionEngine(
// deload after 1.25s below 40 mm/s — Issue #553), so DELOAD_OCCURRED fires
// routinely mid-set as the athlete fatigues. It is NOT a cable-release
// signal in Echo mode and must never arm the auto-stop stall timer.
if (params.isEchoMode) {
Logger.d("DELOAD_OCCURRED ignored - Echo mode (deload windows define Echo levels)")
if (params.isEchoMode || params.hasEccentricOverload) {
Logger.d("DELOAD_OCCURRED ignored - eccentric overload active (Echo or Old School+ecc)")
return@collect
}
if (!isWarmupGateOpenForAutoStop()) {
Expand Down Expand Up @@ -1843,8 +1844,8 @@ class ActiveSessionEngine(
val params = coordinator._workoutParameters.value
if (!params.isJustLift) return

val eccentricLoadPct = if (params.isEchoMode) params.eccentricLoad.percentage else 100
val echoLevelVal = if (params.isEchoMode) params.echoLevel.levelValue else 0
val eccentricLoadPct = params.eccentricLoad.percentage
val echoLevelVal = params.echoLevel.levelValue

try {
val defaults = JustLiftDefaultsDocument(
Expand Down Expand Up @@ -1883,8 +1884,8 @@ class ActiveSessionEngine(
val exerciseId = currentExercise.exercise.id ?: return

val isEchoExercise = currentExercise.programMode == ProgramMode.Echo
val eccentricLoadPct = if (isEchoExercise) currentExercise.eccentricLoad.percentage else 100
val echoLevelVal = if (isEchoExercise) currentExercise.echoLevel.levelValue else 0
val eccentricLoadPct = currentExercise.eccentricLoad.percentage
val echoLevelVal = currentExercise.echoLevel.levelValue

try {
val setReps = currentExercise.setReps.ifEmpty { listOf(10) }
Expand Down Expand Up @@ -2831,10 +2832,11 @@ class ActiveSessionEngine(
}
}

val commandValidation = if (bleParams.isEchoMode) {
val hasEccentricOverload = bleParams.hasEccentricOverload
val commandValidation = if (bleParams.isEchoMode || hasEccentricOverload) {
WorkoutCommandValidator.validateEchoControl(
level = bleParams.echoLevel,
warmupReps = bleParams.warmupReps,
level = if (bleParams.isEchoMode) bleParams.echoLevel else EchoLevel.HARDER,
warmupReps = if (bleParams.isEchoMode) bleParams.warmupReps else Constants.DEFAULT_WARMUP_REPS,
targetReps = bleParams.reps,
isJustLift = isJustLiftMode || bleParams.isJustLift,
isAMRAP = bleParams.isAMRAP,
Expand All @@ -2849,10 +2851,10 @@ class ActiveSessionEngine(
return@launch
}

val command = if (bleParams.isEchoMode) {
val command = if (bleParams.isEchoMode || hasEccentricOverload) {
BlePacketFactory.createEchoControl(
Comment on lines +2854 to 2855

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the configured Old School weight in overload packets

When an Old School set uses more than 100% eccentric load, this branch replaces the 96-byte program frame with createEchoControl(). That 32-byte frame serializes reps, Echo timing/velocity, and eccentric percentage, but neither weightPerCableKg, progression, nor the Old School phase profile, so a freshly connected machine cannot receive the user's selected concentric weight and is configured as Echo rather than Old School. The overload path needs a protocol sequence or packet that retains the Old School force configuration instead of sending only the Echo frame.

Useful? React with 👍 / 👎.

level = bleParams.echoLevel,
warmupReps = bleParams.warmupReps,
level = if (bleParams.isEchoMode) bleParams.echoLevel else EchoLevel.HARDER,
warmupReps = if (bleParams.isEchoMode) bleParams.warmupReps else Constants.DEFAULT_WARMUP_REPS,
targetReps = bleParams.reps,
isJustLift = isJustLiftMode || bleParams.isJustLift,
isAMRAP = bleParams.isAMRAP,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1064,7 +1064,7 @@ class RoutineFlowManager(
adjustedReps = setReps,
adjustedProgressionKg = progressionKg,
echoLevel = if (exercise.programMode is ProgramMode.Echo) exercise.echoLevel else null,
eccentricLoadPercent = if (exercise.programMode is ProgramMode.Echo) exercise.eccentricLoad.percentage else null,
eccentricLoadPercent = if (exercise.programMode is ProgramMode.Echo || exercise.programMode is ProgramMode.OldSchool) exercise.eccentricLoad.percentage else null,
)

// Issue #129: Determine if this specific set is AMRAP (null reps = AMRAP)
Expand Down Expand Up @@ -1145,7 +1145,7 @@ class RoutineFlowManager(
adjustedReps = adjustedReps,
adjustedProgressionKg = progressionKg,
echoLevel = if (exercise.programMode is ProgramMode.Echo) exercise.echoLevel else null,
eccentricLoadPercent = if (exercise.programMode is ProgramMode.Echo) exercise.eccentricLoad.percentage else null,
eccentricLoadPercent = if (exercise.programMode is ProgramMode.Echo || exercise.programMode is ProgramMode.OldSchool) exercise.eccentricLoad.percentage else null,
)

// Issue #129: Check raw value for AMRAP - null reps in setReps list = AMRAP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ fun ExerciseEditBottomSheet(
val isTutMode = showCableOnlyExerciseControls &&
(selectedMode is WorkoutMode.TUT || selectedMode is WorkoutMode.TUTBeast)
val isEchoMode = showCableOnlyExerciseControls && selectedMode is WorkoutMode.Echo
val isOldSchool = showCableOnlyExerciseControls && selectedMode is WorkoutMode.OldSchool

LaunchedEffect(rackItems, defaultRackItemIds) {
val enabledRackIds = rackItems.filter { it.enabled }.map { it.id }.toSet()
Expand Down Expand Up @@ -414,12 +415,14 @@ fun ExerciseEditBottomSheet(
}
}

// Echo Mode options
if (isEchoMode) {
// Echo Mode options — EccentricLoad visible for Echo + Old School; EchoLevel Echo-only
if (isEchoMode || isOldSchool) {
EccentricLoadSelector(
eccentricLoad = eccentricLoad,
onLoadChange = viewModel::onEccentricLoadChange,
)
Comment on lines +419 to 423

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve Old School overload in portal routine sync

Old School routines can now save a selected overload locally, but PortalSyncAdapter.toPortalRoutine still emits eccentricLoad = null for every non-Echo exercise. After the routine is pushed and restored on another device, PortalPullAdapter.parseEccentricLoad(null) converts it to 100%, silently removing the configured overload and changing the workout behavior. Serialize this field for Old School as well as Echo.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 1a02b59: PortalSyncAdapter.toPortalRoutine now serializes eccentricLoad for both ProgramMode.Echo and ProgramMode.OldSchool. Previously it was Echo-only, which silently dropped the Old School overload setting during portal sync and restore.

}
if (isEchoMode) {
EchoLevelSelector(
level = echoLevel,
onLevelChange = viewModel::onEchoLevelChange,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,8 @@ fun JustLiftScreen(navController: NavController, viewModel: MainViewModel, theme
}

LaunchedEffect(workoutParameters.programMode) {
if (workoutParameters.isEchoMode) {
eccentricLoad = workoutParameters.eccentricLoad
echoLevel = workoutParameters.echoLevel
}
eccentricLoad = workoutParameters.eccentricLoad
echoLevel = workoutParameters.echoLevel
}

// Navigate to ActiveWorkout when workout becomes active
Expand Down Expand Up @@ -582,7 +580,10 @@ fun JustLiftScreen(navController: NavController, viewModel: MainViewModel, theme
}

val isEchoMode = selectedMode is WorkoutMode.Echo
if (isEchoMode) {
val isOldSchool = selectedMode is WorkoutMode.OldSchool
val showEccentricLoad = isEchoMode || isOldSchool

if (showEccentricLoad) {
Comment on lines +583 to +586

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict Old School choices to supported overload values

When Old School is selected, this exposes the existing dropdown containing 0%, 50%, and 75% as well as overload values; the routine editor does the same, and the SetReady/overview sliders expose the full 0–150% range. However, hasEccentricOverload dispatches eccentric behavior only above 100%, so selecting any sub-100 value displays and persists an asymmetric-load choice while the machine receives the ordinary symmetric Old School program. Filter Old School controls to the supported 100%/110–150% choices or implement the advertised underload behavior.

Useful? React with 👍 / 👎.

Card(
modifier = flexibleBodyModifier,
colors = CardDefaults.cardColors(
Expand Down Expand Up @@ -645,48 +646,51 @@ fun JustLiftScreen(navController: NavController, viewModel: MainViewModel, theme
overflow = TextOverflow.Ellipsis,
)

HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f))
// EchoLevel selector — only for Echo mode
if (isEchoMode) {
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f))

Text(
stringResource(Res.string.echo_level),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
stringResource(Res.string.echo_level),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
)

if (useCompactAccessibility) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
EchoLevel.entries.forEach { level ->
FilterChip(
selected = echoLevel == level,
onClick = {
echoLevel = level
selectedMode = WorkoutMode.Echo(level)
},
label = { Text(echoLevelLabel(level)) },
)
if (useCompactAccessibility) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
EchoLevel.entries.forEach { level ->
FilterChip(
selected = echoLevel == level,
onClick = {
echoLevel = level
selectedMode = WorkoutMode.Echo(level)
},
label = { Text(echoLevelLabel(level)) },
)
}
}
}
} else {
SingleChoiceSegmentedButtonRow(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp),
) {
EchoLevel.entries.forEachIndexed { index, level ->
SegmentedButton(
shape = SegmentedButtonDefaults.itemShape(index = index, count = EchoLevel.entries.size),
onClick = {
echoLevel = level
selectedMode = WorkoutMode.Echo(level)
},
selected = echoLevel == level,
) {
Text(echoLevelLabel(level), maxLines = 1)
} else {
SingleChoiceSegmentedButtonRow(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp),
) {
EchoLevel.entries.forEachIndexed { index, level ->
SegmentedButton(
shape = SegmentedButtonDefaults.itemShape(index = index, count = EchoLevel.entries.size),
onClick = {
echoLevel = level
selectedMode = WorkoutMode.Echo(level)
},
selected = echoLevel == level,
) {
Text(echoLevelLabel(level), maxLines = 1)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ fun RoutineOverviewScreen(navController: NavController, viewModel: MainViewModel
adjustedReps = adjustments.reps,
isAMRAP = exercise.isAMRAP,
isEchoMode = exercise.programMode is ProgramMode.Echo,
isOldSchool = exercise.programMode is ProgramMode.OldSchool,
echoLevel = adjustments.echoLevel,
eccentricLoadPercent = adjustments.eccentricLoadPercent,
sizing = overviewSizing,
Expand Down Expand Up @@ -552,6 +553,7 @@ private fun ExerciseOverviewCard(
adjustedReps: Int,
isAMRAP: Boolean,
isEchoMode: Boolean,
isOldSchool: Boolean = false,
echoLevel: EchoLevel,
eccentricLoadPercent: Int,
sizing: RoutineOverviewSizing,
Expand Down Expand Up @@ -667,18 +669,22 @@ private fun ExerciseOverviewCard(
)

if (isEchoMode) {
// Echo mode: Show Echo Level + Eccentric Load + Reps
EchoLevelPillSelector(
selectedLevel = echoLevel,
onLevelChange = onEchoLevelChange,
)
}

// Eccentric Load slider — visible for Echo and Old School modes
if (isEchoMode || isOldSchool) {
OverviewEccentricLoadSlider(
percent = eccentricLoadPercent,
onPercentChange = onEccentricLoadChange,
Comment on lines +679 to 682

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply the overview eccentric adjustment when starting

For an Old School routine, changing this newly visible slider only updates ExerciseOverviewAdjustments; startCurrentExercise() passes only adjusted weight and reps to enterSetReadyWithAdjustments(), which then restores both the SetReady percentage and WorkoutParameters.eccentricLoad from the saved exercise. Consequently, selecting (for example) 130% on the overview and pressing Start silently begins with the routine's original percentage. Pass the adjusted eccentric value through the start path alongside weight and reps.

Useful? React with 👍 / 👎.

)
}

// Reps for Echo mode
// Reps adjuster for Echo mode only
if (isEchoMode) {
if (!isAMRAP) {
SliderWithButtons(
value = adjustedReps.toFloat(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ fun SetReadyScreen(navController: NavController, viewModel: MainViewModel, exerc
var pendingOverridesToSave by remember { mutableStateOf<Map<String, RackItemBehavior>>(emptyMap()) }

val isEchoMode = currentExercise.programMode is ProgramMode.Echo
val isOldSchool = currentExercise.programMode is ProgramMode.OldSchool
val isAMRAP = currentExercise.isAMRAP

// #635: explicit stored flag with equipment-derivation fallback
Expand Down Expand Up @@ -626,7 +627,7 @@ fun SetReadyScreen(navController: NavController, viewModel: MainViewModel, exerc
verticalArrangement = Arrangement.spacedBy(Spacing.medium),
) {
Text(
if (isEchoMode) "ECHO SETTINGS" else "SET CONFIGURATION",
if (isEchoMode) "ECHO SETTINGS" else if (isOldSchool) "OLD SCHOOL SETTINGS" else "SET CONFIGURATION",
style = labelAllCaps.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Expand All @@ -637,14 +638,18 @@ fun SetReadyScreen(navController: NavController, viewModel: MainViewModel, exerc
selectedLevel = setReadyState.echoLevel ?: EchoLevel.HARDER,
onLevelChange = { viewModel.updateSetReadyEchoLevel(it) },
)
}

// Eccentric Load slider - matching RestTimerCard style
// Eccentric Load slider — visible for Echo and Old School modes
if (isEchoMode || isOldSchool) {
SetReadyEccentricLoadSlider(
percent = setReadyState.eccentricLoadPercent ?: 100,
onPercentChange = { viewModel.updateSetReadyEccentricLoad(it) },
)
}

// Reps adjuster for Echo mode too
// Reps adjuster for Echo mode only
if (isEchoMode) {
if (!isAMRAP) {
SliderWithButtons(
value = setReadyState.adjustedReps.toFloat(),
Expand Down
Loading
Loading