ui: Add landscape two-column layout (US-18)
Detect orientation via LocalConfiguration and switch to a Row-based two-column layout in landscape mode. Left column shows the title, BPM display and pulse ring; right column shows the BPM slider, presets, volume slider and the START/STOP button. All controls remain visible without scrolling. Portrait layout is unchanged. Extracted shared UI into private composables (Title, BpmDisplay, PulseRing, BpmControls, VolumeControl, StartStopButton) to avoid duplication between the two layout paths.
This commit is contained in:
parent
22fc569874
commit
d79fcebc41
2 changed files with 306 additions and 186 deletions
|
|
@ -68,6 +68,10 @@ US-15: As a user, I want an app icon and splash screen so that it feels native.
|
||||||
US-16: As a developer, I want a signed APK for installation so that the app can be deployed.
|
US-16: As a developer, I want a signed APK for installation so that the app can be deployed.
|
||||||
- Acceptance: Release build generated; offline functionality verified.
|
- Acceptance: Release build generated; offline functionality verified.
|
||||||
|
|
||||||
|
### Theme 8: Landscape Support
|
||||||
|
US-18: As a user, I want the app to work in landscape orientation so that I can use it however I hold my phone.
|
||||||
|
- Acceptance: In landscape, all controls are visible and usable without scrolling; layout switches to a two-column arrangement (pulse ring + BPM display on the left, sliders/buttons on the right); no content is clipped or overlapping.
|
||||||
|
|
||||||
## Additional Notes
|
## Additional Notes
|
||||||
- TDD: Focus on US-03, US-04, US-13 first—write failing tests, then implement.
|
- TDD: Focus on US-03, US-04, US-13 first—write failing tests, then implement.
|
||||||
- Priorities: Complete setup (US-01-02), then audio (US-03-05), UI (US-06-07), service (US-08-10), rest.
|
- Priorities: Complete setup (US-01-02), then audio (US-03-05), UI (US-06-07), service (US-08-10), rest.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package net.jeena.pacer.ui
|
package net.jeena.pacer.ui
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
|
@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -35,6 +37,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.graphics.lerp
|
import androidx.compose.ui.graphics.lerp
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
@ -104,197 +107,40 @@ fun PacerScreen(viewModel: PacerViewModel, modifier: Modifier = Modifier) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val bpmColor = lerp(FG, ACCENT, bpmFlash.value)
|
val bpmColor = lerp(FG, ACCENT, bpmFlash.value)
|
||||||
val stepsPerSec = "%.1f steps/sec".format(bpm / 60.0)
|
val stepsPerSec = "%.1f steps/sec".format(bpm / 60.0)
|
||||||
|
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
|
||||||
Box(modifier = modifier.background(BG)) {
|
Box(modifier = modifier.background(BG)) {
|
||||||
|
|
||||||
Column(
|
if (isLandscape) {
|
||||||
modifier = Modifier
|
LandscapeLayout(
|
||||||
.fillMaxSize()
|
isPlaying = isPlaying,
|
||||||
.padding(horizontal = 24.dp)
|
bpm = bpm,
|
||||||
.padding(top = 64.dp, bottom = 48.dp),
|
volume = volume,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
bpmColor = bpmColor,
|
||||||
verticalArrangement = Arrangement.SpaceBetween
|
stepsPerSec = stepsPerSec,
|
||||||
) {
|
rippleScale = rippleScale.value,
|
||||||
// Title
|
rippleAlpha = rippleAlpha.value,
|
||||||
Text(
|
dotScale = dotScale.value,
|
||||||
text = "PACER",
|
onBpmChange = { viewModel.updateBpm(it) },
|
||||||
fontSize = 28.sp,
|
onVolChange = { viewModel.updateVolume(it) },
|
||||||
fontWeight = FontWeight.Bold,
|
onToggle = { viewModel.togglePlayback() }
|
||||||
fontFamily = JetBrainsMono,
|
)
|
||||||
color = ACCENT,
|
} else {
|
||||||
letterSpacing = 5.sp
|
PortraitLayout(
|
||||||
|
isPlaying = isPlaying,
|
||||||
|
bpm = bpm,
|
||||||
|
volume = volume,
|
||||||
|
bpmColor = bpmColor,
|
||||||
|
stepsPerSec = stepsPerSec,
|
||||||
|
rippleScale = rippleScale.value,
|
||||||
|
rippleAlpha = rippleAlpha.value,
|
||||||
|
dotScale = dotScale.value,
|
||||||
|
onBpmChange = { viewModel.updateBpm(it) },
|
||||||
|
onVolChange = { viewModel.updateVolume(it) },
|
||||||
|
onToggle = { viewModel.togglePlayback() }
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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 = 13.sp,
|
|
||||||
fontFamily = JetBrainsMono,
|
|
||||||
color = FG.copy(alpha = 0.65f),
|
|
||||||
letterSpacing = 2.sp
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stepsPerSec.uppercase(),
|
|
||||||
fontSize = 13.sp,
|
|
||||||
fontFamily = JetBrainsMono,
|
|
||||||
color = FG.copy(alpha = 0.5f),
|
|
||||||
letterSpacing = 1.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pulse ring + ripple + center dot
|
|
||||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(80.dp)) {
|
|
||||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
|
||||||
drawCircle(
|
|
||||||
color = ACCENT.copy(alpha = rippleAlpha.value),
|
|
||||||
radius = (size.minDimension / 2f) * rippleScale.value,
|
|
||||||
style = Stroke(width = 2.dp.toPx())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(50.dp)
|
|
||||||
.border(2.dp, ACCENT, CircleShape)
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(12.dp)
|
|
||||||
.scale(dotScale.value)
|
|
||||||
.background(ACCENT, CircleShape)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BPM slider with SLOW / FAST labels + presets
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "SLOW",
|
|
||||||
fontSize = 13.sp,
|
|
||||||
fontFamily = JetBrainsMono,
|
|
||||||
color = FG.copy(alpha = 0.65f),
|
|
||||||
letterSpacing = 2.sp
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "FAST",
|
|
||||||
fontSize = 13.sp,
|
|
||||||
fontFamily = JetBrainsMono,
|
|
||||||
color = FG.copy(alpha = 0.65f),
|
|
||||||
letterSpacing = 2.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Slider(
|
|
||||||
value = bpm.toFloat(),
|
|
||||||
onValueChange = { viewModel.updateBpm(it.toInt()) },
|
|
||||||
valueRange = 40f..200f,
|
|
||||||
colors = SliderDefaults.colors(
|
|
||||||
thumbColor = ACCENT,
|
|
||||||
activeTrackColor = ACCENT,
|
|
||||||
inactiveTrackColor = DIM
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
PRESETS.forEach { (preset, label) ->
|
|
||||||
val active = bpm == preset
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = { viewModel.updateBpm(preset) },
|
|
||||||
modifier = Modifier.size(width = 72.dp, height = 48.dp),
|
|
||||||
shape = RectangleShape,
|
|
||||||
contentPadding = PaddingValues(0.dp),
|
|
||||||
border = BorderStroke(1.dp, if (active) ACCENT else DIM),
|
|
||||||
colors = ButtonDefaults.outlinedButtonColors(
|
|
||||||
contentColor = if (active) BG else FG.copy(alpha = 0.6f),
|
|
||||||
containerColor = if (active) ACCENT else Color.Transparent
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Text(
|
|
||||||
text = "$preset",
|
|
||||||
fontSize = 13.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontFamily = JetBrainsMono
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = label.uppercase(),
|
|
||||||
fontSize = 10.sp,
|
|
||||||
fontFamily = JetBrainsMono,
|
|
||||||
letterSpacing = 1.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Volume slider
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "VOL",
|
|
||||||
fontSize = 13.sp,
|
|
||||||
fontFamily = JetBrainsMono,
|
|
||||||
color = FG.copy(alpha = 0.65f),
|
|
||||||
letterSpacing = 2.sp
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "${(volume * 100).toInt()}%",
|
|
||||||
fontSize = 13.sp,
|
|
||||||
fontFamily = JetBrainsMono,
|
|
||||||
color = FG.copy(alpha = 0.65f),
|
|
||||||
letterSpacing = 2.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Slider(
|
|
||||||
value = volume,
|
|
||||||
onValueChange = { viewModel.updateVolume(it) },
|
|
||||||
valueRange = 0f..1f,
|
|
||||||
colors = SliderDefaults.colors(
|
|
||||||
thumbColor = ACCENT,
|
|
||||||
activeTrackColor = ACCENT,
|
|
||||||
inactiveTrackColor = DIM
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
// Progress bar — fills linearly across one beat interval
|
||||||
|
|
@ -321,3 +167,273 @@ fun PacerScreen(viewModel: PacerViewModel, modifier: Modifier = Modifier) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PortraitLayout(
|
||||||
|
isPlaying: Boolean,
|
||||||
|
bpm: Int,
|
||||||
|
volume: Float,
|
||||||
|
bpmColor: Color,
|
||||||
|
stepsPerSec: String,
|
||||||
|
rippleScale: Float,
|
||||||
|
rippleAlpha: Float,
|
||||||
|
dotScale: Float,
|
||||||
|
onBpmChange: (Int) -> Unit,
|
||||||
|
onVolChange: (Float) -> Unit,
|
||||||
|
onToggle: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 24.dp)
|
||||||
|
.padding(top = 64.dp, bottom = 48.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Title()
|
||||||
|
BpmDisplay(bpm, bpmColor, stepsPerSec)
|
||||||
|
PulseRing(rippleScale, rippleAlpha, dotScale, size = 80.dp)
|
||||||
|
BpmControls(bpm, onBpmChange)
|
||||||
|
VolumeControl(volume, onVolChange)
|
||||||
|
StartStopButton(isPlaying, onToggle, Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LandscapeLayout(
|
||||||
|
isPlaying: Boolean,
|
||||||
|
bpm: Int,
|
||||||
|
volume: Float,
|
||||||
|
bpmColor: Color,
|
||||||
|
stepsPerSec: String,
|
||||||
|
rippleScale: Float,
|
||||||
|
rippleAlpha: Float,
|
||||||
|
dotScale: Float,
|
||||||
|
onBpmChange: (Int) -> Unit,
|
||||||
|
onVolChange: (Float) -> Unit,
|
||||||
|
onToggle: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(24.dp)
|
||||||
|
) {
|
||||||
|
// Left column: title + BPM + pulse ring
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
Title()
|
||||||
|
BpmDisplay(bpm, bpmColor, stepsPerSec)
|
||||||
|
PulseRing(rippleScale, rippleAlpha, dotScale, size = 64.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right column: sliders + presets + start/stop
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
BpmControls(bpm, onBpmChange)
|
||||||
|
VolumeControl(volume, onVolChange)
|
||||||
|
StartStopButton(isPlaying, onToggle, Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Title() {
|
||||||
|
Text(
|
||||||
|
text = "PACER",
|
||||||
|
fontSize = 28.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontFamily = JetBrainsMono,
|
||||||
|
color = ACCENT,
|
||||||
|
letterSpacing = 5.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BpmDisplay(bpm: Int, bpmColor: Color, stepsPerSec: String) {
|
||||||
|
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 = 13.sp,
|
||||||
|
fontFamily = JetBrainsMono,
|
||||||
|
color = FG.copy(alpha = 0.65f),
|
||||||
|
letterSpacing = 2.sp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stepsPerSec.uppercase(),
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontFamily = JetBrainsMono,
|
||||||
|
color = FG.copy(alpha = 0.5f),
|
||||||
|
letterSpacing = 1.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PulseRing(rippleScale: Float, rippleAlpha: Float, dotScale: Float, size: androidx.compose.ui.unit.Dp) {
|
||||||
|
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(size)) {
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
drawCircle(
|
||||||
|
color = ACCENT.copy(alpha = rippleAlpha),
|
||||||
|
radius = (this.size.minDimension / 2f) * rippleScale,
|
||||||
|
style = Stroke(width = 2.dp.toPx())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(size * 0.625f)
|
||||||
|
.border(2.dp, ACCENT, CircleShape)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(size * 0.15f)
|
||||||
|
.scale(dotScale)
|
||||||
|
.background(ACCENT, CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BpmControls(bpm: Int, onBpmChange: (Int) -> Unit) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "SLOW",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontFamily = JetBrainsMono,
|
||||||
|
color = FG.copy(alpha = 0.65f),
|
||||||
|
letterSpacing = 2.sp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "FAST",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontFamily = JetBrainsMono,
|
||||||
|
color = FG.copy(alpha = 0.65f),
|
||||||
|
letterSpacing = 2.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Slider(
|
||||||
|
value = bpm.toFloat(),
|
||||||
|
onValueChange = { onBpmChange(it.toInt()) },
|
||||||
|
valueRange = 40f..200f,
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = ACCENT,
|
||||||
|
activeTrackColor = ACCENT,
|
||||||
|
inactiveTrackColor = DIM
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
PRESETS.forEach { (preset, label) ->
|
||||||
|
val active = bpm == preset
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { onBpmChange(preset) },
|
||||||
|
modifier = Modifier.size(width = 72.dp, height = 48.dp),
|
||||||
|
shape = RectangleShape,
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
border = BorderStroke(1.dp, if (active) ACCENT else DIM),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = if (active) BG else FG.copy(alpha = 0.6f),
|
||||||
|
containerColor = if (active) ACCENT else Color.Transparent
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text(
|
||||||
|
text = "$preset",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontFamily = JetBrainsMono
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = label.uppercase(),
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontFamily = JetBrainsMono,
|
||||||
|
letterSpacing = 1.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VolumeControl(volume: Float, onVolChange: (Float) -> Unit) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "VOL",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontFamily = JetBrainsMono,
|
||||||
|
color = FG.copy(alpha = 0.65f),
|
||||||
|
letterSpacing = 2.sp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${(volume * 100).toInt()}%",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontFamily = JetBrainsMono,
|
||||||
|
color = FG.copy(alpha = 0.65f),
|
||||||
|
letterSpacing = 2.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Slider(
|
||||||
|
value = volume,
|
||||||
|
onValueChange = { onVolChange(it) },
|
||||||
|
valueRange = 0f..1f,
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = ACCENT,
|
||||||
|
activeTrackColor = ACCENT,
|
||||||
|
inactiveTrackColor = DIM
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StartStopButton(isPlaying: Boolean, onToggle: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onToggle,
|
||||||
|
modifier = modifier,
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue