From 3f0613f5f4dfa9c6a5b7a96bec20ec0225cbbb8c Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:35:12 -0400 Subject: [PATCH] Fix play/shuffle buttons on music library tabs (#1307) ## Description Fixes the play & shuffle buttons on music library tabs. They previously attempted to play the audio via the video playback method. This PR adds the ability to override the buttons and does so for music libraries. ### Related issues Fixes #1304 ### Testing Emulator, compared results against the web UI ## Screenshots N/A ## AI or LLM usage None --- .../ui/components/CollectionFolderGrid.kt | 154 ++++++++++++------ .../ui/detail/CollectionFolderMusic.kt | 105 +++++++++--- .../ui/detail/CollectionFolderPhotoAlbum.kt | 43 +++-- .../wholphin/ui/detail/FavoritesPage.kt | 20 ++- 4 files changed, 233 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index f574e883..848288d5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -570,7 +570,44 @@ fun CollectionFolderGrid( itemId.toServerString(), initialFilter, recursive, - onClickItem, + GridClickActions(onClickItem = onClickItem), + sortOptions, + playEnabled, + viewModelKey = viewModelKey, + defaultViewOptions = defaultViewOptions, + modifier = modifier, + initialSortAndDirection = initialSortAndDirection, + showTitle = showTitle, + positionCallback = positionCallback, + useSeriesForPrimary = useSeriesForPrimary, + filterOptions = filterOptions, + focusRequesterOnEmpty = focusRequesterOnEmpty, +) + +@Composable +fun CollectionFolderGrid( + preferences: UserPreferences, + itemId: UUID, + initialFilter: CollectionFolderFilter, + recursive: Boolean, + actions: GridClickActions, + sortOptions: List, + playEnabled: Boolean, + defaultViewOptions: ViewOptions, + modifier: Modifier = Modifier, + viewModelKey: String? = itemId.toServerString(), + initialSortAndDirection: SortAndDirection? = null, + showTitle: Boolean = true, + positionCallback: ((columns: Int, position: Int) -> Unit)? = null, + useSeriesForPrimary: Boolean = true, + filterOptions: List> = DefaultFilterOptions, + focusRequesterOnEmpty: FocusRequester? = null, +) = CollectionFolderGrid( + preferences, + itemId.toServerString(), + initialFilter, + recursive, + actions, sortOptions, playEnabled, viewModelKey = viewModelKey, @@ -590,7 +627,7 @@ fun CollectionFolderGrid( itemId: String, initialFilter: CollectionFolderFilter, recursive: Boolean, - onClickItem: (Int, BaseItem) -> Unit, + actions: GridClickActions, sortOptions: List, playEnabled: Boolean, defaultViewOptions: ViewOptions, @@ -630,6 +667,61 @@ fun CollectionFolderGrid( var showDeleteDialog by remember { mutableStateOf(null) } val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) + val gridActions = + remember(actions) { + GridClickActions( + onClickItem = actions.onClickItem, + onLongClickItem = + actions.onLongClickItem ?: { position, item -> + moreDialog.makePresent(PositionItem(position, item)) + }, + onClickPlayAll = + actions.onClickPlayAll ?: { shuffle -> + itemId.toUUIDOrNull()?.let { + val destination = + if (item?.type == BaseItemKind.PHOTO_ALBUM) { + Destination.Slideshow( + parentId = it, + index = 0, + filter = CollectionFolderFilter(filter = filter), + sortAndDirection = sortAndDirection, + recursive = true, + startSlideshow = true, + ) + } else { + Destination.PlaybackList( + itemId = it, + startIndex = 0, + shuffle = shuffle, + recursive = recursive, + sortAndDirection = sortAndDirection, + filter = filter, + ) + } + viewModel.navigateTo(destination) + } + Unit + }, + onClickPlayRemoteButton = + actions.onClickPlayRemoteButton ?: { index, item -> + val destination = + if (item.type == BaseItemKind.PHOTO_ALBUM) { + Destination.Slideshow( + parentId = item.id, + index = index, + filter = CollectionFolderFilter(filter = filter), + sortAndDirection = sortAndDirection, + recursive = true, + startSlideshow = true, + ) + } else { + Destination.Playback(item) + } + viewModel.navigateTo(destination) + }, + ) + } + when (val state = loading) { DataLoadingState.Loading, DataLoadingState.Pending, @@ -662,10 +754,8 @@ fun CollectionFolderGrid( sortAndDirection = sortAndDirection!!, modifier = Modifier.fillMaxSize(), focusRequesterOnEmpty = focusRequesterOnEmpty, - onClickItem = onClickItem, - onLongClickItem = { position, item -> - moreDialog.makePresent(PositionItem(position, item)) - }, + onClickItem = gridActions.onClickItem, + onLongClickItem = gridActions.onLongClickItem!!, onSortChange = { viewModel.onSortChange(it, recursive, filter) }, @@ -689,47 +779,8 @@ fun CollectionFolderGrid( onSaveViewOptions = { viewModel.saveViewOptions(it) }, onChangeBackdrop = viewModel::updateBackdrop, playEnabled = playEnabled, - onClickPlay = { index, item -> - val destination = - if (item.type == BaseItemKind.PHOTO_ALBUM) { - Destination.Slideshow( - parentId = item.id, - index = index, - filter = CollectionFolderFilter(filter = filter), - sortAndDirection = sortAndDirection, - recursive = true, - startSlideshow = true, - ) - } else { - Destination.Playback(item) - } - viewModel.navigateTo(destination) - }, - onClickPlayAll = { shuffle -> - itemId.toUUIDOrNull()?.let { - val destination = - if (item?.type == BaseItemKind.PHOTO_ALBUM) { - Destination.Slideshow( - parentId = it, - index = 0, - filter = CollectionFolderFilter(filter = filter), - sortAndDirection = sortAndDirection, - recursive = true, - startSlideshow = true, - ) - } else { - Destination.PlaybackList( - itemId = it, - startIndex = 0, - shuffle = shuffle, - recursive = recursive, - sortAndDirection = sortAndDirection, - filter = filter, - ) - } - viewModel.navigateTo(destination) - } - }, + onClickPlay = gridActions.onClickPlayRemoteButton!!, + onClickPlayAll = gridActions.onClickPlayAll!!, ) AnimatedVisibility( @@ -782,7 +833,7 @@ fun CollectionFolderGrid( showDeleteDialog = PositionItem(position, item) }, onClickGoTo = { - onClickItem.invoke(position, it) + gridActions.onClickItem.invoke(position, it) }, onClickAddToQueue = { viewModel.addToQueue(it, 0) @@ -1165,3 +1216,10 @@ fun GridTitle( overflow = TextOverflow.Ellipsis, modifier = modifier.fillMaxWidth(), ) + +data class GridClickActions( + val onClickItem: (Int, BaseItem) -> Unit, + val onLongClickItem: ((Int, BaseItem) -> Unit)? = null, + val onClickPlayAll: ((shuffle: Boolean) -> Unit)? = null, + val onClickPlayRemoteButton: ((Int, BaseItem) -> Unit)? = null, +) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMusic.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMusic.kt index 30534ffc..1911fc7a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMusic.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMusic.kt @@ -23,14 +23,18 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.CollectionFolderFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.MusicService +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.GenreCardGrid +import com.github.damontecres.wholphin.ui.components.GridClickActions import com.github.damontecres.wholphin.ui.components.RecommendedMusic import com.github.damontecres.wholphin.ui.components.TabRow import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare @@ -40,17 +44,36 @@ import com.github.damontecres.wholphin.ui.data.SongSortOptions import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.logTab import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel +import com.github.damontecres.wholphin.util.ApiRequestPager +import com.github.damontecres.wholphin.util.GetItemsRequestHandler +import com.github.damontecres.wholphin.util.RememberTabManager +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel +import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.model.api.BaseItemKind -import javax.inject.Inject +import org.jellyfin.sdk.model.api.request.GetItemsRequest +import java.util.UUID -@HiltViewModel +@HiltViewModel(assistedFactory = CollectionFolderMusicViewModel.Factory::class) class CollectionFolderMusicViewModel - @Inject + @AssistedInject constructor( + private val api: ApiClient, + private val serverRepository: ServerRepository, private val musicService: MusicService, - ) : ViewModel() { + private val navigationManager: NavigationManager, + val backdropService: BackdropService, + private val rememberTabManager: RememberTabManager, + @Assisted private val itemId: UUID, + ) : ViewModel(), + RememberTabManager by rememberTabManager { + @AssistedFactory + interface Factory { + fun create(itemId: UUID): CollectionFolderMusicViewModel + } + fun play(item: BaseItem) { if (item.type == BaseItemKind.AUDIO) { viewModelScope.launchDefault { @@ -58,6 +81,44 @@ class CollectionFolderMusicViewModel } } } + + fun onClick( + index: Int, + item: BaseItem, + ) { + if (item.type == BaseItemKind.AUDIO) { + viewModelScope.launchDefault { + musicService.setQueue(listOf(item), false) + } + } else { + navigationManager.navigateTo(item.destination()) + } + } + + fun onClickPlayAll(shuffle: Boolean) { + viewModelScope.launchDefault { + val request = + GetItemsRequest( + userId = serverRepository.currentUser.value?.id, + parentId = itemId, + includeItemTypes = listOf(BaseItemKind.AUDIO), + recursive = true, + ) + val pager = ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope).init() + musicService.setQueue(pager, 0, shuffle) + } + } + + fun onClickPlayRemoteButton( + index: Int, + item: BaseItem, + ) { + if (item.type == BaseItemKind.AUDIO) { + viewModelScope.launchDefault { + musicService.setQueue(listOf(item), false) + } + } + } } @Composable @@ -65,11 +126,13 @@ fun CollectionFolderMusic( preferences: UserPreferences, destination: Destination.MediaItem, modifier: Modifier = Modifier, - viewModel: CollectionFolderMusicViewModel = hiltViewModel(), - preferencesViewModel: PreferencesViewModel = hiltViewModel(), + viewModel: CollectionFolderMusicViewModel = + hiltViewModel( + creationCallback = { it.create(destination.itemId) }, + ), ) { val rememberedTabIndex = - remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) } + remember { viewModel.getRememberedTab(preferences, destination.itemId, 0) } val tabs = listOf( @@ -88,12 +151,22 @@ fun CollectionFolderMusic( LaunchedEffect(selectedTabIndex) { logTab("music", selectedTabIndex) - preferencesViewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex) - preferencesViewModel.backdropService.clearBackdrop() + viewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex) + viewModel.backdropService.clearBackdrop() } var showHeader by rememberSaveable { mutableStateOf(true) } + val actions = + remember { + GridClickActions( + onClickItem = viewModel::onClick, + onLongClickItem = null, + onClickPlayAll = viewModel::onClickPlayAll, + onClickPlayRemoteButton = viewModel::onClickPlayRemoteButton, + ) + } + Column( modifier = modifier, ) { @@ -133,9 +206,7 @@ fun CollectionFolderMusic( 1 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = { _, item -> - preferencesViewModel.navigationManager.navigateTo(item.destination()) - }, + actions = actions, itemId = destination.itemId, viewModelKey = "${destination.itemId}_albums", initialFilter = @@ -165,9 +236,7 @@ fun CollectionFolderMusic( 2 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = { _, item -> - preferencesViewModel.navigationManager.navigateTo(item.destination()) - }, + actions = actions, itemId = destination.itemId, viewModelKey = "${destination.itemId}_artists", initialFilter = @@ -209,9 +278,7 @@ fun CollectionFolderMusic( 4 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = { _, item -> - viewModel.play(item) - }, + actions = actions, itemId = destination.itemId, viewModelKey = "${destination.itemId}_songs", initialFilter = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPhotoAlbum.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPhotoAlbum.kt index 562af214..62186f55 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPhotoAlbum.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPhotoAlbum.kt @@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid import com.github.damontecres.wholphin.ui.components.CollectionFolderViewModel +import com.github.damontecres.wholphin.ui.components.GridClickActions import com.github.damontecres.wholphin.ui.components.ViewOptionsWide import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.data.VideoSortOptions @@ -51,22 +52,32 @@ fun CollectionFolderPhotoAlbum( var showHeader by remember { mutableStateOf(true) } CollectionFolderGrid( preferences = preferences, - onClickItem = { index, item -> - val destination = - if (item.type == BaseItemKind.PHOTO) { - Destination.Slideshow( - parentId = itemId, - index = index, - filter = CollectionFolderFilter(filter = viewModel.filter.value ?: GetItemsFilter()), - sortAndDirection = viewModel.sortAndDirection.value ?: SortAndDirection.DEFAULT, - recursive = recursive, - startSlideshow = false, - ) - } else { - item.destination(index) - } - viewModel.navigateTo(destination) - }, + actions = + remember { + GridClickActions( + onClickItem = { index, item -> + val destination = + if (item.type == BaseItemKind.PHOTO) { + Destination.Slideshow( + parentId = itemId, + index = index, + filter = + CollectionFolderFilter( + filter = viewModel.filter.value ?: GetItemsFilter(), + ), + sortAndDirection = + viewModel.sortAndDirection.value + ?: SortAndDirection.DEFAULT, + recursive = recursive, + startSlideshow = false, + ) + } else { + item.destination(index) + } + viewModel.navigateTo(destination) + }, + ) + }, itemId = itemId.toServerString(), initialFilter = filter, showTitle = showHeader, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt index 50b0053e..9b03aa0c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt @@ -29,6 +29,7 @@ import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid import com.github.damontecres.wholphin.ui.components.ErrorMessage +import com.github.damontecres.wholphin.ui.components.GridClickActions import com.github.damontecres.wholphin.ui.components.TabRow import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare @@ -109,13 +110,20 @@ fun FavoritesPage( focusRequesters = tabFocusRequesters, ) } + val actions = + remember { + GridClickActions( + onClickItem = { _, item -> onClickItem.invoke(item) }, + ) + } + // TODO playEnabled = true for movies & episodes when (selectedTabIndex) { // Movies 0 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = { _, item -> onClickItem.invoke(item) }, + actions = actions, itemId = "${NavDrawerItem.Favorites.id}_movies", initialFilter = CollectionFolderFilter( @@ -147,7 +155,7 @@ fun FavoritesPage( 1 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = { _, item -> onClickItem.invoke(item) }, + actions = actions, itemId = "${NavDrawerItem.Favorites.id}_series", initialFilter = CollectionFolderFilter( @@ -179,7 +187,7 @@ fun FavoritesPage( 2 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = { _, item -> onClickItem.invoke(item) }, + actions = actions, itemId = "${NavDrawerItem.Favorites.id}_episodes", initialFilter = CollectionFolderFilter( @@ -212,7 +220,7 @@ fun FavoritesPage( 3 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = { _, item -> onClickItem.invoke(item) }, + actions = actions, itemId = "${NavDrawerItem.Favorites.id}_videos", initialFilter = CollectionFolderFilter( @@ -244,7 +252,7 @@ fun FavoritesPage( 4 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = { _, item -> onClickItem.invoke(item) }, + actions = actions, itemId = "${NavDrawerItem.Favorites.id}_playlists", initialFilter = CollectionFolderFilter( @@ -276,7 +284,7 @@ fun FavoritesPage( 5 -> { CollectionFolderGrid( preferences = preferences, - onClickItem = { _, item -> onClickItem.invoke(item) }, + actions = actions, itemId = "${NavDrawerItem.Favorites.id}_people", initialFilter = CollectionFolderFilter(