mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-07 01:08:51 +00:00
Add shared player controls boundary
This commit is contained in:
parent
42f905af62
commit
fa4adcc5d8
4 changed files with 173 additions and 0 deletions
|
|
@ -0,0 +1,54 @@
|
|||
package com.fluxa.app.player
|
||||
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import com.fluxa.app.shared.feature.player.PlaybackController
|
||||
import com.fluxa.app.shared.feature.player.PlayerAction
|
||||
import com.fluxa.app.shared.feature.player.PlayerContentUiModel
|
||||
import com.fluxa.app.shared.feature.player.PlayerUiState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class AndroidPlaybackController(
|
||||
private val player: ExoPlayer,
|
||||
private val content: () -> PlayerContentUiModel?
|
||||
) : PlaybackController, Player.Listener {
|
||||
private val mutableState = MutableStateFlow(snapshot())
|
||||
override val state: StateFlow<PlayerUiState> = mutableState.asStateFlow()
|
||||
|
||||
init {
|
||||
player.addListener(this)
|
||||
}
|
||||
|
||||
override suspend fun dispatch(action: PlayerAction) {
|
||||
when (action) {
|
||||
PlayerAction.PlayPause -> if (player.isPlaying) player.pause() else player.play()
|
||||
is PlayerAction.SeekTo -> player.seekTo(action.positionMs)
|
||||
is PlayerAction.SetVolume -> player.volume = action.volume.coerceIn(0f, 1f)
|
||||
is PlayerAction.SetSubtitleEnabled -> Unit
|
||||
PlayerAction.Stop -> player.stop()
|
||||
}
|
||||
mutableState.value = snapshot()
|
||||
}
|
||||
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
mutableState.value = snapshot()
|
||||
}
|
||||
|
||||
fun close() {
|
||||
player.removeListener(this)
|
||||
}
|
||||
|
||||
private fun snapshot(): PlayerUiState {
|
||||
return PlayerUiState(
|
||||
content = content(),
|
||||
isPlaying = player.isPlaying,
|
||||
isBuffering = player.playbackState == Player.STATE_BUFFERING,
|
||||
positionMs = player.currentPosition.coerceAtLeast(0L),
|
||||
durationMs = player.duration.coerceAtLeast(0L),
|
||||
bufferedPositionMs = player.bufferedPosition.coerceAtLeast(0L),
|
||||
volume = player.volume
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.fluxa.app.shared.feature.player
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
data class PlayerContentUiModel(
|
||||
val id: String,
|
||||
val type: String,
|
||||
val title: String,
|
||||
val subtitle: String,
|
||||
val logoUrl: String?,
|
||||
val backgroundUrl: String?,
|
||||
val streamLabel: String
|
||||
)
|
||||
|
||||
data class PlayerUiState(
|
||||
val content: PlayerContentUiModel? = null,
|
||||
val isPlaying: Boolean = false,
|
||||
val isBuffering: Boolean = false,
|
||||
val positionMs: Long = 0L,
|
||||
val durationMs: Long = 0L,
|
||||
val bufferedPositionMs: Long = 0L,
|
||||
val volume: Float = 1f,
|
||||
val subtitleEnabled: Boolean = false,
|
||||
val errorKey: String? = null
|
||||
)
|
||||
|
||||
sealed interface PlayerAction {
|
||||
data object PlayPause : PlayerAction
|
||||
data class SeekTo(val positionMs: Long) : PlayerAction
|
||||
data class SetVolume(val volume: Float) : PlayerAction
|
||||
data class SetSubtitleEnabled(val enabled: Boolean) : PlayerAction
|
||||
data object Stop : PlayerAction
|
||||
}
|
||||
|
||||
interface PlaybackController {
|
||||
val state: StateFlow<PlayerUiState>
|
||||
suspend fun dispatch(action: PlayerAction)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.fluxa.app.shared.feature.player
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.fluxa.app.common.AppStrings
|
||||
import com.fluxa.app.ui.catalog.FluxaColors
|
||||
|
||||
@Composable
|
||||
fun PlayerScreen(
|
||||
state: PlayerUiState,
|
||||
language: String?,
|
||||
onAction: (PlayerAction) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
) {
|
||||
if (state.isBuffering) {
|
||||
CircularProgressIndicator(
|
||||
color = Color.White,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
state.content?.let { content ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.background(FluxaColors.background.copy(alpha = 0.86f))
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(text = content.title, color = Color.White, fontWeight = FontWeight.Bold)
|
||||
if (content.subtitle.isNotBlank()) {
|
||||
Text(text = content.subtitle, color = Color.White.copy(alpha = 0.72f))
|
||||
}
|
||||
if (content.streamLabel.isNotBlank()) {
|
||||
Text(text = content.streamLabel, color = Color.White.copy(alpha = 0.72f))
|
||||
}
|
||||
Slider(
|
||||
value = state.positionMs.toFloat(),
|
||||
onValueChange = { onAction(PlayerAction.SeekTo(it.toLong())) },
|
||||
valueRange = 0f..state.durationMs.coerceAtLeast(1L).toFloat()
|
||||
)
|
||||
Button(onClick = { onAction(PlayerAction.PlayPause) }) {
|
||||
Text(AppStrings.t(language, if (state.isPlaying) "common.pause" else "common.play"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.fluxa.app.shared.feature.player
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class PlayerStore(
|
||||
private val controller: PlaybackController
|
||||
) {
|
||||
val state: StateFlow<PlayerUiState> = controller.state
|
||||
|
||||
suspend fun dispatch(action: PlayerAction) {
|
||||
controller.dispatch(action)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue