From dc1d05b1a790233c307c4ac7820ef05f752ab6f0 Mon Sep 17 00:00:00 2001 From: Jeena Date: Mon, 9 Mar 2026 12:09:44 +0000 Subject: [PATCH] feat(US-06): match original layout and labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../net/jeena/pacer/SettingsRepository.kt | 4 +- .../kotlin/net/jeena/pacer/ui/PacerScreen.kt | 144 +++++++++++------- .../net/jeena/pacer/SettingsRepositoryTest.kt | 12 +- 3 files changed, 96 insertions(+), 64 deletions(-) diff --git a/app/src/main/kotlin/net/jeena/pacer/SettingsRepository.kt b/app/src/main/kotlin/net/jeena/pacer/SettingsRepository.kt index 6fc21e2..58d78eb 100644 --- a/app/src/main/kotlin/net/jeena/pacer/SettingsRepository.kt +++ b/app/src/main/kotlin/net/jeena/pacer/SettingsRepository.kt @@ -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) } diff --git a/app/src/main/kotlin/net/jeena/pacer/ui/PacerScreen.kt b/app/src/main/kotlin/net/jeena/pacer/ui/PacerScreen.kt index 1cd7f03..97f7fe1 100644 --- a/app/src/main/kotlin/net/jeena/pacer/ui/PacerScreen.kt +++ b/app/src/main/kotlin/net/jeena/pacer/ui/PacerScreen.kt @@ -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 diff --git a/app/src/test/kotlin/net/jeena/pacer/SettingsRepositoryTest.kt b/app/src/test/kotlin/net/jeena/pacer/SettingsRepositoryTest.kt index fdbe464..7b7e03f 100644 --- a/app/src/test/kotlin/net/jeena/pacer/SettingsRepositoryTest.kt +++ b/app/src/test/kotlin/net/jeena/pacer/SettingsRepositoryTest.kt @@ -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()) } }