mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Refactoring queue
This commit is contained in:
parent
65b38d1231
commit
7703bcbd13
4 changed files with 89 additions and 56 deletions
|
|
@ -1,9 +1,11 @@
|
|||
package com.github.damontecres.wholphin.data.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Stable
|
||||
data class AudioItem(
|
||||
val id: UUID,
|
||||
val albumId: UUID?,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package com.github.damontecres.wholphin.services
|
|||
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Timeline
|
||||
|
|
@ -15,6 +17,7 @@ import com.github.damontecres.wholphin.data.model.AudioItem
|
|||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||
import com.github.damontecres.wholphin.ui.onMain
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.util.BlockingList
|
||||
import com.github.damontecres.wholphin.util.profile.Codec
|
||||
|
|
@ -30,6 +33,7 @@ import org.jellyfin.sdk.api.client.extensions.instantMixApi
|
|||
import org.jellyfin.sdk.api.client.extensions.universalAudioApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
|
@ -95,15 +99,7 @@ class MusicService
|
|||
player.shuffleModeEnabled = shuffled
|
||||
player.play()
|
||||
}
|
||||
(startIndex..items.lastIndex).forEach {
|
||||
val item = items.getBlocking(it)
|
||||
if (item != null && item.type == BaseItemKind.AUDIO) {
|
||||
val mediaItem = convert(item)
|
||||
withContext(Dispatchers.Main) {
|
||||
player.addMediaItem(mediaItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
addAllToQueue(items)
|
||||
}
|
||||
|
||||
suspend fun setQueue(
|
||||
|
|
@ -116,6 +112,7 @@ class MusicService
|
|||
.filter { it.type == BaseItemKind.AUDIO }
|
||||
.map(::convert)
|
||||
withContext(Dispatchers.Main) {
|
||||
updateQueueSize()
|
||||
player.setMediaItems(mediaItems)
|
||||
player.shuffleModeEnabled = shuffled
|
||||
player.play()
|
||||
|
|
@ -124,12 +121,17 @@ class MusicService
|
|||
|
||||
suspend fun addToQueue(
|
||||
item: BaseItem,
|
||||
position: Int? = null,
|
||||
index: Int? = null,
|
||||
) {
|
||||
if (item.type == BaseItemKind.AUDIO) {
|
||||
val mediaItem = convert(item)
|
||||
withContext(Dispatchers.Main) {
|
||||
player.addMediaItem(mediaItem)
|
||||
if (index != null) {
|
||||
player.addMediaItem(index, mediaItem)
|
||||
} else {
|
||||
player.addMediaItem(mediaItem)
|
||||
}
|
||||
updateQueueSize()
|
||||
if (player.mediaItemCount == 1) {
|
||||
// Start playing if this was the first time added
|
||||
player.play()
|
||||
|
|
@ -138,6 +140,22 @@ class MusicService
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun addAllToQueue(list: BlockingList<BaseItem?>) {
|
||||
list.indices
|
||||
.chunked(25)
|
||||
.forEach {
|
||||
val mediaItems =
|
||||
it.mapNotNull {
|
||||
list
|
||||
.getBlocking(it)
|
||||
?.takeIf { it.type == BaseItemKind.AUDIO }
|
||||
?.let(::convert)
|
||||
}
|
||||
onMain { player.addMediaItems(mediaItems) }
|
||||
}
|
||||
updateQueueSize()
|
||||
}
|
||||
|
||||
private fun convert(audio: BaseItem): MediaItem {
|
||||
val url =
|
||||
api.universalAudioApi.getUniversalAudioStreamUrl(
|
||||
|
|
@ -164,19 +182,26 @@ class MusicService
|
|||
.setTag(AudioItem.from(audio, imageUrl))
|
||||
.build()
|
||||
}
|
||||
|
||||
private suspend fun updateQueueSize() {
|
||||
withContext(Dispatchers.Main) {
|
||||
_state.update {
|
||||
it.copy(queueSize = player.mediaItemCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
data class MusicServiceState(
|
||||
val queue: List<AudioItem>,
|
||||
val queueSize: Int,
|
||||
val currentIndex: Int,
|
||||
val currentItemId: UUID?,
|
||||
val isPlaying: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
val EMPTY = MusicServiceState(emptyList(), 0, false)
|
||||
val EMPTY = MusicServiceState(0, 0, null, false)
|
||||
}
|
||||
|
||||
val currentItemId: UUID? get() = if (isPlaying) queue.getOrNull(currentIndex)?.id else null
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -190,7 +215,7 @@ private class MusicPlayerListener(
|
|||
Timber.v("MusicPlayerListener init")
|
||||
state.update {
|
||||
it.copy(
|
||||
queue = PlayerMediaItemList(player, player.mediaItemCount),
|
||||
queueSize = player.mediaItemCount,
|
||||
currentIndex = player.currentMediaItemIndex,
|
||||
isPlaying = player.isPlaying,
|
||||
)
|
||||
|
|
@ -211,11 +236,7 @@ private class MusicPlayerListener(
|
|||
reason: Int,
|
||||
) {
|
||||
Timber.v("MusicPlayerListener onMediaItemTransition")
|
||||
state.update {
|
||||
it.copy(
|
||||
currentIndex = player.currentMediaItemIndex,
|
||||
)
|
||||
}
|
||||
updateCurrentIndex()
|
||||
}
|
||||
|
||||
override fun onTimelineChanged(
|
||||
|
|
@ -224,40 +245,41 @@ private class MusicPlayerListener(
|
|||
) {
|
||||
Timber.v("MusicPlayerListener onTimelineChanged")
|
||||
if (reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) {
|
||||
state.update {
|
||||
it.copy(
|
||||
queue = PlayerMediaItemList(player, player.mediaItemCount),
|
||||
currentIndex = player.currentMediaItemIndex,
|
||||
)
|
||||
}
|
||||
updateCurrentIndex()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCurrentIndex() {
|
||||
state.update {
|
||||
it.copy(
|
||||
currentIndex = player.currentMediaItemIndex,
|
||||
currentItemId = player.getMediaItemAt(player.currentMediaItemIndex).mediaId.toUUIDOrNull(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PlayerMediaItemList(
|
||||
data class PlayerMediaItemList(
|
||||
private val player: Player,
|
||||
override val size: Int,
|
||||
) : AbstractList<AudioItem>() {
|
||||
override fun get(index: Int): AudioItem {
|
||||
) {
|
||||
val size: Int get() = player.mediaItemCount
|
||||
|
||||
operator fun get(index: Int): AudioItem {
|
||||
// Timber.v("get %s", index)
|
||||
return player.getMediaItemAt(index).localConfiguration?.tag as AudioItem
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is PlayerMediaItemList) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
if (size != other.size) return false
|
||||
if (player != other.player) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + size
|
||||
result = 31 * result + player.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberQueue(
|
||||
player: Player,
|
||||
queueSize: Int,
|
||||
): List<AudioItem> =
|
||||
remember(queueSize) {
|
||||
object : AbstractList<AudioItem>() {
|
||||
override val size: Int
|
||||
get() = player.mediaItemCount
|
||||
|
||||
override fun get(index: Int): AudioItem = player.getMediaItemAt(index).localConfiguration?.tag as AudioItem
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import androidx.media3.ui.compose.state.rememberNextButtonState
|
|||
import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
|
||||
import androidx.media3.ui.compose.state.rememberPreviousButtonState
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.data.model.AudioItem
|
||||
import com.github.damontecres.wholphin.services.MusicServiceState
|
||||
import com.github.damontecres.wholphin.ui.playback.ControllerViewState
|
||||
import com.github.damontecres.wholphin.ui.playback.PlaybackButtons
|
||||
|
|
@ -37,11 +38,11 @@ import kotlin.time.Duration.Companion.seconds
|
|||
fun NowPlayingOverlay(
|
||||
state: MusicServiceState,
|
||||
player: Player,
|
||||
current: AudioItem?,
|
||||
queue: List<AudioItem>,
|
||||
controllerViewState: ControllerViewState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val current = state.queue.getOrNull(state.currentIndex)
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
val playPauseState = rememberPlayPauseButtonState(player)
|
||||
|
|
@ -77,7 +78,7 @@ fun NowPlayingOverlay(
|
|||
PlaybackButtons(
|
||||
player = player,
|
||||
initialFocusRequester = focusRequester,
|
||||
onControllerInteraction = {},
|
||||
onControllerInteraction = { controllerViewState.pulseControls() },
|
||||
onPlaybackActionClick = {},
|
||||
showPlay = playPauseState.showPlay,
|
||||
previousEnabled = previousState.isEnabled,
|
||||
|
|
@ -87,21 +88,21 @@ fun NowPlayingOverlay(
|
|||
seekForward = 30.seconds,
|
||||
)
|
||||
}
|
||||
if (state.queue.isEmpty()) {
|
||||
if (queue.isEmpty()) {
|
||||
Text("No items")
|
||||
} else {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
itemsIndexed(state.queue) { index, song ->
|
||||
itemsIndexed(queue) { index, song ->
|
||||
SongListItem(
|
||||
title = song.title,
|
||||
artist = song.artistNames,
|
||||
indexNumber = index + 1,
|
||||
runtime = song.runtime?.roundSeconds,
|
||||
showArtist = true,
|
||||
isPlaying = state.currentIndex == index,
|
||||
isPlaying = current?.id == song.id,
|
||||
onClick = {
|
||||
player.seekTo(index, 0L)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import com.github.damontecres.wholphin.services.ImageUrlService
|
|||
import com.github.damontecres.wholphin.services.MusicService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||
import com.github.damontecres.wholphin.services.rememberQueue
|
||||
import com.github.damontecres.wholphin.ui.playback.ControllerViewState
|
||||
import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
|
|
@ -67,6 +68,9 @@ class NowPlayingViewModel
|
|||
val state get() = musicService.state
|
||||
val player get() = musicService.player
|
||||
|
||||
init {
|
||||
}
|
||||
|
||||
fun reportInteraction() {
|
||||
controllerViewState.pulseControls()
|
||||
}
|
||||
|
|
@ -83,7 +87,9 @@ fun NowPlayingPage(
|
|||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val player = viewModel.player
|
||||
val current = state.queue.getOrNull(state.currentIndex)
|
||||
val queue = rememberQueue(player, state.queueSize)
|
||||
val current = queue.getOrNull(state.currentIndex)
|
||||
|
||||
val controllerViewState = viewModel.controllerViewState
|
||||
val preferences by viewModel.userPreferencesService.flow.collectAsState(AppPreferences.getDefaultInstance())
|
||||
|
||||
|
|
@ -131,6 +137,8 @@ fun NowPlayingPage(
|
|||
NowPlayingOverlay(
|
||||
state = state,
|
||||
player = player,
|
||||
current = current,
|
||||
queue = queue,
|
||||
controllerViewState = controllerViewState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue