Tweak player code
This commit is contained in:
parent
7869f1a388
commit
6ebb5dfc65
8 changed files with 186 additions and 200 deletions
|
@ -81,7 +81,6 @@ import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlay
|
import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||||
import it.vfsfitvnm.vimusic.utils.getEnum
|
import it.vfsfitvnm.vimusic.utils.getEnum
|
||||||
import it.vfsfitvnm.vimusic.utils.intent
|
import it.vfsfitvnm.vimusic.utils.intent
|
||||||
import it.vfsfitvnm.vimusic.utils.listener
|
|
||||||
import it.vfsfitvnm.vimusic.utils.preferences
|
import it.vfsfitvnm.vimusic.utils.preferences
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey
|
import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey
|
||||||
import it.vfsfitvnm.innertube.Innertube
|
import it.vfsfitvnm.innertube.Innertube
|
||||||
|
@ -366,7 +365,7 @@ class MainActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
player.listener(object : Player.Listener {
|
val listener = object : Player.Listener {
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && mediaItem != null) {
|
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && mediaItem != null) {
|
||||||
if (mediaItem.mediaMetadata.extras?.getBoolean("isFromPersistentQueue") != true) {
|
if (mediaItem.mediaMetadata.extras?.getBoolean("isFromPersistentQueue") != true) {
|
||||||
|
@ -376,7 +375,11 @@ class MainActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
player.addListener(listener)
|
||||||
|
|
||||||
|
onDispose { player.removeListener(listener) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,10 +45,10 @@ import it.vfsfitvnm.vimusic.ui.components.SeekBar
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.IconButton
|
import it.vfsfitvnm.vimusic.ui.components.themed.IconButton
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.favoritesIcon
|
import it.vfsfitvnm.vimusic.ui.styling.favoritesIcon
|
||||||
|
import it.vfsfitvnm.vimusic.utils.DisposableListener
|
||||||
import it.vfsfitvnm.vimusic.utils.bold
|
import it.vfsfitvnm.vimusic.utils.bold
|
||||||
import it.vfsfitvnm.vimusic.utils.forceSeekToNext
|
import it.vfsfitvnm.vimusic.utils.forceSeekToNext
|
||||||
import it.vfsfitvnm.vimusic.utils.forceSeekToPrevious
|
import it.vfsfitvnm.vimusic.utils.forceSeekToPrevious
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberRepeatMode
|
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -70,7 +70,17 @@ fun Controls(
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
binder?.player ?: return
|
binder?.player ?: return
|
||||||
|
|
||||||
val repeatMode by rememberRepeatMode(binder.player)
|
var repeatMode by remember {
|
||||||
|
mutableStateOf(binder.player.repeatMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
binder.player.DisposableListener {
|
||||||
|
object : Player.Listener {
|
||||||
|
override fun onRepeatModeChanged(newRepeatMode: Int) {
|
||||||
|
repeatMode = newRepeatMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var scrubbingPosition by remember(mediaId) {
|
var scrubbingPosition by remember(mediaId) {
|
||||||
mutableStateOf<Long?>(null)
|
mutableStateOf<Long?>(null)
|
||||||
|
|
|
@ -29,6 +29,8 @@ import androidx.compose.foundation.text.BasicText
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.neverEqualPolicy
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
@ -44,6 +46,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import it.vfsfitvnm.innertube.models.NavigationEndpoint
|
||||||
import it.vfsfitvnm.route.OnGlobalRoute
|
import it.vfsfitvnm.route.OnGlobalRoute
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
@ -58,16 +61,15 @@ import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.collapsedPlayerProgressBar
|
import it.vfsfitvnm.vimusic.ui.styling.collapsedPlayerProgressBar
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.DisposableListener
|
||||||
import it.vfsfitvnm.vimusic.utils.forceSeekToNext
|
import it.vfsfitvnm.vimusic.utils.forceSeekToNext
|
||||||
import it.vfsfitvnm.vimusic.utils.isLandscape
|
import it.vfsfitvnm.vimusic.utils.isLandscape
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberMediaItem
|
import it.vfsfitvnm.vimusic.utils.positionAndDurationState
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberPositionAndDuration
|
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberShouldBePlaying
|
|
||||||
import it.vfsfitvnm.vimusic.utils.seamlessPlay
|
import it.vfsfitvnm.vimusic.utils.seamlessPlay
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
import it.vfsfitvnm.vimusic.utils.shouldBePlaying
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||||
import it.vfsfitvnm.innertube.models.NavigationEndpoint
|
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
@ExperimentalFoundationApi
|
@ExperimentalFoundationApi
|
||||||
|
@ -84,12 +86,33 @@ fun Player(
|
||||||
|
|
||||||
binder?.player ?: return
|
binder?.player ?: return
|
||||||
|
|
||||||
val nullableMediaItem by rememberMediaItem(binder.player)
|
var nullableMediaItem by remember {
|
||||||
|
mutableStateOf(binder.player.currentMediaItem, neverEqualPolicy())
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldBePlaying by remember {
|
||||||
|
mutableStateOf(binder.player.shouldBePlaying)
|
||||||
|
}
|
||||||
|
|
||||||
|
binder.player.DisposableListener {
|
||||||
|
object : Player.Listener {
|
||||||
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
|
nullableMediaItem = mediaItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||||
|
shouldBePlaying = binder.player.shouldBePlaying
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
|
shouldBePlaying = binder.player.shouldBePlaying
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val mediaItem = nullableMediaItem ?: return
|
val mediaItem = nullableMediaItem ?: return
|
||||||
|
|
||||||
val shouldBePlaying by rememberShouldBePlaying(binder.player)
|
val positionAndDuration by binder.player.positionAndDurationState()
|
||||||
val positionAndDuration by rememberPositionAndDuration(binder.player)
|
|
||||||
|
|
||||||
val windowInsets = WindowInsets.systemBars
|
val windowInsets = WindowInsets.systemBars
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,9 @@ import androidx.compose.foundation.text.BasicText
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
|
@ -40,6 +43,9 @@ import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.Timeline
|
||||||
import com.valentinilk.shimmer.shimmer
|
import com.valentinilk.shimmer.shimmer
|
||||||
import it.vfsfitvnm.reordering.ReorderingLazyColumn
|
import it.vfsfitvnm.reordering.ReorderingLazyColumn
|
||||||
import it.vfsfitvnm.reordering.animateItemPlacement
|
import it.vfsfitvnm.reordering.animateItemPlacement
|
||||||
|
@ -61,12 +67,12 @@ import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
|
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.DisposableListener
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex
|
import it.vfsfitvnm.vimusic.utils.shouldBePlaying
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberShouldBePlaying
|
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberWindows
|
|
||||||
import it.vfsfitvnm.vimusic.utils.shuffleQueue
|
import it.vfsfitvnm.vimusic.utils.shuffleQueue
|
||||||
import it.vfsfitvnm.vimusic.utils.smoothScrollToTop
|
import it.vfsfitvnm.vimusic.utils.smoothScrollToTop
|
||||||
|
import it.vfsfitvnm.vimusic.utils.windows
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@ExperimentalFoundationApi
|
@ExperimentalFoundationApi
|
||||||
|
@ -112,19 +118,52 @@ fun Queue(
|
||||||
|
|
||||||
binder?.player ?: return@BottomSheet
|
binder?.player ?: return@BottomSheet
|
||||||
|
|
||||||
|
val player = binder.player
|
||||||
|
|
||||||
val menuState = LocalMenuState.current
|
val menuState = LocalMenuState.current
|
||||||
|
|
||||||
val thumbnailSizeDp = Dimensions.thumbnails.song
|
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
val mediaItemIndex by rememberMediaItemIndex(binder.player)
|
var mediaItemIndex by remember {
|
||||||
val windows by rememberWindows(binder.player)
|
mutableStateOf(if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex)
|
||||||
val shouldBePlaying by rememberShouldBePlaying(binder.player)
|
}
|
||||||
|
|
||||||
|
var windows by remember {
|
||||||
|
mutableStateOf(player.currentTimeline.windows)
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldBePlaying by remember {
|
||||||
|
mutableStateOf(binder.player.shouldBePlaying)
|
||||||
|
}
|
||||||
|
|
||||||
|
player.DisposableListener {
|
||||||
|
object : Player.Listener {
|
||||||
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
|
mediaItemIndex =
|
||||||
|
if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||||
|
windows = timeline.windows
|
||||||
|
mediaItemIndex =
|
||||||
|
if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||||
|
shouldBePlaying = binder.player.shouldBePlaying
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
|
shouldBePlaying = binder.player.shouldBePlaying
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val reorderingState = rememberReorderingState(
|
val reorderingState = rememberReorderingState(
|
||||||
lazyListState = rememberLazyListState(initialFirstVisibleItemIndex = mediaItemIndex),
|
lazyListState = rememberLazyListState(initialFirstVisibleItemIndex = mediaItemIndex),
|
||||||
key = windows,
|
key = windows,
|
||||||
onDragEnd = binder.player::moveMediaItem,
|
onDragEnd = player::moveMediaItem,
|
||||||
extraItemCount = 0
|
extraItemCount = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -219,13 +258,13 @@ fun Queue(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (isPlayingThisMediaItem) {
|
if (isPlayingThisMediaItem) {
|
||||||
if (shouldBePlaying) {
|
if (shouldBePlaying) {
|
||||||
binder.player.pause()
|
player.pause()
|
||||||
} else {
|
} else {
|
||||||
binder.player.play()
|
player.play()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
binder.player.playWhenReady = true
|
player.playWhenReady = true
|
||||||
binder.player.seekToDefaultPosition(window.firstPeriodIndex)
|
player.seekToDefaultPosition(window.firstPeriodIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -266,7 +305,7 @@ fun Queue(
|
||||||
reorderingState.coroutineScope.launch {
|
reorderingState.coroutineScope.launch {
|
||||||
reorderingState.lazyListState.smoothScrollToTop()
|
reorderingState.lazyListState.smoothScrollToTop()
|
||||||
}.invokeOnCompletion {
|
}.invokeOnCompletion {
|
||||||
binder.player.shuffleQueue()
|
player.shuffleQueue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,8 +26,12 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
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 it.vfsfitvnm.innertube.Innertube
|
||||||
|
import it.vfsfitvnm.innertube.models.bodies.PlayerBody
|
||||||
|
import it.vfsfitvnm.innertube.requests.player
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.models.Format
|
import it.vfsfitvnm.vimusic.models.Format
|
||||||
|
@ -35,12 +39,9 @@ import it.vfsfitvnm.vimusic.query
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
|
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.overlay
|
import it.vfsfitvnm.vimusic.ui.styling.overlay
|
||||||
|
import it.vfsfitvnm.vimusic.utils.DisposableListener
|
||||||
import it.vfsfitvnm.vimusic.utils.color
|
import it.vfsfitvnm.vimusic.utils.color
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberVolume
|
|
||||||
import it.vfsfitvnm.innertube.Innertube
|
|
||||||
import it.vfsfitvnm.innertube.models.bodies.PlayerBody
|
|
||||||
import it.vfsfitvnm.innertube.requests.player
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
@ -70,7 +71,17 @@ fun StatsForNerds(
|
||||||
Database.format(mediaId).distinctUntilChanged()
|
Database.format(mediaId).distinctUntilChanged()
|
||||||
}.collectAsState(initial = null, context = Dispatchers.IO)
|
}.collectAsState(initial = null, context = Dispatchers.IO)
|
||||||
|
|
||||||
val volume by rememberVolume(binder.player)
|
var volume by remember {
|
||||||
|
mutableStateOf(binder.player.volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
binder.player.DisposableListener {
|
||||||
|
object : Player.Listener {
|
||||||
|
override fun onVolumeChanged(newVolume: Float) {
|
||||||
|
volume = newVolume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DisposableEffect(mediaId) {
|
DisposableEffect(mediaId) {
|
||||||
val listener = object : Cache.Listener {
|
val listener = object : Cache.Listener {
|
||||||
|
@ -193,7 +204,8 @@ fun StatsForNerds(
|
||||||
onClick = {
|
onClick = {
|
||||||
query {
|
query {
|
||||||
runBlocking(Dispatchers.IO) {
|
runBlocking(Dispatchers.IO) {
|
||||||
Innertube.player(PlayerBody(videoId = mediaId))
|
Innertube
|
||||||
|
.player(PlayerBody(videoId = mediaId))
|
||||||
?.map { response ->
|
?.map { response ->
|
||||||
response.streamingData?.adaptiveFormats
|
response.streamingData?.adaptiveFormats
|
||||||
?.findLast { format ->
|
?.findLast { format ->
|
||||||
|
@ -205,7 +217,9 @@ fun StatsForNerds(
|
||||||
itag = format.itag,
|
itag = format.itag,
|
||||||
mimeType = format.mimeType,
|
mimeType = format.mimeType,
|
||||||
bitrate = format.bitrate,
|
bitrate = format.bitrate,
|
||||||
loudnessDb = response.playerConfig?.audioConfig?.loudnessDb?.toFloat()?.plus(7),
|
loudnessDb = response.playerConfig?.audioConfig?.loudnessDb
|
||||||
|
?.toFloat()
|
||||||
|
?.plus(7),
|
||||||
contentLength = format.contentLength,
|
contentLength = format.contentLength,
|
||||||
lastModified = format.lastModified
|
lastModified = format.lastModified
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,12 +17,18 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.PlaybackException
|
||||||
|
import androidx.media3.common.Player
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
@ -34,9 +40,8 @@ import it.vfsfitvnm.vimusic.service.VideoIdMismatchException
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberError
|
import it.vfsfitvnm.vimusic.utils.currentWindow
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberMediaItem
|
import it.vfsfitvnm.vimusic.utils.DisposableListener
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex
|
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
import java.nio.channels.UnresolvedAddressException
|
import java.nio.channels.UnresolvedAddressException
|
||||||
|
@ -57,17 +62,38 @@ fun Thumbnail(
|
||||||
it to (it - 64.dp).px
|
it to (it - 64.dp).px
|
||||||
}
|
}
|
||||||
|
|
||||||
val mediaItemIndex by rememberMediaItemIndex(player)
|
var nullableWindow by remember {
|
||||||
val mediaItem by rememberMediaItem(player)
|
mutableStateOf(player.currentWindow)
|
||||||
|
}
|
||||||
|
|
||||||
val error by rememberError(player)
|
var error by remember {
|
||||||
|
mutableStateOf<PlaybackException?>(player.playerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
player.DisposableListener {
|
||||||
|
object : Player.Listener {
|
||||||
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
|
nullableWindow = player.currentWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
|
error = player.playerError
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayerError(playbackException: PlaybackException) {
|
||||||
|
error = playbackException
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val window = nullableWindow ?: return
|
||||||
|
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = mediaItemIndex to mediaItem,
|
targetState = window,
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
val duration = 500
|
val duration = 500
|
||||||
val slideDirection =
|
val slideDirection =
|
||||||
if (targetState.first > initialState.first) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right
|
if (targetState.firstPeriodIndex > initialState.firstPeriodIndex) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right
|
||||||
|
|
||||||
ContentTransform(
|
ContentTransform(
|
||||||
targetContentEnter = slideIntoContainer(
|
targetContentEnter = slideIntoContainer(
|
||||||
|
@ -92,9 +118,7 @@ fun Thumbnail(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) { (_, currentMediaItem) ->
|
) {currentWindow ->
|
||||||
val currentMediaItem = currentMediaItem ?: return@AnimatedContent
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
|
@ -102,7 +126,7 @@ fun Thumbnail(
|
||||||
.size(thumbnailSizeDp)
|
.size(thumbnailSizeDp)
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = currentMediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx),
|
model = currentWindow.mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -116,23 +140,23 @@ fun Thumbnail(
|
||||||
)
|
)
|
||||||
|
|
||||||
Lyrics(
|
Lyrics(
|
||||||
mediaId = currentMediaItem.mediaId,
|
mediaId = currentWindow.mediaItem.mediaId,
|
||||||
isDisplayed = isShowingLyrics && error == null,
|
isDisplayed = isShowingLyrics && error == null,
|
||||||
onDismiss = { onShowLyrics(false) },
|
onDismiss = { onShowLyrics(false) },
|
||||||
onLyricsUpdate = { areSynchronized, mediaId, lyrics ->
|
onLyricsUpdate = { areSynchronized, mediaId, lyrics ->
|
||||||
query {
|
query {
|
||||||
if (areSynchronized) {
|
if (areSynchronized) {
|
||||||
if (Database.updateSynchronizedLyrics(mediaId, lyrics) == 0) {
|
if (Database.updateSynchronizedLyrics(mediaId, lyrics) == 0) {
|
||||||
if (mediaId == currentMediaItem.mediaId) {
|
if (mediaId == currentWindow.mediaItem.mediaId) {
|
||||||
Database.insert(currentMediaItem) { song ->
|
Database.insert(currentWindow.mediaItem) { song ->
|
||||||
song.copy(synchronizedLyrics = lyrics)
|
song.copy(synchronizedLyrics = lyrics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (Database.updateLyrics(mediaId, lyrics) == 0) {
|
if (Database.updateLyrics(mediaId, lyrics) == 0) {
|
||||||
if (mediaId == currentMediaItem.mediaId) {
|
if (mediaId == currentWindow.mediaItem.mediaId) {
|
||||||
Database.insert(currentMediaItem) { song ->
|
Database.insert(currentWindow.mediaItem) { song ->
|
||||||
song.copy(lyrics = lyrics)
|
song.copy(lyrics = lyrics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,12 +165,12 @@ fun Thumbnail(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
size = thumbnailSizeDp,
|
size = thumbnailSizeDp,
|
||||||
mediaMetadataProvider = currentMediaItem::mediaMetadata,
|
mediaMetadataProvider = currentWindow.mediaItem::mediaMetadata,
|
||||||
durationProvider = player::getDuration,
|
durationProvider = player::getDuration,
|
||||||
)
|
)
|
||||||
|
|
||||||
StatsForNerds(
|
StatsForNerds(
|
||||||
mediaId = currentMediaItem.mediaId,
|
mediaId = currentWindow.mediaItem.mediaId,
|
||||||
isDisplayed = isShowingStatsForNerds && error == null,
|
isDisplayed = isShowingStatsForNerds && error == null,
|
||||||
onDismiss = { onShowStatsForNerds(false) }
|
onDismiss = { onShowStatsForNerds(false) }
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,6 +5,9 @@ import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Timeline
|
import androidx.media3.common.Timeline
|
||||||
|
|
||||||
|
val Player.currentWindow: Timeline.Window?
|
||||||
|
get() = if (mediaItemCount == 0) null else currentTimeline.getWindow(currentMediaItemIndex, Timeline.Window())
|
||||||
|
|
||||||
val Timeline.mediaItems: List<MediaItem>
|
val Timeline.mediaItems: List<MediaItem>
|
||||||
get() = List(windowCount) {
|
get() = List(windowCount) {
|
||||||
getWindow(it, Timeline.Window()).mediaItem
|
getWindow(it, Timeline.Window()).mediaItem
|
||||||
|
|
|
@ -2,127 +2,33 @@ package it.vfsfitvnm.vimusic.utils
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.DisposableEffectResult
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.DisposableEffectScope
|
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.neverEqualPolicy
|
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.PlaybackException
|
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Timeline
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
context(DisposableEffectScope)
|
@Composable
|
||||||
fun Player.listener(listener: Player.Listener): DisposableEffectResult {
|
inline fun Player.DisposableListener(crossinline listenerProvider: () -> Player.Listener) {
|
||||||
addListener(listener)
|
DisposableEffect(this) {
|
||||||
return onDispose {
|
val listener = listenerProvider()
|
||||||
removeListener(listener)
|
addListener(listener)
|
||||||
|
onDispose { removeListener(listener) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberMediaItemIndex(player: Player): State<Int> {
|
fun Player.positionAndDurationState(): State<Pair<Long, Long>> {
|
||||||
val mediaItemIndexState = remember(player) {
|
val state = remember {
|
||||||
mutableStateOf(if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex)
|
mutableStateOf(currentPosition to duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(player) {
|
LaunchedEffect(this) {
|
||||||
player.listener(object : Player.Listener {
|
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
||||||
mediaItemIndexState.value =
|
|
||||||
if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
|
||||||
mediaItemIndexState.value =
|
|
||||||
if (player.mediaItemCount == 0) -1 else player.currentMediaItemIndex
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return mediaItemIndexState
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun rememberMediaItem(player: Player): State<MediaItem?> {
|
|
||||||
val state = remember(player) {
|
|
||||||
mutableStateOf(player.currentMediaItem, neverEqualPolicy())
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(player) {
|
|
||||||
player.listener(object : Player.Listener {
|
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
||||||
state.value = mediaItem
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun rememberWindows(player: Player): State<List<Timeline.Window>> {
|
|
||||||
val windowsState = remember(player) {
|
|
||||||
mutableStateOf(player.currentTimeline.windows)
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(player) {
|
|
||||||
player.listener(object : Player.Listener {
|
|
||||||
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
|
||||||
windowsState.value = timeline.windows
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return windowsState
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun rememberShouldBePlaying(player: Player): State<Boolean> {
|
|
||||||
val state = remember(player) {
|
|
||||||
mutableStateOf(!(player.playbackState == Player.STATE_ENDED || !player.playWhenReady))
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(player) {
|
|
||||||
player.listener(object : Player.Listener {
|
|
||||||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
|
||||||
state.value = !(player.playbackState == Player.STATE_ENDED || !playWhenReady)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
|
||||||
state.value = !(playbackState == Player.STATE_ENDED || !player.playWhenReady)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
var isSeeking = false
|
||||||
|
|
||||||
val listener = object : Player.Listener {
|
val listener = object : Player.Listener {
|
||||||
|
@ -133,7 +39,7 @@ fun rememberPositionAndDuration(player: Player): State<Pair<Long, Long>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
value = player.currentPosition to value.second
|
state.value = currentPosition to state.value.second
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPositionDiscontinuity(
|
override fun onPositionDiscontinuity(
|
||||||
|
@ -143,65 +49,29 @@ fun rememberPositionAndDuration(player: Player): State<Pair<Long, Long>> {
|
||||||
) {
|
) {
|
||||||
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
||||||
isSeeking = true
|
isSeeking = true
|
||||||
value = player.currentPosition to player.duration
|
state.value = currentPosition to duration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
player.addListener(listener)
|
addListener(listener)
|
||||||
|
|
||||||
val pollJob = launch {
|
val pollJob = launch {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
delay(500)
|
delay(500)
|
||||||
if (!isSeeking) {
|
if (!isSeeking) {
|
||||||
value = player.currentPosition to player.duration
|
state.value = currentPosition to duration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
awaitDispose {
|
try {
|
||||||
|
suspendCancellableCoroutine<Nothing> { }
|
||||||
|
} finally {
|
||||||
pollJob.cancel()
|
pollJob.cancel()
|
||||||
player.removeListener(listener)
|
removeListener(listener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue