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:
parent
6cde8ce8a6
commit
52c26bc0fc
4 changed files with 397 additions and 725 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue