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(), itemId.toServerString(),
initialFilter, initialFilter,
recursive, 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, sortOptions,
playEnabled, playEnabled,
viewModelKey = viewModelKey, viewModelKey = viewModelKey,
@ -590,7 +627,7 @@ fun CollectionFolderGrid(
itemId: String, itemId: String,
initialFilter: CollectionFolderFilter, initialFilter: CollectionFolderFilter,
recursive: Boolean, recursive: Boolean,
onClickItem: (Int, BaseItem) -> Unit, actions: GridClickActions,
sortOptions: List<ItemSortBy>, sortOptions: List<ItemSortBy>,
playEnabled: Boolean, playEnabled: Boolean,
defaultViewOptions: ViewOptions, defaultViewOptions: ViewOptions,
@ -630,6 +667,61 @@ fun CollectionFolderGrid(
var showDeleteDialog by remember { mutableStateOf<PositionItem?>(null) } var showDeleteDialog by remember { mutableStateOf<PositionItem?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) 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) { when (val state = loading) {
DataLoadingState.Loading, DataLoadingState.Loading,
DataLoadingState.Pending, DataLoadingState.Pending,
@ -662,10 +754,8 @@ fun CollectionFolderGrid(
sortAndDirection = sortAndDirection!!, sortAndDirection = sortAndDirection!!,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
focusRequesterOnEmpty = focusRequesterOnEmpty, focusRequesterOnEmpty = focusRequesterOnEmpty,
onClickItem = onClickItem, onClickItem = gridActions.onClickItem,
onLongClickItem = { position, item -> onLongClickItem = gridActions.onLongClickItem!!,
moreDialog.makePresent(PositionItem(position, item))
},
onSortChange = { onSortChange = {
viewModel.onSortChange(it, recursive, filter) viewModel.onSortChange(it, recursive, filter)
}, },
@ -689,47 +779,8 @@ fun CollectionFolderGrid(
onSaveViewOptions = { viewModel.saveViewOptions(it) }, onSaveViewOptions = { viewModel.saveViewOptions(it) },
onChangeBackdrop = viewModel::updateBackdrop, onChangeBackdrop = viewModel::updateBackdrop,
playEnabled = playEnabled, playEnabled = playEnabled,
onClickPlay = { index, item -> onClickPlay = gridActions.onClickPlayRemoteButton!!,
val destination = onClickPlayAll = gridActions.onClickPlayAll!!,
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)
}
},
) )
AnimatedVisibility( AnimatedVisibility(
@ -782,7 +833,7 @@ fun CollectionFolderGrid(
showDeleteDialog = PositionItem(position, item) showDeleteDialog = PositionItem(position, item)
}, },
onClickGoTo = { onClickGoTo = {
onClickItem.invoke(position, it) gridActions.onClickItem.invoke(position, it)
}, },
onClickAddToQueue = { onClickAddToQueue = {
viewModel.addToQueue(it, 0) viewModel.addToQueue(it, 0)
@ -1165,3 +1216,10 @@ fun GridTitle(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = modifier.fillMaxWidth(), 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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R 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.BaseItem
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences 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.MusicService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.GenreCardGrid 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.RecommendedMusic
import com.github.damontecres.wholphin.ui.components.TabRow import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare 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.launchDefault
import com.github.damontecres.wholphin.ui.logTab import com.github.damontecres.wholphin.ui.logTab
import com.github.damontecres.wholphin.ui.nav.Destination 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 dagger.hilt.android.lifecycle.HiltViewModel
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.api.BaseItemKind 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 class CollectionFolderMusicViewModel
@Inject @AssistedInject
constructor( constructor(
private val api: ApiClient,
private val serverRepository: ServerRepository,
private val musicService: MusicService, 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) { fun play(item: BaseItem) {
if (item.type == BaseItemKind.AUDIO) { if (item.type == BaseItemKind.AUDIO) {
viewModelScope.launchDefault { 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 @Composable
@ -65,11 +126,13 @@ fun CollectionFolderMusic(
preferences: UserPreferences, preferences: UserPreferences,
destination: Destination.MediaItem, destination: Destination.MediaItem,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: CollectionFolderMusicViewModel = hiltViewModel(), viewModel: CollectionFolderMusicViewModel =
preferencesViewModel: PreferencesViewModel = hiltViewModel(), hiltViewModel<CollectionFolderMusicViewModel, CollectionFolderMusicViewModel.Factory>(
creationCallback = { it.create(destination.itemId) },
),
) { ) {
val rememberedTabIndex = val rememberedTabIndex =
remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) } remember { viewModel.getRememberedTab(preferences, destination.itemId, 0) }
val tabs = val tabs =
listOf( listOf(
@ -88,12 +151,22 @@ fun CollectionFolderMusic(
LaunchedEffect(selectedTabIndex) { LaunchedEffect(selectedTabIndex) {
logTab("music", selectedTabIndex) logTab("music", selectedTabIndex)
preferencesViewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex) viewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex)
preferencesViewModel.backdropService.clearBackdrop() viewModel.backdropService.clearBackdrop()
} }
var showHeader by rememberSaveable { mutableStateOf(true) } var showHeader by rememberSaveable { mutableStateOf(true) }
val actions =
remember {
GridClickActions(
onClickItem = viewModel::onClick,
onLongClickItem = null,
onClickPlayAll = viewModel::onClickPlayAll,
onClickPlayRemoteButton = viewModel::onClickPlayRemoteButton,
)
}
Column( Column(
modifier = modifier, modifier = modifier,
) { ) {
@ -133,9 +206,7 @@ fun CollectionFolderMusic(
1 -> { 1 -> {
CollectionFolderGrid( CollectionFolderGrid(
preferences = preferences, preferences = preferences,
onClickItem = { _, item -> actions = actions,
preferencesViewModel.navigationManager.navigateTo(item.destination())
},
itemId = destination.itemId, itemId = destination.itemId,
viewModelKey = "${destination.itemId}_albums", viewModelKey = "${destination.itemId}_albums",
initialFilter = initialFilter =
@ -165,9 +236,7 @@ fun CollectionFolderMusic(
2 -> { 2 -> {
CollectionFolderGrid( CollectionFolderGrid(
preferences = preferences, preferences = preferences,
onClickItem = { _, item -> actions = actions,
preferencesViewModel.navigationManager.navigateTo(item.destination())
},
itemId = destination.itemId, itemId = destination.itemId,
viewModelKey = "${destination.itemId}_artists", viewModelKey = "${destination.itemId}_artists",
initialFilter = initialFilter =
@ -209,9 +278,7 @@ fun CollectionFolderMusic(
4 -> { 4 -> {
CollectionFolderGrid( CollectionFolderGrid(
preferences = preferences, preferences = preferences,
onClickItem = { _, item -> actions = actions,
viewModel.play(item)
},
itemId = destination.itemId, itemId = destination.itemId,
viewModelKey = "${destination.itemId}_songs", viewModelKey = "${destination.itemId}_songs",
initialFilter = 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.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
import com.github.damontecres.wholphin.ui.components.CollectionFolderViewModel 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.components.ViewOptionsWide
import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.data.VideoSortOptions import com.github.damontecres.wholphin.ui.data.VideoSortOptions
@ -51,14 +52,22 @@ fun CollectionFolderPhotoAlbum(
var showHeader by remember { mutableStateOf(true) } var showHeader by remember { mutableStateOf(true) }
CollectionFolderGrid( CollectionFolderGrid(
preferences = preferences, preferences = preferences,
actions =
remember {
GridClickActions(
onClickItem = { index, item -> onClickItem = { index, item ->
val destination = val destination =
if (item.type == BaseItemKind.PHOTO) { if (item.type == BaseItemKind.PHOTO) {
Destination.Slideshow( Destination.Slideshow(
parentId = itemId, parentId = itemId,
index = index, index = index,
filter = CollectionFolderFilter(filter = viewModel.filter.value ?: GetItemsFilter()), filter =
sortAndDirection = viewModel.sortAndDirection.value ?: SortAndDirection.DEFAULT, CollectionFolderFilter(
filter = viewModel.filter.value ?: GetItemsFilter(),
),
sortAndDirection =
viewModel.sortAndDirection.value
?: SortAndDirection.DEFAULT,
recursive = recursive, recursive = recursive,
startSlideshow = false, startSlideshow = false,
) )
@ -67,6 +76,8 @@ fun CollectionFolderPhotoAlbum(
} }
viewModel.navigateTo(destination) viewModel.navigateTo(destination)
}, },
)
},
itemId = itemId.toServerString(), itemId = itemId.toServerString(),
initialFilter = filter, initialFilter = filter,
showTitle = showHeader, 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.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
import com.github.damontecres.wholphin.ui.components.ErrorMessage 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.TabRow
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare
@ -109,13 +110,20 @@ fun FavoritesPage(
focusRequesters = tabFocusRequesters, focusRequesters = tabFocusRequesters,
) )
} }
val actions =
remember {
GridClickActions(
onClickItem = { _, item -> onClickItem.invoke(item) },
)
}
// TODO playEnabled = true for movies & episodes // TODO playEnabled = true for movies & episodes
when (selectedTabIndex) { when (selectedTabIndex) {
// Movies // Movies
0 -> { 0 -> {
CollectionFolderGrid( CollectionFolderGrid(
preferences = preferences, preferences = preferences,
onClickItem = { _, item -> onClickItem.invoke(item) }, actions = actions,
itemId = "${NavDrawerItem.Favorites.id}_movies", itemId = "${NavDrawerItem.Favorites.id}_movies",
initialFilter = initialFilter =
CollectionFolderFilter( CollectionFolderFilter(
@ -147,7 +155,7 @@ fun FavoritesPage(
1 -> { 1 -> {
CollectionFolderGrid( CollectionFolderGrid(
preferences = preferences, preferences = preferences,
onClickItem = { _, item -> onClickItem.invoke(item) }, actions = actions,
itemId = "${NavDrawerItem.Favorites.id}_series", itemId = "${NavDrawerItem.Favorites.id}_series",
initialFilter = initialFilter =
CollectionFolderFilter( CollectionFolderFilter(
@ -179,7 +187,7 @@ fun FavoritesPage(
2 -> { 2 -> {
CollectionFolderGrid( CollectionFolderGrid(
preferences = preferences, preferences = preferences,
onClickItem = { _, item -> onClickItem.invoke(item) }, actions = actions,
itemId = "${NavDrawerItem.Favorites.id}_episodes", itemId = "${NavDrawerItem.Favorites.id}_episodes",
initialFilter = initialFilter =
CollectionFolderFilter( CollectionFolderFilter(
@ -212,7 +220,7 @@ fun FavoritesPage(
3 -> { 3 -> {
CollectionFolderGrid( CollectionFolderGrid(
preferences = preferences, preferences = preferences,
onClickItem = { _, item -> onClickItem.invoke(item) }, actions = actions,
itemId = "${NavDrawerItem.Favorites.id}_videos", itemId = "${NavDrawerItem.Favorites.id}_videos",
initialFilter = initialFilter =
CollectionFolderFilter( CollectionFolderFilter(
@ -244,7 +252,7 @@ fun FavoritesPage(
4 -> { 4 -> {
CollectionFolderGrid( CollectionFolderGrid(
preferences = preferences, preferences = preferences,
onClickItem = { _, item -> onClickItem.invoke(item) }, actions = actions,
itemId = "${NavDrawerItem.Favorites.id}_playlists", itemId = "${NavDrawerItem.Favorites.id}_playlists",
initialFilter = initialFilter =
CollectionFolderFilter( CollectionFolderFilter(
@ -276,7 +284,7 @@ fun FavoritesPage(
5 -> { 5 -> {
CollectionFolderGrid( CollectionFolderGrid(
preferences = preferences, preferences = preferences,
onClickItem = { _, item -> onClickItem.invoke(item) }, actions = actions,
itemId = "${NavDrawerItem.Favorites.id}_people", itemId = "${NavDrawerItem.Favorites.id}_people",
initialFilter = initialFilter =
CollectionFolderFilter( CollectionFolderFilter(