Remove PlayerState class
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user