Make Thumbnail composable smart recompose
This commit is contained in:
@@ -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,169 +366,13 @@ private fun Thumbnail(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
)
|
)
|
||||||
|
|
||||||
AnimatedVisibility(
|
StatsForNerds(
|
||||||
visible = isShowingStatsForNerds,
|
mediaId = mediaItem.mediaId,
|
||||||
enter = fadeIn(),
|
isDisplayed = isShowingStatsForNerds,
|
||||||
exit = fadeOut(),
|
onDismiss = {
|
||||||
) {
|
isShowingStatsForNerds = false
|
||||||
val key = playerState.mediaItem.mediaId
|
|
||||||
|
|
||||||
var cachedBytes by remember(key) {
|
|
||||||
mutableStateOf(binder.cache.getCachedBytes(key, 0, -1))
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
val format by remember(key) {
|
|
||||||
Database.format(key)
|
|
||||||
}.collectAsState(initial = null, context = Dispatchers.IO)
|
|
||||||
|
|
||||||
DisposableEffect(key) {
|
|
||||||
val listener = object : Cache.Listener {
|
|
||||||
override fun onSpanAdded(cache: Cache, span: CacheSpan) {
|
|
||||||
cachedBytes += span.length
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSpanRemoved(cache: Cache, span: CacheSpan) {
|
|
||||||
cachedBytes -= span.length
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSpanTouched(
|
|
||||||
cache: Cache,
|
|
||||||
oldSpan: CacheSpan,
|
|
||||||
newSpan: CacheSpan
|
|
||||||
) = Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
binder.cache.addListener(key, listener)
|
|
||||||
|
|
||||||
onDispose {
|
|
||||||
binder.cache.removeListener(key, listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.SpaceBetween,
|
|
||||||
modifier = Modifier
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
isShowingStatsForNerds = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.background(Color.Black.copy(alpha = 0.8f))
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(all = 16.dp)
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
BasicText(
|
|
||||||
text = "Id",
|
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = "Volume",
|
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = "Loudness",
|
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = "Bitrate",
|
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = "Size",
|
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = "Cached",
|
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
BasicText(
|
|
||||||
text = playerState.mediaItem.mediaId,
|
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = "${playerState.volume.times(100).roundToInt()}%",
|
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = format?.loudnessDb?.let { loudnessDb ->
|
|
||||||
"%.2f dB".format(loudnessDb)
|
|
||||||
} ?: "Unknown",
|
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = format?.bitrate?.let { bitrate ->
|
|
||||||
"${bitrate / 1000} kbps"
|
|
||||||
} ?: "Unknown",
|
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = format?.contentLength?.let { contentLength ->
|
|
||||||
Formatter.formatShortFileSize(
|
|
||||||
context,
|
|
||||||
contentLength
|
|
||||||
)
|
|
||||||
} ?: "Unknown",
|
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = buildString {
|
|
||||||
append(Formatter.formatShortFileSize(context, cachedBytes))
|
|
||||||
|
|
||||||
format?.contentLength?.let { contentLength ->
|
|
||||||
append(" (${(cachedBytes.toFloat() / contentLength * 100).roundToInt()}%)")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (format != null && format?.itag == null) {
|
|
||||||
BasicText(
|
|
||||||
text = "FETCH MISSING DATA",
|
|
||||||
style = typography.xxs.semiBold.color(BlackColorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = {
|
|
||||||
query {
|
|
||||||
runBlocking(Dispatchers.IO) {
|
|
||||||
YouTube.player(key)?.map { response ->
|
|
||||||
response.streamingData?.adaptiveFormats?.findLast { format ->
|
|
||||||
format.itag == 251 || format.itag == 140
|
|
||||||
}?.let { format ->
|
|
||||||
Format(
|
|
||||||
songId = key,
|
|
||||||
itag = format.itag,
|
|
||||||
mimeType = format.mimeType,
|
|
||||||
bitrate = format.bitrate,
|
|
||||||
loudnessDb = response.playerConfig?.audioConfig?.loudnessDb?.toFloat(),
|
|
||||||
contentLength = format.contentLength,
|
|
||||||
lastModified = format.lastModified
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}?.getOrNull()?.let(Database::insert)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(all = 16.dp)
|
|
||||||
.align(Alignment.End)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -549,7 +384,7 @@ private fun Thumbnail(
|
|||||||
.size(thumbnailSizeDp)
|
.size(thumbnailSizeDp)
|
||||||
) {
|
) {
|
||||||
LoadingOrError(
|
LoadingOrError(
|
||||||
errorMessage = playerState.error.javaClass.canonicalName,
|
errorMessage = error?.javaClass?.canonicalName,
|
||||||
onRetry = {
|
onRetry = {
|
||||||
player.playWhenReady = true
|
player.playWhenReady = true
|
||||||
player.prepare()
|
player.prepare()
|
||||||
@@ -559,6 +394,188 @@ private fun Thumbnail(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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(
|
||||||
|
visible = isDisplayed,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
) {
|
||||||
|
var cachedBytes by remember(mediaId) {
|
||||||
|
mutableStateOf(binder.cache.getCachedBytes(mediaId, 0, -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
val format by remember(mediaId) {
|
||||||
|
Database.format(mediaId).distinctUntilChanged()
|
||||||
|
}.collectAsState(initial = null, context = Dispatchers.IO)
|
||||||
|
|
||||||
|
val volume by rememberVolume(binder.player)
|
||||||
|
|
||||||
|
DisposableEffect(mediaId) {
|
||||||
|
val listener = object : Cache.Listener {
|
||||||
|
override fun onSpanAdded(cache: Cache, span: CacheSpan) {
|
||||||
|
cachedBytes += span.length
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSpanRemoved(cache: Cache, span: CacheSpan) {
|
||||||
|
cachedBytes -= span.length
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSpanTouched(
|
||||||
|
cache: Cache,
|
||||||
|
oldSpan: CacheSpan,
|
||||||
|
newSpan: CacheSpan
|
||||||
|
) = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
binder.cache.addListener(mediaId, listener)
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
binder.cache.removeListener(mediaId, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = modifier
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onTap = {
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.background(Color.Black.copy(alpha = 0.8f))
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
BasicText(
|
||||||
|
text = "Id",
|
||||||
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = "Volume",
|
||||||
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = "Loudness",
|
||||||
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = "Bitrate",
|
||||||
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = "Size",
|
||||||
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = "Cached",
|
||||||
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
BasicText(
|
||||||
|
text = mediaId,
|
||||||
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = "${volume.times(100).roundToInt()}%",
|
||||||
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = format?.loudnessDb?.let { loudnessDb ->
|
||||||
|
"%.2f dB".format(loudnessDb)
|
||||||
|
} ?: "Unknown",
|
||||||
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = format?.bitrate?.let { bitrate ->
|
||||||
|
"${bitrate / 1000} kbps"
|
||||||
|
} ?: "Unknown",
|
||||||
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = format?.contentLength?.let { contentLength ->
|
||||||
|
Formatter.formatShortFileSize(
|
||||||
|
context,
|
||||||
|
contentLength
|
||||||
|
)
|
||||||
|
} ?: "Unknown",
|
||||||
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = buildString {
|
||||||
|
append(Formatter.formatShortFileSize(context, cachedBytes))
|
||||||
|
|
||||||
|
format?.contentLength?.let { contentLength ->
|
||||||
|
append(" (${(cachedBytes.toFloat() / contentLength * 100).roundToInt()}%)")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format != null && format?.itag == null) {
|
||||||
|
BasicText(
|
||||||
|
text = "FETCH MISSING DATA",
|
||||||
|
style = typography.xxs.semiBold.color(BlackColorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
indication = rememberRipple(bounded = true),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = {
|
||||||
|
query {
|
||||||
|
runBlocking(Dispatchers.IO) {
|
||||||
|
YouTube
|
||||||
|
.player(mediaId)
|
||||||
|
?.map { response ->
|
||||||
|
response.streamingData?.adaptiveFormats
|
||||||
|
?.findLast { format ->
|
||||||
|
format.itag == 251 || format.itag == 140
|
||||||
|
}
|
||||||
|
?.let { format ->
|
||||||
|
it.vfsfitvnm.vimusic.models.Format(
|
||||||
|
songId = mediaId,
|
||||||
|
itag = format.itag,
|
||||||
|
mimeType = format.mimeType,
|
||||||
|
bitrate = format.bitrate,
|
||||||
|
loudnessDb = response.playerConfig?.audioConfig?.loudnessDb?.toFloat(),
|
||||||
|
contentLength = format.contentLength,
|
||||||
|
lastModified = format.lastModified
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?.getOrNull()
|
||||||
|
?.let(Database::insert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.align(Alignment.End)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Controls(
|
private fun Controls(
|
||||||
playerState: PlayerState,
|
playerState: PlayerState,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user