155 lines
5.7 KiB
Kotlin
155 lines
5.7 KiB
Kotlin
|
|
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
|
||
|
|
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
|
||
|
|
import androidx.compose.material3.Icon
|
||
|
|
import androidx.compose.material3.IconButton
|
||
|
|
import androidx.compose.material3.MaterialTheme
|
||
|
|
import androidx.compose.material3.Slider
|
||
|
|
import androidx.compose.material3.Text
|
||
|
|
import androidx.compose.runtime.Composable
|
||
|
|
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
|
||
|
|
import androidx.compose.ui.text.style.TextOverflow
|
||
|
|
import androidx.compose.ui.tooling.preview.Preview
|
||
|
|
import androidx.compose.ui.unit.dp
|
||
|
|
|
||
|
|
data class NowPlayingUiState(
|
||
|
|
val title: String,
|
||
|
|
val artist: 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(
|
||
|
|
modifier = modifier
|
||
|
|
.fillMaxSize()
|
||
|
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||
|
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||
|
|
) {
|
||
|
|
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")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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(
|
||
|
|
state = NowPlayingUiState(
|
||
|
|
title = "Example Track Title",
|
||
|
|
artist = "Example Artist",
|
||
|
|
progress = progress,
|
||
|
|
durationSeconds = 215,
|
||
|
|
isPlaying = playing,
|
||
|
|
),
|
||
|
|
onProgressChange = { progress = it },
|
||
|
|
onPrevious = {},
|
||
|
|
onTogglePlayPause = { playing = !playing },
|
||
|
|
onNext = {},
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|