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
This commit is contained in:
Damontecres 2026-04-25 10:35:12 -04:00 committed by GitHub
parent 50174db22e
commit 3f0613f5f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 233 additions and 89 deletions

View file

@ -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<ItemSortBy>,
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<ItemFilterBy<*>> = 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<ItemSortBy>,
playEnabled: Boolean,
defaultViewOptions: ViewOptions,
@ -630,6 +667,61 @@ fun CollectionFolderGrid(
var showDeleteDialog by remember { mutableStateOf<PositionItem?>(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,
)

View file

@ -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<CollectionFolderMusicViewModel, CollectionFolderMusicViewModel.Factory>(
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 =

View file

@ -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,14 +52,22 @@ fun CollectionFolderPhotoAlbum(
var showHeader by remember { mutableStateOf(true) }
CollectionFolderGrid(
preferences = preferences,
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,
filter =
CollectionFolderFilter(
filter = viewModel.filter.value ?: GetItemsFilter(),
),
sortAndDirection =
viewModel.sortAndDirection.value
?: SortAndDirection.DEFAULT,
recursive = recursive,
startSlideshow = false,
)
@ -67,6 +76,8 @@ fun CollectionFolderPhotoAlbum(
}
viewModel.navigateTo(destination)
},
)
},
itemId = itemId.toServerString(),
initialFilter = filter,
showTitle = showHeader,

View file

@ -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(