Add indicator for queued songs

This commit is contained in:
Damontecres 2026-03-06 16:20:54 -05:00
parent 7f75da2493
commit dc58b31e2b
No known key found for this signature in database
4 changed files with 72 additions and 30 deletions

View file

@ -5,6 +5,8 @@ import androidx.annotation.OptIn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
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
@ -38,6 +40,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.toUUID
import org.jellyfin.sdk.model.serializer.toUUIDOrNull import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
@ -67,6 +70,13 @@ class MusicService
), ),
).build() ).build()
.also { .also {
it.setAudioAttributes(
AudioAttributes
.Builder()
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build(),
false,
)
it.addListener(MusicPlayerListener(it, _state)) it.addListener(MusicPlayerListener(it, _state))
} }
} }
@ -236,11 +246,24 @@ class MusicService
} }
private suspend fun updateQueueSize() { private suspend fun updateQueueSize() {
// val ids =
// withContext(Dispatchers.Default) {
// (0..<player.mediaItemCount).map { player.getMediaItemAt(it).mediaId.toUUID() }
// }
val timeline = onMain { player.currentTimeline }
val window = Timeline.Window()
val ids =
(0..<timeline.windowCount)
.map {
timeline.getWindow(it, window)
window.mediaItem.mediaId.toUUID()
}.toSet()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_state.update { _state.update {
it.copy( it.copy(
queueVersion = it.queueVersion + 1, queueVersion = it.queueVersion + 1,
queueSize = player.mediaItemCount, queueSize = player.mediaItemCount,
queuedIds = ids,
) )
} }
} }
@ -298,9 +321,20 @@ data class MusicServiceState(
val status: NowPlayingStatus, val status: NowPlayingStatus,
val currentItemTitle: String?, val currentItemTitle: String?,
val loadingState: LoadingState = LoadingState.Pending, val loadingState: LoadingState = LoadingState.Pending,
val queuedIds: Set<UUID>,
) { ) {
companion object { companion object {
val EMPTY = MusicServiceState(0L, 0, 0, null, NowPlayingStatus.IDLE, null) val EMPTY =
MusicServiceState(
0L,
0,
0,
null,
NowPlayingStatus.IDLE,
null,
LoadingState.Pending,
emptySet(),
)
} }
} }

View file

@ -421,7 +421,7 @@ fun AlbumDetailsPage(
} }
item { item {
Text( Text(
text = stringResource(R.string.songs), text = stringResource(R.string.songs) + " (${state.songs.size})",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.padding(start = 8.dp), modifier = Modifier.padding(start = 8.dp),
@ -457,6 +457,7 @@ fun AlbumDetailsPage(
}, },
showArtist = state.isVariousArtists, showArtist = state.isVariousArtists,
isPlaying = song != null && currentMusic.currentItemId == song.id, isPlaying = song != null && currentMusic.currentItemId == song.id,
isQueued = song != null && song.id in currentMusic.queuedIds,
modifier = modifier =
Modifier Modifier
.fillMaxWidth(.75f) .fillMaxWidth(.75f)

View file

@ -443,6 +443,7 @@ fun ArtistDetailsPage(
}, },
showArtist = false, showArtist = false,
isPlaying = song != null && currentMusic.currentItemId == song.id, isPlaying = song != null && currentMusic.currentItemId == song.id,
isQueued = song != null && song.id in currentMusic.queuedIds,
modifier = modifier =
Modifier Modifier
.fillMaxWidth(.75f) .fillMaxWidth(.75f)

View file

@ -1,13 +1,15 @@
package com.github.damontecres.wholphin.ui.detail.music package com.github.damontecres.wholphin.ui.detail.music
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -15,16 +17,17 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
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.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem import androidx.tv.material3.ListItem
import androidx.tv.material3.ListItemDefaults import androidx.tv.material3.ListItemDefaults
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.enableMarquee import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.roundMinutes
import com.github.damontecres.wholphin.ui.roundSeconds import com.github.damontecres.wholphin.ui.roundSeconds
import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
@ -32,30 +35,6 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@Composable
fun SongListDisplay(
songs: List<BaseItem?>,
showArtist: Boolean,
onClick: (Int, BaseItem) -> Unit,
onLongClick: (Int, BaseItem) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier,
) {
itemsIndexed(songs) { index, song ->
SongListItem(
song = song,
onClick = { song?.let { onClick.invoke(index, song) } },
onLongClick = { song?.let { onLongClick.invoke(index, song) } },
showArtist = showArtist,
modifier = Modifier,
)
}
}
}
@Composable @Composable
fun SongListItem( fun SongListItem(
song: BaseItem?, song: BaseItem?,
@ -64,6 +43,7 @@ fun SongListItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showArtist: Boolean = false, showArtist: Boolean = false,
isPlaying: Boolean = false, isPlaying: Boolean = false,
isQueued: Boolean = false,
) = SongListItem( ) = SongListItem(
title = song?.title, title = song?.title,
artist = if (showArtist) song?.data?.albumArtist else null, artist = if (showArtist) song?.data?.albumArtist else null,
@ -81,6 +61,7 @@ fun SongListItem(
modifier = modifier, modifier = modifier,
showArtist = showArtist, showArtist = showArtist,
isPlaying = isPlaying, isPlaying = isPlaying,
isQueued = isQueued,
) )
@Composable @Composable
@ -94,6 +75,7 @@ fun SongListItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showArtist: Boolean = false, showArtist: Boolean = false,
isPlaying: Boolean = false, isPlaying: Boolean = false,
isQueued: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) { ) {
Row( Row(
@ -113,6 +95,19 @@ fun SongListItem(
imageVector = Icons.Default.PlayArrow, imageVector = Icons.Default.PlayArrow,
contentDescription = null, contentDescription = null,
) )
} else if (isQueued) {
// Icon(
// imageVector = Icons.Default.Add,
// contentDescription = null,
// )
Box(
modifier =
Modifier
.padding(horizontal = 8.dp)
.clip(CircleShape)
.background(LocalContentColor.current)
.size(8.dp),
)
} }
} }
} }
@ -181,6 +176,17 @@ fun SongListItemPreview() {
modifier = Modifier, modifier = Modifier,
showArtist = false, showArtist = false,
) )
SongListItem(
title = "Song title",
artist = "Artists",
indexNumber = 1,
runtime = 2.minutes + 30.seconds,
onClick = {},
onLongClick = { },
modifier = Modifier,
showArtist = false,
isQueued = true,
)
SongListItem( SongListItem(
title = "Song title", title = "Song title",
artist = "Artists", artist = "Artists",