Convert PlayerState to a data class
This commit is contained in:
@@ -31,7 +31,6 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.datasource.cache.Cache
|
||||
import androidx.media3.datasource.cache.CacheSpan
|
||||
@@ -53,7 +52,6 @@ import it.vfsfitvnm.youtubemusic.YouTube
|
||||
import it.vfsfitvnm.youtubemusic.models.PlayerResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -78,6 +76,7 @@ fun PlayerView(
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
player ?: return
|
||||
playerState?.mediaItem ?: return
|
||||
|
||||
val smallThumbnailSize = remember {
|
||||
@@ -156,9 +155,9 @@ fun PlayerView(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
if (playerState.playbackState == Player.STATE_IDLE) {
|
||||
player?.prepare()
|
||||
player.prepare()
|
||||
}
|
||||
player?.play()
|
||||
player.play()
|
||||
}
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
@@ -169,9 +168,7 @@ fun PlayerView(
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
player?.pause()
|
||||
}
|
||||
.clickable(onClick = player::pause)
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.size(24.dp)
|
||||
@@ -181,11 +178,8 @@ fun PlayerView(
|
||||
}
|
||||
}
|
||||
) {
|
||||
val song by remember(playerState.mediaItem?.mediaId) {
|
||||
playerState.mediaItem?.mediaId?.let(Database::song)?.distinctUntilChanged()
|
||||
?: flowOf(
|
||||
null
|
||||
)
|
||||
val song by remember(playerState.mediaItem.mediaId) {
|
||||
playerState.mediaItem.mediaId.let(Database::song).distinctUntilChanged()
|
||||
}.collectAsState(initial = null, context = Dispatchers.IO)
|
||||
|
||||
var isShowingStatsForNerds by rememberSaveable {
|
||||
@@ -218,7 +212,7 @@ fun PlayerView(
|
||||
.clickable {
|
||||
menuState.display {
|
||||
QueuedMediaItemMenu(
|
||||
mediaItem = playerState.mediaItem ?: MediaItem.EMPTY,
|
||||
mediaItem = playerState.mediaItem,
|
||||
indexInQueue = null,
|
||||
onDismiss = menuState::hide,
|
||||
onGlobalRouteEmitted = layoutState.collapse
|
||||
@@ -247,7 +241,7 @@ fun PlayerView(
|
||||
.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
val artworkUri = remember(it) {
|
||||
player?.getMediaItemAt(it)?.mediaMetadata?.artworkUri.thumbnail(
|
||||
player.getMediaItemAt(it).mediaMetadata.artworkUri.thumbnail(
|
||||
thumbnailSizePx
|
||||
)
|
||||
}
|
||||
@@ -281,7 +275,7 @@ fun PlayerView(
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
var cachedBytes by remember(song?.id) {
|
||||
mutableStateOf(binder?.cache?.getCachedBytes(song?.id ?: "", 0, -1) ?: 0L)
|
||||
mutableStateOf(binder.cache.getCachedBytes(playerState.mediaItem.mediaId, 0, -1))
|
||||
}
|
||||
|
||||
val loudnessDb by remember {
|
||||
@@ -297,6 +291,8 @@ fun PlayerView(
|
||||
}
|
||||
|
||||
DisposableEffect(song?.id) {
|
||||
val key = playerState.mediaItem.mediaId
|
||||
|
||||
val listener = object : Cache.Listener {
|
||||
override fun onSpanAdded(cache: Cache, span: CacheSpan) {
|
||||
cachedBytes += span.length
|
||||
@@ -313,15 +309,10 @@ fun PlayerView(
|
||||
) = Unit
|
||||
}
|
||||
|
||||
song?.id?.let { key ->
|
||||
binder?.cache?.addListener(key, listener)
|
||||
}
|
||||
|
||||
binder.cache.addListener(key, listener)
|
||||
|
||||
onDispose {
|
||||
song?.id?.let { key ->
|
||||
binder?.cache?.removeListener(key, listener)
|
||||
}
|
||||
binder.cache.removeListener(key, listener)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +359,7 @@ fun PlayerView(
|
||||
|
||||
Column {
|
||||
BasicText(
|
||||
text = playerState.mediaItem?.mediaId ?: "Unknown",
|
||||
text = playerState.mediaItem.mediaId,
|
||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||
)
|
||||
BasicText(
|
||||
@@ -451,11 +442,10 @@ fun PlayerView(
|
||||
.size(thumbnailSizeDp)
|
||||
) {
|
||||
LoadingOrError(
|
||||
errorMessage = playerState.error?.javaClass?.canonicalName,
|
||||
errorMessage = playerState.error.javaClass.canonicalName,
|
||||
onRetry = {
|
||||
player?.playWhenReady = true
|
||||
player?.prepare()
|
||||
playerState.error = null
|
||||
player.playWhenReady = true
|
||||
player.prepare()
|
||||
}
|
||||
) {}
|
||||
}
|
||||
@@ -494,10 +484,7 @@ fun PlayerView(
|
||||
}
|
||||
},
|
||||
onDragEnd = {
|
||||
scrubbingPosition?.let { scrubbingPosition ->
|
||||
player?.seekTo(scrubbingPosition)
|
||||
playerState.currentPosition = scrubbingPosition
|
||||
}
|
||||
scrubbingPosition?.let(player::seekTo)
|
||||
scrubbingPosition = null
|
||||
},
|
||||
color = colorPalette.text,
|
||||
@@ -553,9 +540,7 @@ fun PlayerView(
|
||||
query {
|
||||
song?.let { song ->
|
||||
Database.update(song.toggleLike())
|
||||
} ?: playerState.mediaItem?.let { mediaItem ->
|
||||
Database.insert(mediaItem, Song::toggleLike)
|
||||
}
|
||||
} ?: Database.insert(playerState.mediaItem, Song::toggleLike)
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 16.dp)
|
||||
@@ -567,9 +552,7 @@ fun PlayerView(
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
player?.seekToPrevious()
|
||||
}
|
||||
.clickable(onClick = player::seekToPrevious)
|
||||
.padding(horizontal = 16.dp)
|
||||
.size(32.dp)
|
||||
)
|
||||
@@ -581,11 +564,11 @@ fun PlayerView(
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
if (player?.playbackState == Player.STATE_IDLE) {
|
||||
if (player.playbackState == Player.STATE_IDLE) {
|
||||
player.prepare()
|
||||
}
|
||||
|
||||
player?.play()
|
||||
player.play()
|
||||
}
|
||||
.size(64.dp)
|
||||
)
|
||||
@@ -594,9 +577,7 @@ fun PlayerView(
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
player?.pause()
|
||||
}
|
||||
.clickable(onClick = player::pause)
|
||||
.size(64.dp)
|
||||
)
|
||||
}
|
||||
@@ -606,14 +587,11 @@ fun PlayerView(
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
player?.seekToNext()
|
||||
}
|
||||
.clickable(onClick = player::seekToNext)
|
||||
.padding(horizontal = 16.dp)
|
||||
.size(32.dp)
|
||||
)
|
||||
|
||||
|
||||
Image(
|
||||
painter = painterResource(
|
||||
if (playerState.repeatMode == Player.REPEAT_MODE_ONE) {
|
||||
@@ -632,10 +610,10 @@ fun PlayerView(
|
||||
),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
player?.repeatMode
|
||||
?.plus(2)
|
||||
?.mod(3)
|
||||
?.let { repeatMode ->
|
||||
player.repeatMode
|
||||
.plus(2)
|
||||
.mod(3)
|
||||
.let { repeatMode ->
|
||||
player.repeatMode = repeatMode
|
||||
preferences.repeatMode = repeatMode
|
||||
}
|
||||
|
||||
@@ -6,107 +6,131 @@ import androidx.compose.runtime.*
|
||||
import androidx.media3.common.*
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
|
||||
@Stable
|
||||
class PlayerState(private val player: Player) : Player.Listener, Runnable {
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
var currentPosition by mutableStateOf(player.currentPosition)
|
||||
|
||||
var duration by mutableStateOf(player.duration)
|
||||
private set
|
||||
data class PlayerState(
|
||||
val currentPosition: Long,
|
||||
val duration: Long,
|
||||
val playbackState: Int,
|
||||
val mediaItemIndex: Int,
|
||||
val mediaItem: MediaItem?,
|
||||
val mediaMetadata: MediaMetadata,
|
||||
val playWhenReady: Boolean,
|
||||
val repeatMode: Int,
|
||||
val error: PlaybackException?,
|
||||
val mediaItems: List<MediaItem>,
|
||||
val volume: Float
|
||||
) {
|
||||
constructor(player: Player) : this(
|
||||
currentPosition = player.currentPosition,
|
||||
duration = player.duration,
|
||||
playbackState = player.playbackState,
|
||||
mediaItemIndex = player.currentMediaItemIndex,
|
||||
mediaItem = player.currentMediaItem,
|
||||
mediaMetadata = player.mediaMetadata,
|
||||
playWhenReady = player.playWhenReady,
|
||||
repeatMode = player.repeatMode,
|
||||
error = player.playerError,
|
||||
mediaItems = player.currentTimeline.mediaItems,
|
||||
volume = player.volume
|
||||
)
|
||||
|
||||
val progress: Float
|
||||
get() = currentPosition.toFloat() / duration.absoluteValue
|
||||
|
||||
var playbackState by mutableStateOf(player.playbackState)
|
||||
private set
|
||||
|
||||
var mediaItemIndex by mutableStateOf(player.currentMediaItemIndex)
|
||||
private set
|
||||
|
||||
var mediaItem by mutableStateOf(player.currentMediaItem)
|
||||
private set
|
||||
|
||||
var mediaMetadata by mutableStateOf(player.mediaMetadata)
|
||||
private set
|
||||
|
||||
var playWhenReady by mutableStateOf(player.playWhenReady)
|
||||
private set
|
||||
|
||||
var repeatMode by mutableStateOf(player.repeatMode)
|
||||
private set
|
||||
|
||||
var error by mutableStateOf(player.playerError)
|
||||
|
||||
var mediaItems by mutableStateOf(player.currentTimeline.mediaItems)
|
||||
private set
|
||||
|
||||
var volume by mutableStateOf(player.volume)
|
||||
private set
|
||||
|
||||
override fun onVolumeChanged(volume: Float) {
|
||||
this.volume = volume
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
this.playbackState = playbackState
|
||||
}
|
||||
|
||||
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
|
||||
this.mediaMetadata = mediaMetadata
|
||||
}
|
||||
|
||||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||
this.playWhenReady = playWhenReady
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
this.mediaItem = mediaItem
|
||||
mediaItemIndex = player.currentMediaItemIndex
|
||||
}
|
||||
|
||||
override fun onRepeatModeChanged(repeatMode: Int) {
|
||||
this.repeatMode = repeatMode
|
||||
}
|
||||
|
||||
override fun onPlayerError(playbackException: PlaybackException) {
|
||||
error = playbackException
|
||||
}
|
||||
|
||||
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||
mediaItems = timeline.mediaItems
|
||||
mediaItemIndex = player.currentMediaItemIndex
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
duration = player.duration
|
||||
currentPosition = player.currentPosition
|
||||
handler.postDelayed(this, 500)
|
||||
}
|
||||
|
||||
fun init() {
|
||||
player.addListener(this)
|
||||
handler.post(this)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
player.removeListener(this)
|
||||
handler.removeCallbacks(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun rememberPlayerState(
|
||||
player: Player?
|
||||
): PlayerState? {
|
||||
val playerState = remember(player) {
|
||||
player?.let(::PlayerState)
|
||||
var playerState by remember(player) {
|
||||
mutableStateOf(player?.let(::PlayerState))
|
||||
}
|
||||
|
||||
playerState?.let {
|
||||
DisposableEffect(Unit) {
|
||||
playerState.init()
|
||||
onDispose(playerState::dispose)
|
||||
DisposableEffect(player) {
|
||||
if (player == null) return@DisposableEffect onDispose { }
|
||||
|
||||
var isSeeking = false
|
||||
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
val listener = object : Player.Listener, Runnable {
|
||||
override fun onVolumeChanged(volume: Float) {
|
||||
playerState = playerState?.copy(volume = volume)
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
playerState = playerState?.copy(playbackState = playbackState)
|
||||
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
isSeeking = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
|
||||
playerState = playerState?.copy(mediaMetadata = mediaMetadata)
|
||||
}
|
||||
|
||||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||
playerState = playerState?.copy(playWhenReady = playWhenReady)
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
playerState = playerState?.copy(
|
||||
currentPosition = player.currentPosition,
|
||||
mediaItem = mediaItem,
|
||||
mediaItemIndex = player.currentMediaItemIndex
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRepeatModeChanged(repeatMode: Int) {
|
||||
playerState = playerState?.copy(repeatMode = repeatMode)
|
||||
}
|
||||
|
||||
override fun onPlayerError(playbackException: PlaybackException) {
|
||||
playerState = playerState?.copy(error = playbackException)
|
||||
}
|
||||
|
||||
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||
playerState = playerState?.copy(
|
||||
mediaItems = timeline.mediaItems,
|
||||
mediaItemIndex = player.currentMediaItemIndex
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
reason: Int
|
||||
) {
|
||||
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
||||
isSeeking = true
|
||||
playerState = playerState?.copy(
|
||||
duration = player.duration,
|
||||
currentPosition = player.currentPosition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
if (!isSeeking) {
|
||||
playerState = playerState?.copy(
|
||||
duration = player.duration,
|
||||
currentPosition = player.currentPosition
|
||||
)
|
||||
}
|
||||
|
||||
handler.postDelayed(this, 500)
|
||||
}
|
||||
}
|
||||
|
||||
player.addListener(listener)
|
||||
handler.post(listener)
|
||||
|
||||
onDispose {
|
||||
player.removeListener(listener)
|
||||
handler.removeCallbacks(listener)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user