From 73b626facdcec60df193de200eb628a6db9efd93 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Tue, 3 Mar 2026 22:27:39 -0500 Subject: [PATCH] Add long click queue operations --- .../wholphin/data/model/AudioItem.kt | 5 ++ .../wholphin/services/MusicService.kt | 29 ++++--- .../ui/detail/music/AlbumDetailsPage.kt | 3 + .../ui/detail/music/ArtistDetailsPage.kt | 3 + .../ui/detail/music/NowPlayingOverlay.kt | 8 +- .../ui/detail/music/NowPlayingPage.kt | 41 +++++++++ .../ui/detail/music/NowPlayingViewModel.kt | 10 ++- .../ui/detail/music/SongListDisplay.kt | 13 +-- .../wholphin/ui/detail/music/Utils.kt | 85 +++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 10 files changed, 176 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt index fbcc8e6d..4a1798e7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt @@ -9,6 +9,7 @@ import kotlin.time.Duration data class AudioItem( val id: UUID, val albumId: UUID?, + val artistId: UUID?, val title: String?, val albumTitle: String?, val artistNames: String?, @@ -24,6 +25,10 @@ data class AudioItem( AudioItem( id = item.id, albumId = item.data.albumId, + artistId = + item.data.artistItems + ?.firstOrNull() + ?.id, title = item.title, albumTitle = item.data.album, artistNames = item.data.albumArtist, 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 64efe810..d21b2b17 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 @@ -244,12 +244,30 @@ class MusicService updateQueueSize() } + suspend fun moveQueue( + index: Int, + newIndex: Int, + ) = withContext(Dispatchers.Main) { + player.moveMediaItem(index, newIndex) + updateQueueSize() + } + + suspend fun playIndex(index: Int) { + onMain { player.seekTo(index, 0L) } + // MusicPlayerListener will update state + } + suspend fun playNext(song: BaseItem) { val mediaItem = convert(song) onMain { player.addMediaItem(state.value.currentIndex + 1, mediaItem) } updateQueueSize() } + suspend fun removeFromQueue(index: Int) { + onMain { player.removeMediaItem(index) } + updateQueueSize() + } + private suspend fun loading(block: suspend () -> T): T { _state.update { it.copy(loadingState = LoadingState.Loading) } val result = block.invoke() @@ -336,17 +354,6 @@ private class MusicPlayerListener( } } -data class PlayerMediaItemList( - private val player: Player, -) { - 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 - } -} - @Composable fun rememberQueue( player: Player, 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 98b0908f..68d7151b 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 @@ -282,6 +282,7 @@ fun AlbumDetailsPage( playlistViewModel.loadPlaylists(MediaType.AUDIO) showPlaylistDialog.makePresent(itemId) }, + onClickRemoveFromQueue = {}, ) } @@ -362,6 +363,7 @@ fun AlbumDetailsPage( actions = moreDialogActions, item = album, index = 0, + canRemove = false, ), ) }, @@ -404,6 +406,7 @@ fun AlbumDetailsPage( actions = moreDialogActions, item = song, index = index, + canRemove = false, ), ) } 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 8b69dfb8..b5cd0522 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 @@ -300,6 +300,7 @@ fun ArtistDetailsPage( playlistViewModel.loadPlaylists(MediaType.AUDIO) showPlaylistDialog.makePresent(itemId) }, + onClickRemoveFromQueue = {}, ) } @@ -369,6 +370,7 @@ fun ArtistDetailsPage( actions = moreDialogActions, item = artist, index = 0, + canRemove = false, ), ) }, @@ -408,6 +410,7 @@ fun ArtistDetailsPage( actions = moreDialogActions, item = song, index = index, + canRemove = false, ), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingOverlay.kt index 2f1acea2..2231fecf 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingOverlay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingOverlay.kt @@ -59,6 +59,8 @@ fun NowPlayingOverlay( current: AudioItem?, queue: List, controllerViewState: ControllerViewState, + onClickSong: (Int, AudioItem) -> Unit, + onLongClickSong: (Int, AudioItem) -> Unit, onClickMore: () -> Unit, onMoveQueue: (Int, MoveDirection) -> Unit, modifier: Modifier = Modifier, @@ -159,10 +161,8 @@ fun NowPlayingOverlay( runtime = song.runtime?.roundSeconds, showArtist = true, isPlaying = current?.id == song.id, - onClick = { - player.seekTo(index, 0L) - }, - onLongClick = {}, + onClick = { onClickSong.invoke(index, song) }, + onLongClick = { onLongClickSong.invoke(index, song) }, modifier = Modifier .weight(1f) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt index d940bf69..212d50b7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -42,6 +43,8 @@ import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.rememberQueue import com.github.damontecres.wholphin.ui.AppColors +import com.github.damontecres.wholphin.ui.components.DialogParams +import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.playback.BottomDialog import com.github.damontecres.wholphin.ui.playback.BottomDialogItem @@ -59,6 +62,7 @@ fun NowPlayingPage( creationCallback = { it.create() }, ), ) { + val context = LocalContext.current val state by viewModel.state.collectAsState() val player = viewModel.player val queue = rememberQueue(player, state.musicServiceState.queueSize) @@ -97,9 +101,20 @@ fun NowPlayingPage( ) } + val actions = + remember { + MusicQueueDialogActions( + onNavigate = { viewModel.navigationManager.navigateTo(it) }, + onClickPlay = { index, _ -> viewModel.play(index) }, + onClickPlayNext = { index, _ -> viewModel.playNext(index) }, + onClickRemoveFromQueue = { index, _ -> viewModel.removeFromQueue(index) }, + ) + } + var showMoreDialog by remember { mutableStateOf(false) } var lyricsActive by remember { mutableStateOf(true) } var showDebugInfo by remember { mutableStateOf(true) } + var itemMoreDialog by remember { mutableStateOf(null) } val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } @@ -189,6 +204,22 @@ fun NowPlayingPage( controllerViewState = controllerViewState, onMoveQueue = { index, direction -> viewModel.moveQueue(index, direction) }, onClickMore = { showMoreDialog = true }, + onClickSong = { index, _ -> viewModel.play(index) }, + onLongClickSong = { index, song -> + itemMoreDialog = + DialogParams( + title = song.title ?: "", + fromLongClick = true, + items = + buildMoreDialogForMusicQueue( + context = context, + actions = actions, + item = song, + index = index, + canRemove = true, + ), + ) + }, modifier = Modifier .background(AppColors.TransparentBlack50) @@ -199,6 +230,16 @@ fun NowPlayingPage( LoadingPage(focusEnabled = false) } } + itemMoreDialog?.let { params -> + DialogPopup( + showDialog = true, + title = params.title, + dialogItems = params.items, + onDismissRequest = { itemMoreDialog = null }, + dismissOnClick = true, + waitToLoad = params.fromLongClick, + ) + } if (showMoreDialog) { NowPlayingBottomDialog( showDebugInfo = showDebugInfo, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt index 0bc283de..4a1d5029 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt @@ -44,7 +44,7 @@ class NowPlayingViewModel constructor( private val api: ApiClient, @param:ApplicationContext private val context: Context, - private val navigationManager: NavigationManager, + val navigationManager: NavigationManager, private val favoriteWatchManager: FavoriteWatchManager, private val backdropService: BackdropService, private val imageUrlService: ImageUrlService, @@ -127,7 +127,7 @@ class NowPlayingViewModel } else { null } - Timber.v("lyricIndex=$lyricIndex") +// Timber.v("lyricIndex=$lyricIndex") state.update { it.copy( lyrics = lyrics, @@ -178,6 +178,12 @@ class NowPlayingViewModel direction: MoveDirection, ) = viewModelScope.launchDefault { musicService.moveQueue(index, direction) } + fun play(index: Int) = viewModelScope.launchDefault { musicService.playIndex(index) } + + fun playNext(index: Int) = viewModelScope.launchDefault { musicService.moveQueue(index, 1) } + + fun removeFromQueue(index: Int) = viewModelScope.launchDefault { musicService.removeFromQueue(index) } + fun stop() { player.stop() navigationManager.goBack() 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 9b92d257..ea32c088 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 @@ -25,6 +25,7 @@ import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.enableMarquee 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.theme.WholphinTheme import org.jellyfin.sdk.model.extensions.ticks import kotlin.time.Duration @@ -68,11 +69,13 @@ fun SongListItem( artist = if (showArtist) song?.data?.albumArtist else null, indexNumber = song?.data?.indexNumber, runtime = - song - ?.data - ?.runTimeTicks - ?.ticks - ?.roundMinutes, + remember(song) { + song + ?.data + ?.runTimeTicks + ?.ticks + ?.roundSeconds + }, onClick = onClick, onLongClick = onLongClick, modifier = modifier, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/Utils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/Utils.kt index 1f8377b7..d38da948 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/Utils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/Utils.kt @@ -4,11 +4,13 @@ import android.content.Context import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowForward +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.data.model.AudioItem import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.DefaultItemFields import com.github.damontecres.wholphin.ui.components.DialogItem @@ -34,6 +36,7 @@ data class MusicMoreDialogActions( val onClickGoToArtist: (UUID) -> Unit = { onNavigate.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ARTIST)) }, + val onClickRemoveFromQueue: (Int) -> Unit, ) fun buildMoreDialogForMusic( @@ -41,6 +44,7 @@ fun buildMoreDialogForMusic( actions: MusicMoreDialogActions, item: BaseItem, index: Int, + canRemove: Boolean, ): List = buildList { add( @@ -61,6 +65,16 @@ fun buildMoreDialogForMusic( actions.onClickPlayNext(index, item) }, ) + if (canRemove) { + add( + DialogItem( + context.getString(R.string.remove_from_queue), + Icons.Default.Delete, + ) { + actions.onClickRemoveFromQueue(index) + }, + ) + } add( DialogItem( context.getString(R.string.add_to_queue), @@ -112,6 +126,77 @@ fun buildMoreDialogForMusic( } } +data class MusicQueueDialogActions( + val onNavigate: (Destination) -> Unit, + val onClickPlay: (Int, AudioItem) -> Unit, + val onClickPlayNext: (Int, AudioItem) -> Unit, + val onClickGoToAlbum: (UUID) -> Unit = { + onNavigate.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ALBUM)) + }, + val onClickGoToArtist: (UUID) -> Unit = { + onNavigate.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ARTIST)) + }, + val onClickRemoveFromQueue: (Int, AudioItem) -> Unit, +) + +fun buildMoreDialogForMusicQueue( + context: Context, + actions: MusicQueueDialogActions, + item: AudioItem, + index: Int, + canRemove: Boolean, +): List = + buildList { + add( + DialogItem( + context.getString(R.string.play), + Icons.Default.PlayArrow, + iconColor = Color.Green.copy(alpha = .8f), + ) { + actions.onClickPlay(index, item) + }, + ) + add( + DialogItem( + context.getString(R.string.play_next), + Icons.Default.PlayArrow, + iconColor = Color.Green.copy(alpha = .8f), + ) { + actions.onClickPlayNext(index, item) + }, + ) + if (canRemove) { + add( + DialogItem( + context.getString(R.string.remove_from_queue), + Icons.Default.Delete, + ) { + actions.onClickRemoveFromQueue(index, item) + }, + ) + } + if (item.albumId != null) { + add( + DialogItem( + context.getString(R.string.go_to_album), + Icons.Default.ArrowForward, + ) { + actions.onClickGoToAlbum.invoke(item.albumId) + }, + ) + } + if (item.artistId != null) { + add( + DialogItem( + context.getString(R.string.go_to_artist), + Icons.Default.ArrowForward, + ) { + actions.onClickGoToArtist.invoke(item.artistId) + }, + ) + } + } + suspend fun ViewModel.getPagerForAlbum( api: ApiClient, albumId: UUID, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9e7ec68f..7e4e8451 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -737,6 +737,7 @@ Hide lyrics Show lyrics Song has lyrics + Remove from queue