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