Remove PlayerState class

This commit is contained in:
vfsfitvnm
2022-07-22 11:01:54 +02:00
parent bc76533512
commit 3b9b5a5124
3 changed files with 157 additions and 190 deletions

View File

@@ -16,7 +16,6 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.BottomSheet import it.vfsfitvnm.vimusic.ui.components.BottomSheet
import it.vfsfitvnm.vimusic.ui.components.BottomSheetState import it.vfsfitvnm.vimusic.ui.components.BottomSheetState
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.PlayerState
@ExperimentalAnimationApi @ExperimentalAnimationApi

View File

@@ -59,6 +59,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlin.math.absoluteValue
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -75,11 +76,18 @@ fun PlayerView(
val context = LocalContext.current val context = LocalContext.current
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val player = binder?.player binder?.player ?: return
val playerState = rememberPlayerState(player)
player ?: return val mediaItemIndex by rememberMediaItemIndex(binder.player)
playerState?.mediaItem ?: return
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( BottomSheet(
state = layoutState, state = layoutState,
@@ -97,6 +105,7 @@ fun PlayerView(
} }
.background(colorPalette.elevatedBackground) .background(colorPalette.elevatedBackground)
.drawBehind { .drawBehind {
val progress = positionAndDuration.first.toFloat() / positionAndDuration.second.absoluteValue
val offset = Dimensions.thumbnails.player.songPreview.toPx() val offset = Dimensions.thumbnails.player.songPreview.toPx()
drawLine( drawLine(
@@ -106,7 +115,7 @@ fun PlayerView(
y = 1.dp.toPx() y = 1.dp.toPx()
), ),
end = Offset( end = Offset(
x = ((size.width - offset) * playerState.progress) + offset, x = ((size.width - offset) * progress) + offset,
y = 1.dp.toPx() y = 1.dp.toPx()
), ),
strokeWidth = 2.dp.toPx() strokeWidth = 2.dp.toPx()
@@ -114,7 +123,7 @@ fun PlayerView(
} }
) { ) {
AsyncImage( AsyncImage(
model = playerState.mediaMetadata.artworkUri.thumbnail(Dimensions.thumbnails.player.songPreview.px), model = mediaItem.mediaMetadata.artworkUri.thumbnail(Dimensions.thumbnails.player.songPreview.px),
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
@@ -127,45 +136,46 @@ fun PlayerView(
.weight(1f) .weight(1f)
) { ) {
BasicText( BasicText(
text = playerState.mediaMetadata.title?.toString() ?: "", text = mediaItem.mediaMetadata.title?.toString() ?: "",
style = typography.xs.semiBold, style = typography.xs.semiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
BasicText( BasicText(
text = playerState.mediaMetadata.artist?.toString() ?: "", text = mediaItem.mediaMetadata.artist?.toString() ?: "",
style = typography.xs.semiBold.secondary, style = typography.xs.semiBold.secondary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
} }
when { if (shouldBePlaying) {
playerState.playbackState == Player.STATE_ENDED || !playerState.playWhenReady -> Image( 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), painter = painterResource(R.drawable.play),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text), colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier modifier = Modifier
.clickable { .clickable {
if (playerState.playbackState == Player.STATE_IDLE) { if (binder.player.playbackState == Player.STATE_IDLE) {
player.prepare() binder.player.prepare()
} }
player.play() binder.player.play()
} }
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.size(22.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( Controls(
playerState = playerState, mediaItemIndex = mediaItemIndex,
shouldBePlaying = shouldBePlaying,
position = positionAndDuration.first,
duration = positionAndDuration.second,
modifier = Modifier modifier = Modifier
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.fillMaxHeight() .fillMaxHeight()
@@ -237,7 +250,10 @@ fun PlayerView(
} }
Controls( Controls(
playerState = playerState, mediaItemIndex = mediaItemIndex,
shouldBePlaying = shouldBePlaying,
position = positionAndDuration.first,
duration = positionAndDuration.second,
modifier = Modifier modifier = Modifier
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.fillMaxWidth() .fillMaxWidth()
@@ -264,13 +280,13 @@ fun PlayerView(
val resultRegistryOwner = LocalActivityResultRegistryOwner.current val resultRegistryOwner = LocalActivityResultRegistryOwner.current
BaseMediaItemMenu( BaseMediaItemMenu(
mediaItem = playerState.mediaItem, mediaItem = mediaItem,
onGoToEqualizer = { onGoToEqualizer = {
val intent = val intent =
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply {
putExtra( putExtra(
AudioEffect.EXTRA_AUDIO_SESSION, AudioEffect.EXTRA_AUDIO_SESSION,
player.audioSessionId binder.player.audioSessionId
) )
putExtra( putExtra(
AudioEffect.EXTRA_PACKAGE_NAME, AudioEffect.EXTRA_PACKAGE_NAME,
@@ -820,21 +836,29 @@ private fun StatsForNerds(
@Composable @Composable
private fun Controls( private fun Controls(
playerState: PlayerState, mediaItemIndex: Int,
shouldBePlaying: Boolean,
position: Long,
duration: Long,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val (colorPalette, typography) = LocalAppearance.current val (colorPalette, typography) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
val player = binder?.player ?: return binder?.player ?: return
val mediaId = playerState.mediaItem?.mediaId ?: 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<Long?>(null) mutableStateOf<Long?>(null)
} }
val likedAt by remember(mediaId) { val likedAt by remember(mediaItem.mediaId) {
Database.likedAt(mediaId).distinctUntilChanged() Database.likedAt(mediaItem.mediaId).distinctUntilChanged()
}.collectAsState(initial = null, context = Dispatchers.IO) }.collectAsState(initial = null, context = Dispatchers.IO)
Column( Column(
@@ -849,14 +873,14 @@ private fun Controls(
) )
BasicText( BasicText(
text = playerState.mediaMetadata.title?.toString() ?: "", text = mediaItem.mediaMetadata.title?.toString() ?: "",
style = typography.l.bold, style = typography.l.bold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
BasicText( BasicText(
text = playerState.mediaMetadata.artist?.toString() ?: "", text = mediaItem.mediaMetadata.artist?.toString() ?: "",
style = typography.s.semiBold.secondary, style = typography.s.semiBold.secondary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
@@ -868,21 +892,21 @@ private fun Controls(
) )
SeekBar( SeekBar(
value = scrubbingPosition ?: playerState.currentPosition, value = scrubbingPosition ?: position,
minimumValue = 0, minimumValue = 0,
maximumValue = playerState.duration, maximumValue = duration,
onDragStart = { onDragStart = {
scrubbingPosition = it scrubbingPosition = it
}, },
onDrag = { delta -> onDrag = { delta ->
scrubbingPosition = if (playerState.duration != C.TIME_UNSET) { scrubbingPosition = if (duration != C.TIME_UNSET) {
scrubbingPosition?.plus(delta)?.coerceIn(0, playerState.duration) scrubbingPosition?.plus(delta)?.coerceIn(0, duration)
} else { } else {
null null
} }
}, },
onDragEnd = { onDragEnd = {
scrubbingPosition?.let(player::seekTo) scrubbingPosition?.let(binder.player::seekTo)
scrubbingPosition = null scrubbingPosition = null
}, },
color = colorPalette.text, color = colorPalette.text,
@@ -902,17 +926,15 @@ private fun Controls(
.fillMaxWidth() .fillMaxWidth()
) { ) {
BasicText( BasicText(
text = DateUtils.formatElapsedTime( text = DateUtils.formatElapsedTime((scrubbingPosition ?: position) / 1000),
(scrubbingPosition ?: playerState.currentPosition) / 1000
),
style = typography.xxs.semiBold, style = typography.xxs.semiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
if (playerState.duration != C.TIME_UNSET) { if (duration != C.TIME_UNSET) {
BasicText( BasicText(
text = DateUtils.formatElapsedTime(playerState.duration / 1000), text = DateUtils.formatElapsedTime(duration / 1000),
style = typography.xxs.semiBold, style = typography.xxs.semiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@@ -939,11 +961,11 @@ private fun Controls(
.clickable { .clickable {
query { query {
if (Database.like( if (Database.like(
mediaId, mediaItem.mediaId,
if (likedAt == null) System.currentTimeMillis() else null if (likedAt == null) System.currentTimeMillis() else null
) == 0 ) == 0
) { ) {
Database.insert(playerState.mediaItem, Song::toggleLike) Database.insert(mediaItem, Song::toggleLike)
} }
} }
} }
@@ -956,7 +978,7 @@ private fun Controls(
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text), colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier modifier = Modifier
.clickable(onClick = player::seekToPrevious) .clickable(onClick = binder.player::seekToPrevious)
.weight(1f) .weight(1f)
.size(28.dp) .size(28.dp)
) )
@@ -966,27 +988,23 @@ private fun Controls(
.width(8.dp) .width(8.dp)
) )
val isPaused =
playerState.playbackState == Player.STATE_ENDED || !playerState.playWhenReady
Box( Box(
modifier = Modifier modifier = Modifier
.clickable { .clickable {
if (isPaused) { if (shouldBePlaying) {
if (player.playbackState == Player.STATE_IDLE) { binder.player.pause()
player.prepare()
}
player.play()
} else { } else {
player.pause() if (binder.player.playbackState == Player.STATE_IDLE) {
binder.player.prepare()
}
binder.player.play()
} }
} }
.background(color = colorPalette.text, shape = CircleShape) .background(color = colorPalette.text, shape = CircleShape)
.size(64.dp) .size(64.dp)
) { ) {
Image( 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, contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.background), colorFilter = ColorFilter.tint(colorPalette.background),
modifier = Modifier modifier = Modifier
@@ -1005,14 +1023,14 @@ private fun Controls(
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text), colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier modifier = Modifier
.clickable(onClick = player::seekToNext) .clickable(onClick = binder.player::seekToNext)
.weight(1f) .weight(1f)
.size(28.dp) .size(28.dp)
) )
Image( Image(
painter = painterResource( painter = painterResource(
if (playerState.repeatMode == Player.REPEAT_MODE_ONE) { if (repeatMode == Player.REPEAT_MODE_ONE) {
R.drawable.repeat_one R.drawable.repeat_one
} else { } else {
R.drawable.repeat R.drawable.repeat
@@ -1020,7 +1038,7 @@ private fun Controls(
), ),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint( colorFilter = ColorFilter.tint(
if (playerState.repeatMode == Player.REPEAT_MODE_OFF) { if (repeatMode == Player.REPEAT_MODE_OFF) {
colorPalette.textDisabled colorPalette.textDisabled
} else { } else {
colorPalette.text colorPalette.text
@@ -1028,11 +1046,11 @@ private fun Controls(
), ),
modifier = Modifier modifier = Modifier
.clickable { .clickable {
player.repeatMode binder.player.repeatMode
.plus(2) .plus(2)
.mod(3) .mod(3)
.let { .let {
player.repeatMode = it binder.player.repeatMode = it
} }
} }
.weight(1f) .weight(1f)

View File

@@ -1,130 +1,15 @@
package it.vfsfitvnm.vimusic.utils package it.vfsfitvnm.vimusic.utils
import android.os.Handler
import android.os.Looper
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.media3.common.* import androidx.media3.common.MediaItem
import kotlin.math.absoluteValue 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<MediaItem>,
) {
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) context(DisposableEffectScope)
fun Player.listener(listener: Player.Listener): DisposableEffectResult { fun Player.listener(listener: Player.Listener): DisposableEffectResult {
addListener(listener) addListener(listener)
@@ -136,17 +21,17 @@ fun Player.listener(listener: Player.Listener): DisposableEffectResult {
@Composable @Composable
fun rememberMediaItemIndex(player: Player): State<Int> { fun rememberMediaItemIndex(player: Player): State<Int> {
val mediaItemIndexState = remember(player) { val mediaItemIndexState = remember(player) {
mutableStateOf(player.currentMediaItemIndex) mutableStateOf(if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex)
} }
DisposableEffect(player) { DisposableEffect(player) {
player.listener(object : Player.Listener { player.listener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { 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) { 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<Boolean> {
return state return state
} }
@Composable
fun rememberRepeatMode(player: Player): State<Int> {
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<Pair<Long, Long>> {
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 @Composable
fun rememberVolume(player: Player): State<Float> { fun rememberVolume(player: Player): State<Float> {
val volumeState = remember(player) { val volumeState = remember(player) {