diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt index e5083790..0c67c505 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt @@ -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() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt index 047f85c1..d2bc993c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt @@ -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(null) } - var showConfirmTypeDialog by remember { mutableStateOf(false) } + var showConfirmTypeDialog by remember { mutableStateOf?>(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) }, + ) + } + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt index 8586f1f0..c1bf2505 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt index 6abe02b9..8983e0c6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/ArtistDetailsPage.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/SongListDisplay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/SongListDisplay.kt index 420a99d0..82846282 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/SongListDisplay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/SongListDisplay.kt @@ -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, - ) - } 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), - ) - } + MusicQueueMarker( + isPlaying = isPlaying, + isQueued = isQueued, + ) } } 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", diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6b508599..d0b85039 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -741,6 +741,7 @@ Show album cover Show visualizer Show backdrop + Play as?