feat: Now Playing screen MVP (nb. layout xml and Compose UI are two different sources)
This commit is contained in:
154
app/src/main/java/at/lockstep/player/ui/NowPlayingScreen.kt
Normal file
154
app/src/main/java/at/lockstep/player/ui/NowPlayingScreen.kt
Normal file
@@ -0,0 +1,154 @@
|
||||
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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user