feat(US-06): match original layout and labels

- Layout order: title → BPM → pulse ring → SLOW/FAST slider
  → presets → VOL slider → START/STOP (full-width, at bottom)
- 'steps / min' replaces 'BPM' under the number
- 'X.X steps/sec' calculated sub-label added
- Slider flanked by SLOW / FAST labels
- Volume shows 'VOL' left and '60%' right
- Default BPM 130, default volume 60% to match original
This commit is contained in:
Jeena 2026-03-09 12:09:44 +00:00
parent 12f38c19ab
commit dc1d05b1a7
3 changed files with 96 additions and 64 deletions

View file

@ -20,7 +20,7 @@ class SettingsRepository(context: Context) {
prefs.edit().putFloat(KEY_VOLUME, volume).apply()
}
fun loadBpm(): Int = prefs.getInt(KEY_BPM, 120)
fun loadBpm(): Int = prefs.getInt(KEY_BPM, 130)
fun loadVolume(): Float = prefs.getFloat(KEY_VOLUME, 1f)
fun loadVolume(): Float = prefs.getFloat(KEY_VOLUME, 0.6f)
}

View file

@ -32,10 +32,9 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -106,6 +105,7 @@ fun PacerScreen(viewModel: PacerViewModel, modifier: Modifier = Modifier) {
}
val bpmColor = lerp(FG, ACCENT, bpmFlash.value)
val stepsPerSec = "%.1f steps/sec".format(bpm / 60.0)
Box(modifier = modifier.background(BG)) {
@ -127,6 +127,32 @@ fun PacerScreen(viewModel: PacerViewModel, modifier: Modifier = Modifier) {
letterSpacing = 5.sp
)
// BPM number + labels
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "$bpm",
fontSize = 80.sp,
fontWeight = FontWeight.Bold,
fontFamily = JetBrainsMono,
color = bpmColor,
letterSpacing = (-2).sp
)
Text(
text = "steps / min",
fontSize = 11.sp,
fontFamily = JetBrainsMono,
color = FG.copy(alpha = 0.35f),
letterSpacing = 2.sp
)
Text(
text = stepsPerSec,
fontSize = 11.sp,
fontFamily = JetBrainsMono,
color = FG.copy(alpha = 0.25f),
letterSpacing = 1.sp
)
}
// Pulse ring + ripple + center dot
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(80.dp)) {
Canvas(modifier = Modifier.fillMaxSize()) {
@ -149,54 +175,27 @@ fun PacerScreen(viewModel: PacerViewModel, modifier: Modifier = Modifier) {
)
}
// BPM number
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "$bpm",
fontSize = 80.sp,
fontWeight = FontWeight.Bold,
fontFamily = JetBrainsMono,
color = bpmColor,
letterSpacing = (-2).sp
)
Text(
text = "BPM",
fontSize = 11.sp,
fontFamily = JetBrainsMono,
color = FG.copy(alpha = 0.35f),
letterSpacing = 4.sp
)
}
// Start/Stop button
OutlinedButton(
onClick = { viewModel.togglePlayback() },
shape = RectangleShape,
border = BorderStroke(2.dp, if (isPlaying) ACCENT else FG),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = if (isPlaying) BG else FG,
containerColor = if (isPlaying) ACCENT else Color.Transparent
)
// BPM slider with SLOW / FAST labels + presets
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = if (isPlaying) "STOP" else "START",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
fontFamily = JetBrainsMono,
letterSpacing = 3.sp,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
)
}
// BPM slider + presets
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = "PACE",
text = "SLOW",
fontSize = 10.sp,
fontFamily = JetBrainsMono,
color = FG.copy(alpha = 0.35f),
letterSpacing = 3.sp
letterSpacing = 2.sp
)
Text(
text = "FAST",
fontSize = 10.sp,
fontFamily = JetBrainsMono,
color = FG.copy(alpha = 0.35f),
letterSpacing = 2.sp
)
}
Slider(
value = bpm.toFloat(),
onValueChange = { viewModel.updateBpm(it.toInt()) },
@ -245,13 +244,25 @@ fun PacerScreen(viewModel: PacerViewModel, modifier: Modifier = Modifier) {
// Volume slider
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "VOLUME ${(volume * 100).toInt()}%",
text = "VOL",
fontSize = 10.sp,
fontFamily = JetBrainsMono,
color = FG.copy(alpha = 0.35f),
letterSpacing = 3.sp
letterSpacing = 2.sp
)
Text(
text = "${(volume * 100).toInt()}%",
fontSize = 10.sp,
fontFamily = JetBrainsMono,
color = FG.copy(alpha = 0.35f),
letterSpacing = 2.sp
)
}
Slider(
value = volume,
onValueChange = { viewModel.updateVolume(it) },
@ -263,6 +274,27 @@ fun PacerScreen(viewModel: PacerViewModel, modifier: Modifier = Modifier) {
)
)
}
// Start/Stop button
OutlinedButton(
onClick = { viewModel.togglePlayback() },
modifier = Modifier.fillMaxWidth(),
shape = RectangleShape,
border = BorderStroke(2.dp, if (isPlaying) ACCENT else FG),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = if (isPlaying) BG else FG,
containerColor = if (isPlaying) ACCENT else Color.Transparent
)
) {
Text(
text = if (isPlaying) "STOP" else "START",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
fontFamily = JetBrainsMono,
letterSpacing = 3.sp,
modifier = Modifier.padding(vertical = 6.dp)
)
}
}
// Progress bar — fills linearly across one beat interval

View file

@ -26,8 +26,8 @@ class SettingsRepositoryTest {
prefs = mock()
whenever(prefs.edit()).thenReturn(editor)
whenever(prefs.getInt(eq(SettingsRepository.KEY_BPM), any())).thenReturn(120)
whenever(prefs.getFloat(eq(SettingsRepository.KEY_VOLUME), any())).thenReturn(1f)
whenever(prefs.getInt(eq(SettingsRepository.KEY_BPM), any())).thenReturn(130)
whenever(prefs.getFloat(eq(SettingsRepository.KEY_VOLUME), any())).thenReturn(0.6f)
context = mock()
whenever(context.getSharedPreferences(any(), any())).thenReturn(prefs)
@ -62,18 +62,18 @@ class SettingsRepositoryTest {
}
@Test
fun `loadBpm returns default 120 when nothing saved`() {
fun `loadBpm returns default 130 when nothing saved`() {
whenever(prefs.getInt(eq(SettingsRepository.KEY_BPM), any())).thenAnswer {
it.getArgument(1) as Int
}
assertEquals(120, repo.loadBpm())
assertEquals(130, repo.loadBpm())
}
@Test
fun `loadVolume returns default 1f when nothing saved`() {
fun `loadVolume returns default 0_6f when nothing saved`() {
whenever(prefs.getFloat(eq(SettingsRepository.KEY_VOLUME), any())).thenAnswer {
it.getArgument(1) as Float
}
assertEquals(1f, repo.loadVolume())
assertEquals(0.6f, repo.loadVolume())
}
}