Make Thumbnail composable smart recompose

This commit is contained in:
vfsfitvnm
2022-07-21 12:08:18 +02:00
parent 5ed77b8c32
commit de46f90793
2 changed files with 274 additions and 203 deletions

View File

@@ -36,8 +36,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource 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.*
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
import coil.compose.AsyncImage import coil.compose.AsyncImage
@@ -45,13 +44,15 @@ import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.models.Format
import it.vfsfitvnm.vimusic.models.Song import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.* import it.vfsfitvnm.vimusic.ui.components.*
import it.vfsfitvnm.vimusic.ui.components.themed.BaseMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.BaseMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
import it.vfsfitvnm.vimusic.ui.styling.* import it.vfsfitvnm.vimusic.ui.styling.BlackColorPalette
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.YouTube import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -189,10 +190,7 @@ fun PlayerView(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.padding(bottom = 16.dp) .padding(bottom = 16.dp)
) { ) {
Thumbnail( Thumbnail()
playerState = playerState,
modifier = Modifier
)
} }
Controls( Controls(
@@ -219,10 +217,7 @@ fun PlayerView(
.weight(1.25f) .weight(1.25f)
.padding(horizontal = 32.dp, vertical = 8.dp) .padding(horizontal = 32.dp, vertical = 8.dp)
) { ) {
Thumbnail( Thumbnail()
playerState = playerState,
modifier = Modifier
)
} }
Controls( Controls(
@@ -300,7 +295,6 @@ fun PlayerView(
) )
} }
PlayerBottomSheet( PlayerBottomSheet(
playerState = playerState, playerState = playerState,
layoutState = rememberBottomSheetState(64.dp, layoutState.upperBound * 0.9f), layoutState = rememberBottomSheetState(64.dp, layoutState.upperBound * 0.9f),
@@ -316,27 +310,26 @@ fun PlayerView(
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
private fun Thumbnail( private fun Thumbnail(
playerState: PlayerState,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val (_, typography) = LocalAppearance.current
val context = LocalContext.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
val player = binder?.player ?: return val player = binder?.player ?: return
playerState.mediaItem ?: return
val (thumbnailSizeDp, thumbnailSizePx) = Dimensions.thumbnails.player.song.let { val (thumbnailSizeDp, thumbnailSizePx) = Dimensions.thumbnails.player.song.let {
it to (it - 64.dp).px it to (it - 64.dp).px
} }
val mediaItemIndex by rememberMediaItemIndex(player)
val error by rememberError(player)
var isShowingStatsForNerds by rememberSaveable { var isShowingStatsForNerds by rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
} }
if (playerState.error == null) { if (error == null) {
AnimatedContent( AnimatedContent(
targetState = playerState.mediaItemIndex, targetState = mediaItemIndex,
transitionSpec = { transitionSpec = {
val slideDirection = val slideDirection =
if (targetState > initialState) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right if (targetState > initialState) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right
@@ -348,11 +341,9 @@ private fun Thumbnail(
}, },
modifier = modifier modifier = modifier
.aspectRatio(1f) .aspectRatio(1f)
) { ) { currentMediaItemIndex ->
val artworkUri = remember(it) { val mediaItem = remember(currentMediaItemIndex) {
player.getMediaItemAt(it).mediaMetadata.artworkUri.thumbnail( player.getMediaItemAt(currentMediaItemIndex)
thumbnailSizePx
)
} }
Box( Box(
@@ -361,7 +352,7 @@ private fun Thumbnail(
.size(thumbnailSizeDp) .size(thumbnailSizeDp)
) { ) {
AsyncImage( AsyncImage(
model = artworkUri, model = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx),
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
@@ -375,22 +366,61 @@ private fun Thumbnail(
.fillMaxSize() .fillMaxSize()
) )
StatsForNerds(
mediaId = mediaItem.mediaId,
isDisplayed = isShowingStatsForNerds,
onDismiss = {
isShowingStatsForNerds = false
}
)
}
}
} else {
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.padding(bottom = 32.dp)
.padding(horizontal = 32.dp)
.size(thumbnailSizeDp)
) {
LoadingOrError(
errorMessage = error?.javaClass?.canonicalName,
onRetry = {
player.playWhenReady = true
player.prepare()
}
) {}
}
}
}
@Composable
private fun StatsForNerds(
mediaId: String,
isDisplayed: Boolean,
onDismiss: () -> Unit,
modifier: Modifier = Modifier
) {
val (_, typography) = LocalAppearance.current
val context = LocalContext.current
val binder = LocalPlayerServiceBinder.current ?: return
AnimatedVisibility( AnimatedVisibility(
visible = isShowingStatsForNerds, visible = isDisplayed,
enter = fadeIn(), enter = fadeIn(),
exit = fadeOut(), exit = fadeOut(),
) { ) {
val key = playerState.mediaItem.mediaId var cachedBytes by remember(mediaId) {
mutableStateOf(binder.cache.getCachedBytes(mediaId, 0, -1))
var cachedBytes by remember(key) {
mutableStateOf(binder.cache.getCachedBytes(key, 0, -1))
} }
val format by remember(key) { val format by remember(mediaId) {
Database.format(key) Database.format(mediaId).distinctUntilChanged()
}.collectAsState(initial = null, context = Dispatchers.IO) }.collectAsState(initial = null, context = Dispatchers.IO)
DisposableEffect(key) { val volume by rememberVolume(binder.player)
DisposableEffect(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
@@ -407,20 +437,20 @@ private fun Thumbnail(
) = Unit ) = Unit
} }
binder.cache.addListener(key, listener) binder.cache.addListener(mediaId, listener)
onDispose { onDispose {
binder.cache.removeListener(key, listener) binder.cache.removeListener(mediaId, listener)
} }
} }
Column( Column(
verticalArrangement = Arrangement.SpaceBetween, verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier modifier = modifier
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onPress = { onTap = {
isShowingStatsForNerds = false onDismiss()
} }
) )
} }
@@ -461,11 +491,11 @@ private fun Thumbnail(
Column { Column {
BasicText( BasicText(
text = playerState.mediaItem.mediaId, text = mediaId,
style = typography.xs.semiBold.color(BlackColorPalette.text) style = typography.xs.semiBold.color(BlackColorPalette.text)
) )
BasicText( BasicText(
text = "${playerState.volume.times(100).roundToInt()}%", text = "${volume.times(100).roundToInt()}%",
style = typography.xs.semiBold.color(BlackColorPalette.text) style = typography.xs.semiBold.color(BlackColorPalette.text)
) )
BasicText( BasicText(
@@ -513,12 +543,16 @@ private fun Thumbnail(
onClick = { onClick = {
query { query {
runBlocking(Dispatchers.IO) { runBlocking(Dispatchers.IO) {
YouTube.player(key)?.map { response -> YouTube
response.streamingData?.adaptiveFormats?.findLast { format -> .player(mediaId)
?.map { response ->
response.streamingData?.adaptiveFormats
?.findLast { format ->
format.itag == 251 || format.itag == 140 format.itag == 251 || format.itag == 140
}?.let { format -> }
Format( ?.let { format ->
songId = key, it.vfsfitvnm.vimusic.models.Format(
songId = mediaId,
itag = format.itag, itag = format.itag,
mimeType = format.mimeType, mimeType = format.mimeType,
bitrate = format.bitrate, bitrate = format.bitrate,
@@ -528,7 +562,9 @@ private fun Thumbnail(
) )
} }
} }
}?.getOrNull()?.let(Database::insert) }
?.getOrNull()
?.let(Database::insert)
} }
} }
) )
@@ -539,25 +575,6 @@ private fun Thumbnail(
} }
} }
} }
}
} else {
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.padding(bottom = 32.dp)
.padding(horizontal = 32.dp)
.size(thumbnailSizeDp)
) {
LoadingOrError(
errorMessage = playerState.error.javaClass.canonicalName,
onRetry = {
player.playWhenReady = true
player.prepare()
}
) {}
}
}
}
@Composable @Composable
private fun Controls( private fun Controls(

View File

@@ -17,9 +17,7 @@ data class PlayerState(
val mediaMetadata: MediaMetadata, val mediaMetadata: MediaMetadata,
val playWhenReady: Boolean, val playWhenReady: Boolean,
val repeatMode: Int, val repeatMode: Int,
val error: PlaybackException?,
val mediaItems: List<MediaItem>, val mediaItems: List<MediaItem>,
val volume: Float
) { ) {
constructor(player: Player) : this( constructor(player: Player) : this(
currentPosition = player.currentPosition, currentPosition = player.currentPosition,
@@ -30,9 +28,7 @@ data class PlayerState(
mediaMetadata = player.mediaMetadata, mediaMetadata = player.mediaMetadata,
playWhenReady = player.playWhenReady, playWhenReady = player.playWhenReady,
repeatMode = player.repeatMode, repeatMode = player.repeatMode,
error = player.playerError, mediaItems = player.currentTimeline.mediaItems
mediaItems = player.currentTimeline.mediaItems,
volume = player.volume
) )
val progress: Float val progress: Float
@@ -56,12 +52,8 @@ fun rememberPlayerState(
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
val listener = object : Player.Listener, Runnable { val listener = object : Player.Listener, Runnable {
override fun onVolumeChanged(volume: Float) {
playerState = playerState?.copy(volume = volume)
}
override fun onPlaybackStateChanged(playbackState: Int) { override fun onPlaybackStateChanged(playbackState: Int) {
playerState = playerState?.copy(playbackState = playbackState, error = player.playerError) playerState = playerState?.copy(playbackState = playbackState)
if (playbackState == Player.STATE_READY) { if (playbackState == Player.STATE_READY) {
isSeeking = false isSeeking = false
@@ -88,10 +80,6 @@ fun rememberPlayerState(
playerState = playerState?.copy(repeatMode = repeatMode) playerState = playerState?.copy(repeatMode = repeatMode)
} }
override fun onPlayerError(playbackException: PlaybackException) {
playerState = playerState?.copy(error = playbackException)
}
override fun onTimelineChanged(timeline: Timeline, reason: Int) { override fun onTimelineChanged(timeline: Timeline, reason: Int) {
playerState = playerState?.copy( playerState = playerState?.copy(
mediaItems = timeline.mediaItems, mediaItems = timeline.mediaItems,
@@ -136,3 +124,69 @@ fun rememberPlayerState(
return playerState return playerState
} }
context(DisposableEffectScope)
fun Player.listener(listener: Player.Listener): DisposableEffectResult {
addListener(listener)
return onDispose {
removeListener(listener)
}
}
@Composable
fun rememberMediaItemIndex(player: Player): State<Int> {
val mediaItemIndexState = remember(player) {
mutableStateOf(player.currentMediaItemIndex)
}
DisposableEffect(player) {
player.listener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
mediaItemIndexState.value = player.currentMediaItemIndex
}
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
mediaItemIndexState.value = player.currentMediaItemIndex
}
})
}
return mediaItemIndexState
}
@Composable
fun rememberVolume(player: Player): State<Float> {
val volumeState = remember(player) {
mutableStateOf(player.volume)
}
DisposableEffect(player) {
player.listener(object : Player.Listener {
override fun onVolumeChanged(volume: Float) {
volumeState.value = volume
}
})
}
return volumeState
}
@Composable
fun rememberError(player: Player): State<PlaybackException?> {
val errorState = remember(player) {
mutableStateOf(player.playerError)
}
DisposableEffect(player) {
player.listener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
errorState.value = player.playerError
}
override fun onPlayerError(playbackException: PlaybackException) {
errorState.value = playbackException
}
})
}
return errorState
}