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.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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user