Convert PlayerState to a data class

This commit is contained in:
vfsfitvnm
2022-07-01 22:16:32 +02:00
parent fc9b023174
commit 7e8ec8efdb
2 changed files with 141 additions and 139 deletions

View File

@@ -31,7 +31,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.datasource.cache.Cache import androidx.media3.datasource.cache.Cache
import androidx.media3.datasource.cache.CacheSpan import androidx.media3.datasource.cache.CacheSpan
@@ -53,7 +52,6 @@ import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.PlayerResponse import it.vfsfitvnm.youtubemusic.models.PlayerResponse
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -78,6 +76,7 @@ fun PlayerView(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
player ?: return
playerState?.mediaItem ?: return playerState?.mediaItem ?: return
val smallThumbnailSize = remember { val smallThumbnailSize = remember {
@@ -156,9 +155,9 @@ fun PlayerView(
modifier = Modifier modifier = Modifier
.clickable { .clickable {
if (playerState.playbackState == Player.STATE_IDLE) { if (playerState.playbackState == Player.STATE_IDLE) {
player?.prepare() player.prepare()
} }
player?.play() player.play()
} }
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
@@ -169,9 +168,7 @@ fun PlayerView(
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text), colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier modifier = Modifier
.clickable { .clickable(onClick = player::pause)
player?.pause()
}
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.size(24.dp) .size(24.dp)
@@ -181,11 +178,8 @@ fun PlayerView(
} }
} }
) { ) {
val song by remember(playerState.mediaItem?.mediaId) { val song by remember(playerState.mediaItem.mediaId) {
playerState.mediaItem?.mediaId?.let(Database::song)?.distinctUntilChanged() playerState.mediaItem.mediaId.let(Database::song).distinctUntilChanged()
?: flowOf(
null
)
}.collectAsState(initial = null, context = Dispatchers.IO) }.collectAsState(initial = null, context = Dispatchers.IO)
var isShowingStatsForNerds by rememberSaveable { var isShowingStatsForNerds by rememberSaveable {
@@ -218,7 +212,7 @@ fun PlayerView(
.clickable { .clickable {
menuState.display { menuState.display {
QueuedMediaItemMenu( QueuedMediaItemMenu(
mediaItem = playerState.mediaItem ?: MediaItem.EMPTY, mediaItem = playerState.mediaItem,
indexInQueue = null, indexInQueue = null,
onDismiss = menuState::hide, onDismiss = menuState::hide,
onGlobalRouteEmitted = layoutState.collapse onGlobalRouteEmitted = layoutState.collapse
@@ -247,7 +241,7 @@ fun PlayerView(
.align(Alignment.CenterHorizontally) .align(Alignment.CenterHorizontally)
) { ) {
val artworkUri = remember(it) { val artworkUri = remember(it) {
player?.getMediaItemAt(it)?.mediaMetadata?.artworkUri.thumbnail( player.getMediaItemAt(it).mediaMetadata.artworkUri.thumbnail(
thumbnailSizePx thumbnailSizePx
) )
} }
@@ -281,7 +275,7 @@ fun PlayerView(
exit = fadeOut(), exit = fadeOut(),
) { ) {
var cachedBytes by remember(song?.id) { 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 { val loudnessDb by remember {
@@ -297,6 +291,8 @@ fun PlayerView(
} }
DisposableEffect(song?.id) { DisposableEffect(song?.id) {
val key = playerState.mediaItem.mediaId
val listener = object : Cache.Listener { val listener = object : Cache.Listener {
override fun onSpanAdded(cache: Cache, span: CacheSpan) { override fun onSpanAdded(cache: Cache, span: CacheSpan) {
cachedBytes += span.length cachedBytes += span.length
@@ -313,15 +309,10 @@ fun PlayerView(
) = Unit ) = Unit
} }
song?.id?.let { key -> binder.cache.addListener(key, listener)
binder?.cache?.addListener(key, listener)
}
onDispose { onDispose {
song?.id?.let { key -> binder.cache.removeListener(key, listener)
binder?.cache?.removeListener(key, listener)
}
} }
} }
@@ -368,7 +359,7 @@ fun PlayerView(
Column { Column {
BasicText( BasicText(
text = playerState.mediaItem?.mediaId ?: "Unknown", text = playerState.mediaItem.mediaId,
style = typography.xs.semiBold.color(BlackColorPalette.text) style = typography.xs.semiBold.color(BlackColorPalette.text)
) )
BasicText( BasicText(
@@ -451,11 +442,10 @@ fun PlayerView(
.size(thumbnailSizeDp) .size(thumbnailSizeDp)
) { ) {
LoadingOrError( LoadingOrError(
errorMessage = playerState.error?.javaClass?.canonicalName, errorMessage = playerState.error.javaClass.canonicalName,
onRetry = { onRetry = {
player?.playWhenReady = true player.playWhenReady = true
player?.prepare() player.prepare()
playerState.error = null
} }
) {} ) {}
} }
@@ -494,10 +484,7 @@ fun PlayerView(
} }
}, },
onDragEnd = { onDragEnd = {
scrubbingPosition?.let { scrubbingPosition -> scrubbingPosition?.let(player::seekTo)
player?.seekTo(scrubbingPosition)
playerState.currentPosition = scrubbingPosition
}
scrubbingPosition = null scrubbingPosition = null
}, },
color = colorPalette.text, color = colorPalette.text,
@@ -553,9 +540,7 @@ fun PlayerView(
query { query {
song?.let { song -> song?.let { song ->
Database.update(song.toggleLike()) Database.update(song.toggleLike())
} ?: playerState.mediaItem?.let { mediaItem -> } ?: Database.insert(playerState.mediaItem, Song::toggleLike)
Database.insert(mediaItem, Song::toggleLike)
}
} }
} }
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
@@ -567,9 +552,7 @@ fun PlayerView(
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text), colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier modifier = Modifier
.clickable { .clickable(onClick = player::seekToPrevious)
player?.seekToPrevious()
}
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.size(32.dp) .size(32.dp)
) )
@@ -581,11 +564,11 @@ fun PlayerView(
colorFilter = ColorFilter.tint(colorPalette.text), colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier modifier = Modifier
.clickable { .clickable {
if (player?.playbackState == Player.STATE_IDLE) { if (player.playbackState == Player.STATE_IDLE) {
player.prepare() player.prepare()
} }
player?.play() player.play()
} }
.size(64.dp) .size(64.dp)
) )
@@ -594,9 +577,7 @@ fun PlayerView(
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text), colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier modifier = Modifier
.clickable { .clickable(onClick = player::pause)
player?.pause()
}
.size(64.dp) .size(64.dp)
) )
} }
@@ -606,14 +587,11 @@ fun PlayerView(
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text), colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier modifier = Modifier
.clickable { .clickable(onClick = player::seekToNext)
player?.seekToNext()
}
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.size(32.dp) .size(32.dp)
) )
Image( Image(
painter = painterResource( painter = painterResource(
if (playerState.repeatMode == Player.REPEAT_MODE_ONE) { if (playerState.repeatMode == Player.REPEAT_MODE_ONE) {
@@ -632,10 +610,10 @@ fun PlayerView(
), ),
modifier = Modifier modifier = Modifier
.clickable { .clickable {
player?.repeatMode player.repeatMode
?.plus(2) .plus(2)
?.mod(3) .mod(3)
?.let { repeatMode -> .let { repeatMode ->
player.repeatMode = repeatMode player.repeatMode = repeatMode
preferences.repeatMode = repeatMode preferences.repeatMode = repeatMode
} }

View File

@@ -6,107 +6,131 @@ import androidx.compose.runtime.*
import androidx.media3.common.* import androidx.media3.common.*
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@Stable @Stable
class PlayerState(private val player: Player) : Player.Listener, Runnable { data class PlayerState(
private val handler = Handler(Looper.getMainLooper()) val currentPosition: Long,
val duration: Long,
var currentPosition by mutableStateOf(player.currentPosition) val playbackState: Int,
val mediaItemIndex: Int,
var duration by mutableStateOf(player.duration) val mediaItem: MediaItem?,
private set 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 val progress: Float
get() = currentPosition.toFloat() / duration.absoluteValue 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 @Composable
fun rememberPlayerState( fun rememberPlayerState(
player: Player? player: Player?
): PlayerState? { ): PlayerState? {
val playerState = remember(player) { var playerState by remember(player) {
player?.let(::PlayerState) mutableStateOf(player?.let(::PlayerState))
} }
playerState?.let { DisposableEffect(player) {
DisposableEffect(Unit) { if (player == null) return@DisposableEffect onDispose { }
playerState.init()
onDispose(playerState::dispose) 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)
} }
} }