Update playlist details to match library grid (#2113)

## Description
This PR updates the playlist details page to more closely match the look
of a library grid tab. This allows for a few things:
- Customize layout (grid, list, backdrop, etc)
- Show logos
- Random button
- The play all/filter/etc buttons no longer awkwardly show all the time

Basically, a playlist details will default to the list layout with "Show
details" enabled

The library list layout was actually based on the playlist details
layout originally, so this unifies the code. This means a bit less
maintenance and improvements to one page benefits the other.

### Dev note

This PR adds a `CollectionFolderViewScope` to pass down information
about the current collection and layout. This allows for easier
customizing of components. The PR also lifts the card & list item
composable to be parameters of `CollectionFolderViewContent` which,
again, allows for lots of customization.

### Related issues
Related to
https://github.com/damontecres/Wholphin/discussions/2078#discussioncomment-19046988

### Testing
Emulator

## Screenshots

### Before
<img width="1280" height="720" alt="playlist_ui_before Large"
src="https://github.com/user-attachments/assets/1cf8db82-4f53-4f20-a05e-ab937ffa3e3f"
/>

### After
<img width="1280" height="720" alt="playlist_ui_after Large"
src="https://github.com/user-attachments/assets/96ee6c1a-1a20-44dc-b979-0d314539a04a"
/>

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-10-25 20:56:18 -04:00 • committed by GitHub
parent 6cde8ce8a6
commit 52c26bc0fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 397 additions and 725 deletions

View file

@ -44,7 +44,6 @@ import com.github.damontecres.wholphin.ui.detail.CardGrid
import com.github.damontecres.wholphin.ui.detail.GridItemDetails
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.main.HomePageHeader
import com.github.damontecres.wholphin.ui.playback.scale
import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
@ -53,19 +52,18 @@ import kotlin.time.Duration
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CollectionFolderGrid(
fun CollectionFolderViewScope.CollectionFolderGrid(
preferences: UserPreferences,
collectionType: CollectionType?,
focusedItem: BaseItem?,
items: List<BaseItem?>,
sortAndDirection: SortAndDirection,
onClickItem: (Int, BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit,
letterPosition: suspend (Char) -> Int,
viewOptions: ViewOptions,
onClickPlay: (Int, BaseItem) -> Unit,
initialPosition: Int,
gridFocusRequester: FocusRequester,
cardContent: @Composable CollectionFolderViewScope.(GridItemDetails<BaseItem>) -> Unit,
modifier: Modifier = Modifier,
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
) {
@ -99,18 +97,8 @@ fun CollectionFolderGrid(
modifier = Modifier.fillMaxSize(),
initialPosition = initialPosition,
positionCallback = positionCallback,
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
GridCard(
item = item,
onClick = onClick,
onLongClick = onLongClick,
imageContentScale = viewOptions.contentScale.scale,
imageAspectRatio = viewOptions.aspectRatio.ratio,
imageType = viewOptions.imageType,
showTitle = viewOptions.showTitles,
fillWidth = widthPx,
modifier = mod,
)
cardContent = { details ->
cardContent.invoke(this@CollectionFolderGrid, details)
},
columns = viewOptions.columns,
spacing = viewOptions.spacing.dp,

View file

@ -28,6 +28,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@ -59,6 +60,7 @@ import com.github.damontecres.wholphin.ui.cards.FavoriteIndicator
import com.github.damontecres.wholphin.ui.cards.WatchedIcon
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.AlphabetButtons
import com.github.damontecres.wholphin.ui.detail.GridItemDetails
import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.formatDuration
import com.github.damontecres.wholphin.ui.ifElse
@ -75,7 +77,7 @@ import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.extensions.ticks
@Composable
fun CollectionFolderList(
fun CollectionFolderViewScope.CollectionFolderList(
preferences: UserPreferences,
collectionType: CollectionType?,
focusedItem: BaseItem?,
@ -84,10 +86,10 @@ fun CollectionFolderList(
onClickItem: (Int, BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit,
letterPosition: suspend (Char) -> Int,
viewOptions: ViewOptions,
onClickPlay: (Int, BaseItem) -> Unit,
initialPosition: Int,
gridFocusRequester: FocusRequester,
itemContent: @Composable CollectionFolderViewScope.(GridItemDetails<BaseItem>) -> Unit,
modifier: Modifier = Modifier,
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
) {
@ -153,10 +155,8 @@ fun CollectionFolderList(
).padding(8.dp),
)
}
CollectionFolderContent(
this@CollectionFolderList.CollectionFolderListContent(
items = items,
viewOptions = viewOptions,
collectionType = collectionType,
listState = listState,
position = position,
positionFocusRequester = positionFocusRequester,
@ -167,6 +167,7 @@ fun CollectionFolderList(
},
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
itemContent = itemContent,
modifier =
Modifier
.weight(1f)
@ -266,44 +267,43 @@ fun CollectionFolderListDetails(
}
@Composable
fun CollectionFolderContent(
fun CollectionFolderViewScope.CollectionFolderListContent(
items: List<BaseItem?>,
viewOptions: ViewOptions,
collectionType: CollectionType?,
listState: LazyListState,
position: Int,
positionFocusRequester: FocusRequester,
onClickItem: (Int, BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit,
onPositionChange: (Int) -> Unit,
itemContent: @Composable CollectionFolderViewScope.(GridItemDetails<BaseItem>) -> Unit,
modifier: Modifier = Modifier,
) {
val currentPosition by rememberUpdatedState(position)
LazyColumn(
state = listState,
verticalArrangement = Arrangement.spacedBy(viewOptions.spacing.dp),
modifier = modifier.focusRestorer(positionFocusRequester),
) {
itemsIndexed(items) { index, item ->
val onClick: () -> Unit =
remember(index, item) {
{ item?.let { onClickItem.invoke(index, item) } }
val details =
remember(item, index, onClickItem, onLongClickItem, onPositionChange) {
GridItemDetails(
item = item,
index = index,
onClick = { item?.let { onClickItem.invoke(index, item) } },
onLongClick = { item?.let { onLongClickItem.invoke(index, item) } },
widthPx = -1,
mod =
Modifier
.onFocusChanged {
if (it.isFocused) onPositionChange.invoke(index)
}.ifElse(
index == currentPosition,
Modifier.focusRequester(positionFocusRequester),
),
)
}
val onLongClick: () -> Unit =
remember(index, item) {
{ item?.let { onLongClickItem.invoke(index, item) } }
}
CollectionFolderListItem(
item = item,
dense = viewOptions.type == ViewOptionsType.DENSE_LIST,
collectionType = collectionType,
onClick = onClick,
onLongClick = onLongClick,
modifier =
Modifier
.onFocusChanged {
if (it.isFocused) onPositionChange.invoke(index)
}.ifElse(index == position, Modifier.focusRequester(positionFocusRequester)),
)
itemContent.invoke(this@CollectionFolderListContent, details)
}
}
}

View file

@ -54,12 +54,15 @@ import com.github.damontecres.wholphin.services.ThemeSongPlayer
import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.services.deleteItem
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.cards.GridCard
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.GridItemDetails
import com.github.damontecres.wholphin.ui.detail.music.addToQueue
import com.github.damontecres.wholphin.ui.equalsNotNull
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.playback.scale
import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.ui.toServerString
@ -843,6 +846,33 @@ fun CollectionFolderViewContent(
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
focusRequesterOnEmpty: FocusRequester? = null,
gridCardContent: @Composable CollectionFolderViewScope.(
GridItemDetails<BaseItem>,
) -> Unit = { (item, index, onClick, onLongClick, widthPx, mod) ->
GridCard(
item = item,
onClick = onClick,
onLongClick = onLongClick,
imageContentScale = viewOptions.contentScale.scale,
imageAspectRatio = viewOptions.aspectRatio.ratio,
imageType = viewOptions.imageType,
showTitle = viewOptions.showTitles,
fillWidth = widthPx,
modifier = mod,
)
},
listItemContent: @Composable CollectionFolderViewScope.(
GridItemDetails<BaseItem>,
) -> Unit = { (item, index, onClick, onLongClick, _, mod) ->
CollectionFolderListItem(
item = item,
dense = viewOptions.type == ViewOptionsType.DENSE_LIST,
collectionType = collectionType,
onClick = onClick,
onLongClick = onLongClick,
modifier = mod,
)
},
) {
var position by rememberInt(savedPosition)
@ -1005,11 +1035,22 @@ fun CollectionFolderViewContent(
focusRequester?.tryRequestFocus()
clickedRandom = false
}
val scope =
object : CollectionFolderViewScope {
override val viewOptions: ViewOptions
get() = state.viewOptions
override val collectionType: CollectionType?
get() = item?.data?.collectionType
override val sortAndDirection: SortAndDirection
get() = state.sortAndDirection
override val collectionSize: Int
get() = pager.data.size
}
Box(Modifier.fillMaxSize()) {
if (state.viewOptions.type == ViewOptionsType.GRID) {
CollectionFolderGrid(
scope.CollectionFolderGrid(
preferences = preferences,
collectionType = item?.data?.collectionType,
initialPosition = savedPosition,
items = pager.data,
sortAndDirection = state.sortAndDirection,
@ -1023,12 +1064,12 @@ fun CollectionFolderViewContent(
positionCallback?.invoke(columns, pos)
},
letterPosition = { viewActions.positionOfLetter(it) ?: -1 },
viewOptions = state.viewOptions,
onClickPlay = gridActions.onClickPlayRemoteButton!!,
focusedItem = focusedItem,
cardContent = gridCardContent,
)
} else {
CollectionFolderList(
scope.CollectionFolderList(
preferences = preferences,
collectionType = item?.data?.collectionType,
initialPosition = savedPosition,
@ -1044,9 +1085,9 @@ fun CollectionFolderViewContent(
positionCallback?.invoke(columns, pos)
},
letterPosition = { viewActions.positionOfLetter(it) ?: -1 },
viewOptions = state.viewOptions,
onClickPlay = gridActions.onClickPlayRemoteButton!!,
focusedItem = focusedItem,
itemContent = listItemContent,
)
}
androidx.compose.animation.AnimatedVisibility(
@ -1085,3 +1126,13 @@ fun CollectionFolderViewContent(
)
}
}
/**
* Scope with global data for [CollectionFolderView], [CollectionFolderGrid], [CollectionFolderList], and individual items
*/
interface CollectionFolderViewScope {
val viewOptions: ViewOptions
val collectionType: CollectionType?
val sortAndDirection: SortAndDirection
val collectionSize: Int
}