new: add playback state handling across platforms and integrate main play action in PlayerScreen

This commit is contained in:
2026-06-17 17:46:12 +02:00
parent 1fc8fbbc0d
commit 51a9f2f80d
11 changed files with 223 additions and 8 deletions
@@ -1,26 +1,35 @@
package fr.ajaury.gwenedeg.player.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import fr.ajaury.gwenedeg.player.PlaybackState
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerUiState
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerViewModel
import org.jetbrains.compose.resources.ExperimentalResourceApi
import fr.ajaury.gwenedeg.theme.GwenedegTheme
import org.koin.compose.viewmodel.koinViewModel
@OptIn(ExperimentalResourceApi::class)
@Composable
fun PlayerScreen(
recordTitle: String,
viewModel: PlayerViewModel = koinViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
DisposableEffect(Unit) {
viewModel.play(filePath = "files/demat.ogg")
onDispose {
@@ -28,6 +37,19 @@ fun PlayerScreen(
}
}
PlayerScreen(
recordTitle = recordTitle,
uiState = uiState,
onMainPlayActionButtonClicked = viewModel::performMainPlayAction,
)
}
@Composable
fun PlayerScreen(
recordTitle: String,
uiState: PlayerUiState,
onMainPlayActionButtonClicked: () -> Unit,
) {
Scaffold { innerPadding ->
Box(
modifier = Modifier
@@ -35,12 +57,62 @@ fun PlayerScreen(
.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
Text(
text = "Playing: $recordTitle",
style = MaterialTheme.typography.headlineMedium,
)
Button(onClick = onMainPlayActionButtonClicked) {
Text(text = uiState.playbackState.toActionLabel())
}
}
}
}
}
private fun PlaybackState.toActionLabel(): String =
when (this) {
PlaybackState.PLAYING -> "Pause"
PlaybackState.ENDED -> "Replay"
PlaybackState.PAUSED, PlaybackState.IDLE -> "Play"
}
@Preview
@Composable
private fun PlayerScreenPlayingPreview() {
GwenedegTheme {
PlayerScreen(
recordTitle = "Demat",
uiState = PlayerUiState(playbackState = PlaybackState.PLAYING),
onMainPlayActionButtonClicked = {},
)
}
}
@Preview
@Composable
private fun PlayerScreenPausedPreview() {
GwenedegTheme {
PlayerScreen(
recordTitle = "Demat",
uiState = PlayerUiState(playbackState = PlaybackState.PAUSED),
onMainPlayActionButtonClicked = {},
)
}
}
@Preview
@Composable
private fun PlayerScreenEndedPreview() {
GwenedegTheme {
PlayerScreen(
recordTitle = "Demat",
uiState = PlayerUiState(playbackState = PlaybackState.ENDED),
onMainPlayActionButtonClicked = {},
)
}
}
@@ -0,0 +1,7 @@
package fr.ajaury.gwenedeg.player.ui.viewmodel
import fr.ajaury.gwenedeg.player.PlaybackState
data class PlayerUiState(
val playbackState: PlaybackState = PlaybackState.IDLE,
)
@@ -1,25 +1,48 @@
package fr.ajaury.gwenedeg.player.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import fr.ajaury.gwenedeg.player.AudioPlayer
import fr.ajaury.gwenedeg.player.AudioSessionManager
import fr.ajaury.gwenedeg.player.PlaybackState
import gwenedeg.shared.generated.resources.Res
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlin.time.Duration.Companion.seconds
class PlayerViewModel(
private val audioPlayer: AudioPlayer,
private val audioSessionManager: AudioSessionManager,
) : ViewModel() {
private var filePath: String? = null
val uiState: StateFlow<PlayerUiState> = audioPlayer.playbackState
.map { PlayerUiState(playbackState = it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5.seconds),
initialValue = PlayerUiState(),
)
init {
audioSessionManager.activate()
}
fun play(filePath: String) {
this.filePath = filePath
audioPlayer.load(uri = Res.getUri(filePath))
audioPlayer.play()
}
fun pause() {
audioPlayer.pause()
fun performMainPlayAction() {
when (uiState.value.playbackState) {
PlaybackState.PLAYING -> audioPlayer.pause()
PlaybackState.ENDED -> filePath?.let { play(it) }
PlaybackState.PAUSED, PlaybackState.IDLE -> audioPlayer.play()
}
}
fun stop() {