Files
lockstep-player/app/src/main/java/at/lockstep/player/ui/NowPlayingScreen.kt

245 lines
8.9 KiB
Kotlin
Raw Normal View History

package at.lockstep.player.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
2026-05-14 02:43:49 +02:00
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.SkipPrevious
2026-05-14 02:43:49 +02:00
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
2026-05-14 02:43:49 +02:00
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
2026-05-14 02:43:49 +02:00
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
2026-05-14 02:43:49 +02:00
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
2026-05-14 02:43:49 +02:00
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
2026-05-14 02:43:49 +02:00
import at.lockstep.player.playback.PlaybackService
import at.lockstep.player.R
data class NowPlayingUiState(
val title: String,
val artist: String,
2026-05-14 02:43:49 +02:00
/** Placeholder until live cadence / JNI wiring; MVP shows "--". */
val stepFrequencyDisplay: String,
/** 0f..1f until real timing is wired to Oboe / JNI. */
val progress: Float,
/** Total track length in seconds (placeholder for UI formatting). */
val durationSeconds: Int,
val isPlaying: Boolean,
)
@Composable
fun NowPlayingScreen(
state: NowPlayingUiState,
onProgressChange: (Float) -> Unit,
onPrevious: () -> Unit,
onTogglePlayPause: () -> Unit,
onNext: () -> Unit,
modifier: Modifier = Modifier,
) {
val elapsed = (state.progress * state.durationSeconds).toInt().coerceIn(0, state.durationSeconds)
Column(
2026-05-14 02:43:49 +02:00
modifier =
modifier
.fillMaxSize()
.padding(horizontal = 24.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
2026-05-14 02:43:49 +02:00
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = LocalContext.current.getString(R.string.now_playing_step_frequency_label),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
)
Text(
text = state.stepFrequencyDisplay,
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = state.title,
style = MaterialTheme.typography.headlineSmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
Text(
text = state.artist,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Slider(
value = state.progress,
onValueChange = onProgressChange,
modifier = Modifier.fillMaxWidth(),
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = formatMmSs(elapsed),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = formatMmSs(state.durationSeconds),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(onClick = onPrevious, modifier = Modifier.size(80.dp)) {
Icon(Icons.Default.SkipPrevious, contentDescription = "Previous track")
}
IconButton(onClick = onTogglePlayPause, modifier = Modifier.size(104.dp)) {
Icon(
imageVector = if (state.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
contentDescription = if (state.isPlaying) "Pause" else "Play",
modifier = Modifier.size(56.dp),
)
}
IconButton(onClick = onNext, modifier = Modifier.size(80.dp)) {
Icon(Icons.Default.SkipNext, contentDescription = "Next track")
}
}
}
}
}
2026-05-14 02:43:49 +02:00
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NowPlayingRoute(
playlistId: String,
playback: PlaybackService?,
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
var ui by remember {
mutableStateOf(
NowPlayingUiState(
title = context.getString(R.string.now_playing_idle_title),
artist = context.getString(R.string.now_playing_idle_artist),
stepFrequencyDisplay = context.getString(R.string.step_frequency_placeholder),
progress = 0f,
durationSeconds = 180,
isPlaying = false,
),
)
}
LaunchedEffect(playback) {
val service = playback ?: return@LaunchedEffect
service.uiState.collect { p ->
ui =
NowPlayingUiState(
title = p.title,
artist = p.artist,
stepFrequencyDisplay = context.getString(R.string.step_frequency_placeholder),
progress = p.progress,
durationSeconds = p.durationSeconds,
isPlaying = p.isPlaying,
)
}
}
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text(text = context.getString(R.string.now_playing_title)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
},
)
},
) { padding ->
NowPlayingScreen(
state = ui,
onProgressChange = { fraction ->
playback?.requestSeek(fraction)
ui = ui.copy(progress = fraction)
},
onPrevious = { playback?.requestSkipPrevious() },
onTogglePlayPause = { playback?.requestTogglePause() },
onNext = { playback?.requestSkipNext() },
modifier = Modifier.padding(padding),
)
}
}
private fun formatMmSs(totalSeconds: Int): String {
val s = totalSeconds.coerceAtLeast(0)
val m = s / 60
val r = s % 60
return "%d:%02d".format(m, r)
}
@Preview(showBackground = true)
@Composable
private fun NowPlayingScreenPreview() {
MaterialTheme {
var progress by remember { mutableFloatStateOf(0.35f) }
var playing by remember { mutableStateOf(true) }
NowPlayingScreen(
2026-05-14 02:43:49 +02:00
state =
NowPlayingUiState(
title = "Example Track Title",
artist = "Example Artist",
stepFrequencyDisplay = "--",
progress = progress,
durationSeconds = 215,
isPlaying = playing,
),
onProgressChange = { progress = it },
onPrevious = {},
onTogglePlayPause = { playing = !playing },
onNext = {},
)
}
}