Updates to playlists, add more button on album/artist pages

This commit is contained in:
Damontecres 2026-03-06 20:25:41 -05:00
parent 8ddd4bd747
commit 5f5b07e861
No known key found for this signature in database
6 changed files with 182 additions and 28 deletions

View file

@ -186,7 +186,7 @@ class MusicService
updateQueueSize()
if (player.mediaItemCount == 1) {
// Start playing if this was the first time added
player.play()
start()
}
}
}
@ -295,7 +295,12 @@ class MusicService
suspend fun playNext(song: BaseItem) {
val mediaItem = convert(song)
onMain { player.addMediaItem(state.value.currentIndex + 1, mediaItem) }
onMain {
player.addMediaItem(state.value.currentIndex + 1, mediaItem)
if (player.mediaItemCount == 1) {
start()
}
}
updateQueueSize()
}

View file

@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
@ -44,6 +45,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
@ -67,6 +69,7 @@ import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.TimeFormatter
import com.github.damontecres.wholphin.ui.cards.ItemCardImage
import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.DialogItem
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
@ -77,9 +80,12 @@ import com.github.damontecres.wholphin.ui.components.FilterByButton
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.OverviewText
import com.github.damontecres.wholphin.ui.components.SortByButton
import com.github.damontecres.wholphin.ui.components.TextButton
import com.github.damontecres.wholphin.ui.data.BoxSetSortOptions
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.music.MusicQueueMarker
import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.equalsNotNull
import com.github.damontecres.wholphin.ui.formatDateTime
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.launchDefault
@ -224,17 +230,24 @@ class PlaylistViewModel
}
}
/**
* This method tries to determine the [MediaType] of a playlist
*
* In theory, the server will set the type, but sometime it doesn't
*/
private suspend fun determineMediaType() {
// Use the type the server says
var mediaType =
super.item.value
?.data
?.mediaType ?: MediaType.UNKNOWN
mediaType =
if (mediaType == MediaType.UNKNOWN) {
// Otherwise, if a most of the list is one type, we can assume that type
val pager = (this@PlaylistViewModel.items.value as? ApiRequestPager<*>)
if (pager != null && pager.size < 50) {
if (pager != null && pager.size <= 50) {
val types =
(0..<50).groupBy { index ->
(0..<50.coerceAtMost(pager.size)).groupBy { index ->
val pagerItem = pager.getBlocking(index)
when (pagerItem?.type) {
BaseItemKind.AUDIO -> MediaType.AUDIO
@ -259,6 +272,7 @@ class PlaylistViewModel
} else {
mediaType
}
Timber.d("mediaType=%s", mediaType)
playlistMediaType.update { mediaType }
}
@ -312,7 +326,7 @@ fun PlaylistDetails(
val playlistMediaType by viewModel.playlistMediaType.collectAsState()
var longClickDialog by remember { mutableStateOf<DialogParams?>(null) }
var showConfirmTypeDialog by remember { mutableStateOf(false) }
var showConfirmTypeDialog by remember { mutableStateOf<Pair<Int, Boolean>?>(null) }
val goToString = stringResource(R.string.go_to)
val playFromHereString = stringResource(R.string.play_from_here)
@ -340,7 +354,7 @@ fun PlaylistDetails(
}
else -> {
showConfirmTypeDialog = true
showConfirmTypeDialog = Pair(index, shuffle)
}
}
}
@ -390,6 +404,12 @@ fun PlaylistDetails(
onDismissRequest = { longClickDialog = null },
)
}
showConfirmTypeDialog?.let { (index, shuffle) ->
ConfirmMediaTypeDialog(
onConfirm = { mediaType -> play(index, shuffle, mediaType) },
onCancel = { showConfirmTypeDialog = null },
)
}
}
@Composable
@ -513,10 +533,18 @@ fun PlaylistDetailsContent(
onLongClickIndex.invoke(index, item)
}
},
isPlaying =
equalsNotNull(
musicState.currentItemId,
item?.id,
),
isQueued = item?.id in musicState.queuedIds,
modifier =
Modifier
.height(80.dp)
.ifElse(
item?.type != BaseItemKind.AUDIO,
Modifier.height(80.dp),
).ifElse(
index == savedIndex,
Modifier.focusRequester(focus),
).onFocusChanged {
@ -646,6 +674,8 @@ fun PlaylistItem(
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
isPlaying: Boolean = false,
isQueued: Boolean = false,
) {
val focused by interactionSource.collectIsFocusedAsState()
ListItem(
@ -658,6 +688,12 @@ fun PlaylistItem(
text = item?.title ?: "",
modifier = Modifier.enableMarquee(focused),
)
if (item?.type == BaseItemKind.AUDIO) {
MusicQueueMarker(
isPlaying = isPlaying,
isQueued = isQueued,
)
}
},
supportingContent = {
Text(
@ -714,3 +750,47 @@ fun PlaylistItem(
modifier = modifier,
)
}
@Composable
fun ConfirmMediaTypeDialog(
onConfirm: (MediaType) -> Unit,
onCancel: () -> Unit,
) {
BasicDialog(
onDismissRequest = onCancel,
properties = DialogProperties(),
elevation = 3.dp,
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(16.dp),
modifier = Modifier.wrapContentSize(),
) {
item {
Text(
text = stringResource(R.string.play_as_type),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier.fillParentMaxWidth(),
)
}
item {
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxWidth(),
) {
TextButton(
stringRes = R.string.audio,
onClick = { onConfirm.invoke(MediaType.AUDIO) },
)
TextButton(
stringRes = R.string.video,
onClick = { onConfirm.invoke(MediaType.VIDEO) },
)
}
}
}
}
}

View file

@ -455,6 +455,23 @@ fun AlbumDetailsPage(
)
}
},
onClickMore = {
if (song != null) {
moreDialog =
DialogParams(
fromLongClick = false,
title = song.name ?: "",
items =
buildMoreDialogForMusic(
context = context,
actions = moreDialogActions,
item = song,
index = index,
canRemove = false,
),
)
}
},
showArtist = state.isVariousArtists,
isPlaying = song != null && currentMusic.currentItemId == song.id,
isQueued = song != null && song.id in currentMusic.queuedIds,

View file

@ -441,6 +441,23 @@ fun ArtistDetailsPage(
)
}
},
onClickMore = {
if (song != null) {
moreDialog =
DialogParams(
fromLongClick = false,
title = song.name ?: "",
items =
buildMoreDialogForMusic(
context = context,
actions = moreDialogActions,
item = song,
index = index,
canRemove = false,
),
)
}
},
showArtist = false,
isPlaying = song != null && currentMusic.currentItemId == song.id,
isQueued = song != null && song.id in currentMusic.queuedIds,

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -18,14 +19,17 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem
import androidx.tv.material3.ListItemDefaults
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.components.Button
import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.roundSeconds
@ -40,6 +44,7 @@ fun SongListItem(
song: BaseItem?,
onClick: () -> Unit,
onLongClick: () -> Unit,
onClickMore: () -> Unit,
modifier: Modifier = Modifier,
showArtist: Boolean = false,
isPlaying: Boolean = false,
@ -62,6 +67,8 @@ fun SongListItem(
showArtist = showArtist,
isPlaying = isPlaying,
isQueued = isQueued,
showMoreButton = true,
onClickMore = onClickMore,
)
@Composable
@ -76,6 +83,8 @@ fun SongListItem(
showArtist: Boolean = false,
isPlaying: Boolean = false,
isQueued: Boolean = false,
showMoreButton: Boolean = false,
onClickMore: () -> Unit = {},
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
Row(
@ -90,25 +99,10 @@ fun SongListItem(
Text(
text = indexNumber?.toString() ?: "",
)
if (isPlaying) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = null,
MusicQueueMarker(
isPlaying = isPlaying,
isQueued = isQueued,
)
} 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),
)
}
}
}
val headlineContent = @Composable {
@ -140,7 +134,7 @@ fun SongListItem(
},
trailingContent = trailingContent,
scale = ListItemDefaults.scale(1f, 1f, .95f),
modifier = Modifier,
modifier = Modifier.weight(1f),
)
} else {
ListItem(
@ -153,14 +147,53 @@ fun SongListItem(
supportingContent = null,
trailingContent = trailingContent,
scale = ListItemDefaults.scale(1f, 1f, .95f),
modifier = Modifier,
modifier = Modifier.weight(1f),
)
}
if (showMoreButton) {
Button(
onClick = onClickMore,
enabled = true,
modifier = Modifier.size(32.dp),
) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(R.string.more),
)
}
}
}
}
val BaseItem.artistsString: String? get() = data.artists?.letNotEmpty { it.joinToString(", ") }
/**
* Add an indicator for if the item is currently playing or queued
*/
@Composable
fun MusicQueueMarker(
isPlaying: Boolean,
isQueued: Boolean,
modifier: Modifier = Modifier,
) {
if (isPlaying) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = null,
modifier = modifier,
)
} else if (isQueued) {
Box(
modifier =
modifier
.padding(horizontal = 8.dp)
.clip(CircleShape)
.background(LocalContentColor.current)
.size(8.dp),
)
}
}
@PreviewTvSpec
@Composable
fun SongListItemPreview() {
@ -186,6 +219,7 @@ fun SongListItemPreview() {
modifier = Modifier,
showArtist = false,
isQueued = true,
showMoreButton = true,
)
SongListItem(
title = "Song title",

View file

@ -741,6 +741,7 @@
<string name="show_album_cover">Show album cover</string>
<string name="show_visualizer">Show visualizer</string>
<string name="show_backdrop">Show backdrop</string>
<string name="play_as_type">Play as?</string>
</resources>