From 3b9b5a5124641c428edfcd0de027c3bcd9affae7 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Fri, 22 Jul 2022 11:01:54 +0200 Subject: [PATCH] Remove PlayerState class --- .../vimusic/ui/views/PlayerBottomSheet.kt | 1 - .../vfsfitvnm/vimusic/ui/views/PlayerView.kt | 146 +++++++------ .../it/vfsfitvnm/vimusic/utils/PlayerState.kt | 200 +++++++----------- 3 files changed, 157 insertions(+), 190 deletions(-) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt index f7348a9..0a1d7b9 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt @@ -16,7 +16,6 @@ import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.ui.components.BottomSheet import it.vfsfitvnm.vimusic.ui.components.BottomSheetState import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance -import it.vfsfitvnm.vimusic.utils.PlayerState @ExperimentalAnimationApi 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 6d8244e..8b1789c 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 @@ -59,6 +59,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking +import kotlin.math.absoluteValue import kotlin.math.roundToInt @@ -75,11 +76,18 @@ fun PlayerView( val context = LocalContext.current val configuration = LocalConfiguration.current - val player = binder?.player - val playerState = rememberPlayerState(player) + binder?.player ?: return - player ?: return - playerState?.mediaItem ?: return + val mediaItemIndex by rememberMediaItemIndex(binder.player) + + if (mediaItemIndex == -1) return + + val mediaItem = remember(mediaItemIndex) { + binder.player.getMediaItemAt(mediaItemIndex) + } + + val shouldBePlaying by rememberShouldBePlaying(binder.player) + val positionAndDuration by rememberPositionAndDuration(binder.player) BottomSheet( state = layoutState, @@ -97,6 +105,7 @@ fun PlayerView( } .background(colorPalette.elevatedBackground) .drawBehind { + val progress = positionAndDuration.first.toFloat() / positionAndDuration.second.absoluteValue val offset = Dimensions.thumbnails.player.songPreview.toPx() drawLine( @@ -106,7 +115,7 @@ fun PlayerView( y = 1.dp.toPx() ), end = Offset( - x = ((size.width - offset) * playerState.progress) + offset, + x = ((size.width - offset) * progress) + offset, y = 1.dp.toPx() ), strokeWidth = 2.dp.toPx() @@ -114,7 +123,7 @@ fun PlayerView( } ) { AsyncImage( - model = playerState.mediaMetadata.artworkUri.thumbnail(Dimensions.thumbnails.player.songPreview.px), + model = mediaItem.mediaMetadata.artworkUri.thumbnail(Dimensions.thumbnails.player.songPreview.px), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier @@ -127,45 +136,46 @@ fun PlayerView( .weight(1f) ) { BasicText( - text = playerState.mediaMetadata.title?.toString() ?: "", + text = mediaItem.mediaMetadata.title?.toString() ?: "", style = typography.xs.semiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, ) BasicText( - text = playerState.mediaMetadata.artist?.toString() ?: "", + text = mediaItem.mediaMetadata.artist?.toString() ?: "", style = typography.xs.semiBold.secondary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) } - when { - playerState.playbackState == Player.STATE_ENDED || !playerState.playWhenReady -> Image( + if (shouldBePlaying) { + Image( + painter = painterResource(R.drawable.pause), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable(onClick = binder.player::pause) + .padding(vertical = 8.dp) + .padding(horizontal = 16.dp) + .size(22.dp) + ) + } else { + Image( painter = painterResource(R.drawable.play), contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - if (playerState.playbackState == Player.STATE_IDLE) { - player.prepare() + if (binder.player.playbackState == Player.STATE_IDLE) { + binder.player.prepare() } - player.play() + binder.player.play() } .padding(vertical = 8.dp) .padding(horizontal = 16.dp) .size(22.dp) ) - else -> Image( - painter = painterResource(R.drawable.pause), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = player::pause) - .padding(vertical = 8.dp) - .padding(horizontal = 16.dp) - .size(22.dp) - ) } } } @@ -205,7 +215,10 @@ fun PlayerView( } Controls( - playerState = playerState, + mediaItemIndex = mediaItemIndex, + shouldBePlaying = shouldBePlaying, + position = positionAndDuration.first, + duration = positionAndDuration.second, modifier = Modifier .padding(vertical = 8.dp) .fillMaxHeight() @@ -237,7 +250,10 @@ fun PlayerView( } Controls( - playerState = playerState, + mediaItemIndex = mediaItemIndex, + shouldBePlaying = shouldBePlaying, + position = positionAndDuration.first, + duration = positionAndDuration.second, modifier = Modifier .padding(vertical = 8.dp) .fillMaxWidth() @@ -264,13 +280,13 @@ fun PlayerView( val resultRegistryOwner = LocalActivityResultRegistryOwner.current BaseMediaItemMenu( - mediaItem = playerState.mediaItem, + mediaItem = mediaItem, onGoToEqualizer = { val intent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { putExtra( AudioEffect.EXTRA_AUDIO_SESSION, - player.audioSessionId + binder.player.audioSessionId ) putExtra( AudioEffect.EXTRA_PACKAGE_NAME, @@ -820,21 +836,29 @@ private fun StatsForNerds( @Composable private fun Controls( - playerState: PlayerState, + mediaItemIndex: Int, + shouldBePlaying: Boolean, + position: Long, + duration: Long, modifier: Modifier = Modifier ) { val (colorPalette, typography) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current - val player = binder?.player ?: return - val mediaId = playerState.mediaItem?.mediaId ?: return + binder?.player ?: return - var scrubbingPosition by remember(playerState.mediaItemIndex) { + val repeatMode by rememberRepeatMode(binder.player) + + val mediaItem = remember(mediaItemIndex) { + binder.player.getMediaItemAt(mediaItemIndex) + } + + var scrubbingPosition by remember(mediaItemIndex) { mutableStateOf(null) } - val likedAt by remember(mediaId) { - Database.likedAt(mediaId).distinctUntilChanged() + val likedAt by remember(mediaItem.mediaId) { + Database.likedAt(mediaItem.mediaId).distinctUntilChanged() }.collectAsState(initial = null, context = Dispatchers.IO) Column( @@ -849,14 +873,14 @@ private fun Controls( ) BasicText( - text = playerState.mediaMetadata.title?.toString() ?: "", + text = mediaItem.mediaMetadata.title?.toString() ?: "", style = typography.l.bold, maxLines = 1, overflow = TextOverflow.Ellipsis ) BasicText( - text = playerState.mediaMetadata.artist?.toString() ?: "", + text = mediaItem.mediaMetadata.artist?.toString() ?: "", style = typography.s.semiBold.secondary, maxLines = 1, overflow = TextOverflow.Ellipsis @@ -868,21 +892,21 @@ private fun Controls( ) SeekBar( - value = scrubbingPosition ?: playerState.currentPosition, + value = scrubbingPosition ?: position, minimumValue = 0, - maximumValue = playerState.duration, + maximumValue = duration, onDragStart = { scrubbingPosition = it }, onDrag = { delta -> - scrubbingPosition = if (playerState.duration != C.TIME_UNSET) { - scrubbingPosition?.plus(delta)?.coerceIn(0, playerState.duration) + scrubbingPosition = if (duration != C.TIME_UNSET) { + scrubbingPosition?.plus(delta)?.coerceIn(0, duration) } else { null } }, onDragEnd = { - scrubbingPosition?.let(player::seekTo) + scrubbingPosition?.let(binder.player::seekTo) scrubbingPosition = null }, color = colorPalette.text, @@ -902,17 +926,15 @@ private fun Controls( .fillMaxWidth() ) { BasicText( - text = DateUtils.formatElapsedTime( - (scrubbingPosition ?: playerState.currentPosition) / 1000 - ), + text = DateUtils.formatElapsedTime((scrubbingPosition ?: position) / 1000), style = typography.xxs.semiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - if (playerState.duration != C.TIME_UNSET) { + if (duration != C.TIME_UNSET) { BasicText( - text = DateUtils.formatElapsedTime(playerState.duration / 1000), + text = DateUtils.formatElapsedTime(duration / 1000), style = typography.xxs.semiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -939,11 +961,11 @@ private fun Controls( .clickable { query { if (Database.like( - mediaId, + mediaItem.mediaId, if (likedAt == null) System.currentTimeMillis() else null ) == 0 ) { - Database.insert(playerState.mediaItem, Song::toggleLike) + Database.insert(mediaItem, Song::toggleLike) } } } @@ -956,7 +978,7 @@ private fun Controls( contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier - .clickable(onClick = player::seekToPrevious) + .clickable(onClick = binder.player::seekToPrevious) .weight(1f) .size(28.dp) ) @@ -966,27 +988,23 @@ private fun Controls( .width(8.dp) ) - val isPaused = - playerState.playbackState == Player.STATE_ENDED || !playerState.playWhenReady - Box( modifier = Modifier .clickable { - if (isPaused) { - if (player.playbackState == Player.STATE_IDLE) { - player.prepare() - } - - player.play() + if (shouldBePlaying) { + binder.player.pause() } else { - player.pause() + if (binder.player.playbackState == Player.STATE_IDLE) { + binder.player.prepare() + } + binder.player.play() } } .background(color = colorPalette.text, shape = CircleShape) .size(64.dp) ) { Image( - painter = painterResource(if (isPaused) R.drawable.play else R.drawable.pause), + painter = painterResource(if (shouldBePlaying) R.drawable.pause else R.drawable.play), contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.background), modifier = Modifier @@ -1005,14 +1023,14 @@ private fun Controls( contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier - .clickable(onClick = player::seekToNext) + .clickable(onClick = binder.player::seekToNext) .weight(1f) .size(28.dp) ) Image( painter = painterResource( - if (playerState.repeatMode == Player.REPEAT_MODE_ONE) { + if (repeatMode == Player.REPEAT_MODE_ONE) { R.drawable.repeat_one } else { R.drawable.repeat @@ -1020,7 +1038,7 @@ private fun Controls( ), contentDescription = null, colorFilter = ColorFilter.tint( - if (playerState.repeatMode == Player.REPEAT_MODE_OFF) { + if (repeatMode == Player.REPEAT_MODE_OFF) { colorPalette.textDisabled } else { colorPalette.text @@ -1028,11 +1046,11 @@ private fun Controls( ), modifier = Modifier .clickable { - player.repeatMode + binder.player.repeatMode .plus(2) .mod(3) .let { - player.repeatMode = it + binder.player.repeatMode = it } } .weight(1f) 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 557b182..715d4db 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt @@ -1,130 +1,15 @@ package it.vfsfitvnm.vimusic.utils -import android.os.Handler -import android.os.Looper import androidx.compose.runtime.* -import androidx.media3.common.* -import kotlin.math.absoluteValue +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.common.Timeline +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch -@Stable -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 mediaItems: List, -) { - 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, - mediaItems = player.currentTimeline.mediaItems - ) - - val progress: Float - get() = currentPosition.toFloat() / duration.absoluteValue -} - - -@Composable -fun rememberPlayerState( - player: Player? -): PlayerState? { - var playerState by remember(player) { - mutableStateOf(player?.let(::PlayerState)) - } - - DisposableEffect(player) { - if (player == null) return@DisposableEffect onDispose { } - - var isSeeking = false - - val handler = Handler(Looper.getMainLooper()) - - val listener = object : Player.Listener, Runnable { - 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 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) - } - } - - return playerState -} - context(DisposableEffectScope) fun Player.listener(listener: Player.Listener): DisposableEffectResult { addListener(listener) @@ -136,17 +21,17 @@ fun Player.listener(listener: Player.Listener): DisposableEffectResult { @Composable fun rememberMediaItemIndex(player: Player): State { val mediaItemIndexState = remember(player) { - mutableStateOf(player.currentMediaItemIndex) + mutableStateOf(if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex) } DisposableEffect(player) { player.listener(object : Player.Listener { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - mediaItemIndexState.value = player.currentMediaItemIndex + mediaItemIndexState.value = if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex } override fun onTimelineChanged(timeline: Timeline, reason: Int) { - mediaItemIndexState.value = player.currentMediaItemIndex + mediaItemIndexState.value = if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex } }) } @@ -192,6 +77,71 @@ fun rememberShouldBePlaying(player: Player): State { return state } +@Composable +fun rememberRepeatMode(player: Player): State { + val state = remember(player) { + mutableStateOf(player.repeatMode) + } + + DisposableEffect(player) { + player.listener(object : Player.Listener { + override fun onRepeatModeChanged(repeatMode: Int) { + state.value = repeatMode + } + }) + } + + return state +} + +@Composable +fun rememberPositionAndDuration(player: Player): State> { + val state = produceState(initialValue = player.currentPosition to player.duration) { + var isSeeking = false + + val listener = object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + if (playbackState == Player.STATE_READY) { + isSeeking = false + } + } + + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + value = player.currentPosition to value.second + } + + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + if (reason == Player.DISCONTINUITY_REASON_SEEK) { + isSeeking = true + value = player.currentPosition to player.duration + } + } + } + + player.addListener(listener) + + val pollJob = launch { + while (isActive) { + delay(500) + if (!isSeeking) { + value = player.currentPosition to player.duration + } + } + } + + awaitDispose { + pollJob.cancel() + player.removeListener(listener) + } + } + + return state +} + @Composable fun rememberVolume(player: Player): State { val volumeState = remember(player) {