From 7e8ec8efdbcca09a4a8e3fe89699d5fcc94b13fe Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Fri, 1 Jul 2022 22:16:32 +0200 Subject: [PATCH] Convert PlayerState to a data class --- .../vfsfitvnm/vimusic/ui/views/PlayerView.kt | 78 +++---- .../it/vfsfitvnm/vimusic/utils/PlayerState.kt | 202 ++++++++++-------- 2 files changed, 141 insertions(+), 139 deletions(-) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt index 10055ef..8692eab 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt @@ -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 } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt index 9c85689..d56f7db 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt @@ -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, + 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) } }