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:
parent
12f38c19ab
commit
dc1d05b1a7
3 changed files with 96 additions and 64 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
) {
|
||||
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
|
||||
// BPM slider with SLOW / FAST labels + presets
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "PACE",
|
||||
fontSize = 10.sp,
|
||||
fontFamily = JetBrainsMono,
|
||||
color = FG.copy(alpha = 0.35f),
|
||||
letterSpacing = 3.sp
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "SLOW",
|
||||
fontSize = 10.sp,
|
||||
fontFamily = JetBrainsMono,
|
||||
color = FG.copy(alpha = 0.35f),
|
||||
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()) {
|
||||
Text(
|
||||
text = "VOLUME ${(volume * 100).toInt()}%",
|
||||
fontSize = 10.sp,
|
||||
fontFamily = JetBrainsMono,
|
||||
color = FG.copy(alpha = 0.35f),
|
||||
letterSpacing = 3.sp
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "VOL",
|
||||
fontSize = 10.sp,
|
||||
fontFamily = JetBrainsMono,
|
||||
color = FG.copy(alpha = 0.35f),
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue