mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Add list view for libraries (#1428)
## Description Adds an option to display libraries in a list layout The list can be regular showing 6-7 items or compact showing ~11 items. Can also toggle showing the details on the left side. Movies, tv shows, & albums have type-specific layouts and other types like videos, music videos, etc use a generic one similar to movies. Another change is that showing the backdrop is now a separate toggle from showing details for both grid & list layouts. Sorting, filtering, & jumping to a letter all work as expected! ### Related issues Closes #1101 ### Testing Emulator mostly ## Screenshots <details><summary>Click for screenshots</summary> <p> ### TV shows list <img width="1280" height="720" alt="series_list Large" src="https://github.com/user-attachments/assets/f8f9b801-e537-4cdf-b992-f2f1259457fa" /> ### TV shows compact list <img width="1280" height="720" alt="series_list_compact Large" src="https://github.com/user-attachments/assets/747e5929-90d7-4d59-a179-bc3210ac01b9" /> ### Movie list <img width="1280" height="720" alt="movie_list Large" src="https://github.com/user-attachments/assets/ee3e1c6c-fb25-438a-a11e-522d30fbaabf" /> ### Album list <img width="1280" height="720" alt="music_list Large" src="https://github.com/user-attachments/assets/9b1f59ec-c237-4ab2-9b51-6cb519f5327c" /> </p> </details> ## AI or LLM usage None
This commit is contained in:
parent
8177700e23
commit
982d9e6fe8
18 changed files with 1913 additions and 949 deletions
|
|
@ -27,6 +27,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
|
@ -227,13 +228,7 @@ fun ItemCardImageOverlay(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (favorite) {
|
if (favorite) {
|
||||||
Text(
|
FavoriteIndicator()
|
||||||
color = colorResource(android.R.color.holo_red_light),
|
|
||||||
text = stringResource(R.string.fa_heart),
|
|
||||||
fontSize = 20.sp,
|
|
||||||
fontFamily = FontAwesome,
|
|
||||||
modifier = Modifier,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -281,3 +276,15 @@ fun ItemCardImageOverlay(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FavoriteIndicator(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
fontSize: TextUnit = 20.sp,
|
||||||
|
) = Text(
|
||||||
|
color = colorResource(android.R.color.holo_red_light),
|
||||||
|
text = stringResource(R.string.fa_heart),
|
||||||
|
fontSize = fontSize,
|
||||||
|
fontFamily = FontAwesome,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
|
@ -19,7 +20,10 @@ import com.github.damontecres.wholphin.ui.theme.LocalTheme
|
||||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WatchedIcon(modifier: Modifier = Modifier) {
|
fun WatchedIcon(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
padding: Dp = 2.dp,
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Check,
|
imageVector = Icons.Default.Check,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|
@ -28,7 +32,7 @@ fun WatchedIcon(modifier: Modifier = Modifier) {
|
||||||
modifier
|
modifier
|
||||||
.background(WatchedIconBackground(), shape = CircleShape)
|
.background(WatchedIconBackground(), shape = CircleShape)
|
||||||
.border(.5.dp, Color.Black, CircleShape)
|
.border(.5.dp, Color.Black, CircleShape)
|
||||||
.padding(2.dp),
|
.padding(padding),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,681 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.expandHorizontally
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkHorizontally
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.ListItemDefaults
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
|
import coil3.annotation.ExperimentalCoilApi
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.compose.useExistingImageAsPlaceholder
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.data.filter.FilterValueOption
|
||||||
|
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||||
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||||
|
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
|
||||||
|
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.enableMarquee
|
||||||
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
|
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||||
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CollectionFolderList(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
item: BaseItem?,
|
||||||
|
title: String,
|
||||||
|
loadingState: DataLoadingState<List<BaseItem?>>,
|
||||||
|
sortAndDirection: SortAndDirection,
|
||||||
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
|
onLongClickItem: (Int, BaseItem) -> Unit,
|
||||||
|
onSortChange: (SortAndDirection) -> Unit,
|
||||||
|
letterPosition: suspend (Char) -> Int,
|
||||||
|
sortOptions: List<ItemSortBy>,
|
||||||
|
playEnabled: Boolean,
|
||||||
|
getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List<FilterValueOption>,
|
||||||
|
viewOptions: ViewOptions,
|
||||||
|
onClickPlayAll: (shuffle: Boolean) -> Unit,
|
||||||
|
onClickPlay: (Int, BaseItem) -> Unit,
|
||||||
|
onChangeBackdrop: (BaseItem) -> Unit,
|
||||||
|
onClickShowViewOptions: () -> Unit,
|
||||||
|
initialPosition: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
showTitle: Boolean = true,
|
||||||
|
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||||
|
currentFilter: GetItemsFilter = GetItemsFilter(),
|
||||||
|
filterOptions: List<ItemFilterBy<*>> = listOf(),
|
||||||
|
onFilterChange: (GetItemsFilter) -> Unit = {},
|
||||||
|
focusRequesterOnEmpty: FocusRequester? = null,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val pager = (loadingState as? DataLoadingState.Success)?.data
|
||||||
|
var showHeader by rememberSaveable { mutableStateOf(true) }
|
||||||
|
val headerRowFocusRequester = remember { FocusRequester() }
|
||||||
|
val showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME
|
||||||
|
|
||||||
|
val gridFocusRequester = remember { FocusRequester() }
|
||||||
|
if (pager?.isNotEmpty() == true) {
|
||||||
|
RequestOrRestoreFocus(gridFocusRequester)
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
(focusRequesterOnEmpty ?: headerRowFocusRequester).tryRequestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
var contentHashFocus by remember { mutableStateOf(false) }
|
||||||
|
var position by rememberInt(initialPosition)
|
||||||
|
val positionFocusRequester = remember { FocusRequester() }
|
||||||
|
val focusedItem = pager?.getOrNull(position)
|
||||||
|
if (viewOptions.showBackdrop) {
|
||||||
|
LaunchedEffect(focusedItem) {
|
||||||
|
focusedItem?.let(onChangeBackdrop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun jumpTo(newPosition: Int) {
|
||||||
|
scope.launch {
|
||||||
|
position = newPosition
|
||||||
|
listState.animateScrollToItem(newPosition)
|
||||||
|
positionFocusRequester.tryRequestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(contentHashFocus && position > 0) {
|
||||||
|
jumpTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
CollectionFolderHeader(
|
||||||
|
showHeader = showHeader || loadingState !is DataLoadingState.Success,
|
||||||
|
showTitle = showTitle,
|
||||||
|
playEnabled = playEnabled && pager?.isNotEmpty() == true,
|
||||||
|
title = title,
|
||||||
|
sortAndDirection = sortAndDirection,
|
||||||
|
onSortChange = onSortChange,
|
||||||
|
sortOptions = sortOptions,
|
||||||
|
getPossibleFilterValues = getPossibleFilterValues,
|
||||||
|
onClickShowViewOptions = onClickShowViewOptions,
|
||||||
|
onClickPlayAll = onClickPlayAll,
|
||||||
|
currentFilter = currentFilter,
|
||||||
|
filterOptions = filterOptions,
|
||||||
|
onFilterChange = onFilterChange,
|
||||||
|
modifier = Modifier.focusRequester(headerRowFocusRequester),
|
||||||
|
)
|
||||||
|
when (val state = loadingState) {
|
||||||
|
DataLoadingState.Pending,
|
||||||
|
DataLoadingState.Loading,
|
||||||
|
-> {
|
||||||
|
// This shouldn't happen, so just show placeholder
|
||||||
|
Text(stringResource(R.string.loading))
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataLoadingState.Error -> {
|
||||||
|
ErrorMessage(state.message, state.exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataLoadingState.Success<List<BaseItem?>> -> {
|
||||||
|
val pager = state.data
|
||||||
|
val topPadding by animateDpAsState(
|
||||||
|
targetValue =
|
||||||
|
when {
|
||||||
|
showHeader -> 8.dp
|
||||||
|
|
||||||
|
// showClock-> TODO()
|
||||||
|
else -> 48.dp
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(top = topPadding, bottom = 16.dp)
|
||||||
|
.onFocusChanged {
|
||||||
|
contentHashFocus = it.hasFocus
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = viewOptions.showDetails,
|
||||||
|
enter = fadeIn() + expandHorizontally(expandFrom = Alignment.Start),
|
||||||
|
exit = fadeOut() + shrinkHorizontally(shrinkTowards = Alignment.Start),
|
||||||
|
) {
|
||||||
|
CollectionFolderListDetails(
|
||||||
|
item = focusedItem,
|
||||||
|
showLogo = true,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(.33f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.background(
|
||||||
|
color = Color.Transparent, // MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
).padding(8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CollectionFolderContent(
|
||||||
|
items = state.data,
|
||||||
|
viewOptions = viewOptions,
|
||||||
|
collectionType = item?.data?.collectionType,
|
||||||
|
listState = listState,
|
||||||
|
position = position,
|
||||||
|
positionFocusRequester = positionFocusRequester,
|
||||||
|
onPositionChange = {
|
||||||
|
showHeader = it < 1
|
||||||
|
positionCallback?.invoke(1, it)
|
||||||
|
position = it
|
||||||
|
},
|
||||||
|
onClickItem = onClickItem,
|
||||||
|
onLongClickItem = onLongClickItem,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.focusRequester(gridFocusRequester),
|
||||||
|
)
|
||||||
|
val letters = stringResource(R.string.jump_letters)
|
||||||
|
// Letters
|
||||||
|
val currentLetter =
|
||||||
|
remember(focusedItem) {
|
||||||
|
focusedItem
|
||||||
|
?.sortName
|
||||||
|
?.firstOrNull()
|
||||||
|
?.uppercaseChar()
|
||||||
|
?.let {
|
||||||
|
when (it) {
|
||||||
|
in '0'..'9' -> '#'
|
||||||
|
in 'A'..'Z' -> it
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?: letters[0]
|
||||||
|
}
|
||||||
|
if (showLetterButtons && pager.isNotEmpty()) {
|
||||||
|
AlphabetButtons(
|
||||||
|
letters = letters,
|
||||||
|
currentLetter = currentLetter,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
letterClicked = {
|
||||||
|
scope.launchIO {
|
||||||
|
val position = letterPosition.invoke(it)
|
||||||
|
jumpTo(position)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoilApi::class)
|
||||||
|
@Composable
|
||||||
|
fun CollectionFolderListDetails(
|
||||||
|
item: BaseItem?,
|
||||||
|
showLogo: Boolean,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
val imageUrlService = LocalImageUrlService.current
|
||||||
|
val imageUrl =
|
||||||
|
remember(item) {
|
||||||
|
item?.imageUrlOverride ?: imageUrlService.getItemImageUrl(item, ImageType.PRIMARY, useSeriesForPrimary = true)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
SimpleTitleOrLogo(
|
||||||
|
item,
|
||||||
|
showLogo,
|
||||||
|
Modifier
|
||||||
|
.height(HeaderUtils.logoHeight)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model =
|
||||||
|
ImageRequest
|
||||||
|
.Builder(LocalContext.current)
|
||||||
|
.data(imageUrl)
|
||||||
|
.crossfade(300)
|
||||||
|
.useExistingImageAsPlaceholder(true)
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.clip(shape = RoundedCornerShape(8.dp))
|
||||||
|
.weight(1f),
|
||||||
|
)
|
||||||
|
|
||||||
|
item?.let {
|
||||||
|
QuickDetails(item.ui.quickDetails, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
OverviewText(
|
||||||
|
overview = item?.data?.overview ?: "",
|
||||||
|
maxLines = 5,
|
||||||
|
onClick = {},
|
||||||
|
enabled = false,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
// index?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CollectionFolderContent(
|
||||||
|
items: List<BaseItem?>,
|
||||||
|
viewOptions: ViewOptions,
|
||||||
|
collectionType: CollectionType?,
|
||||||
|
listState: LazyListState,
|
||||||
|
position: Int,
|
||||||
|
positionFocusRequester: FocusRequester,
|
||||||
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
|
onLongClickItem: (Int, BaseItem) -> Unit,
|
||||||
|
onPositionChange: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
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 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)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CollectionFolderListItem(
|
||||||
|
item: BaseItem?,
|
||||||
|
dense: Boolean,
|
||||||
|
collectionType: CollectionType?,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
when (collectionType) {
|
||||||
|
CollectionType.MOVIES -> ListItemMovie(dense, item, onClick, onLongClick, modifier)
|
||||||
|
CollectionType.TVSHOWS -> ListItemSeries(dense, item, onClick, onLongClick, modifier)
|
||||||
|
CollectionType.MUSIC -> ListItemAlbum(dense, item, onClick, onLongClick, modifier)
|
||||||
|
else -> ListItemGeneric(dense, item, onClick, onLongClick, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ListItemGeneric(
|
||||||
|
dense: Boolean,
|
||||||
|
item: BaseItem?,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) {
|
||||||
|
ListItemWrapper(
|
||||||
|
dense = dense,
|
||||||
|
selected = false,
|
||||||
|
enabled = true,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
leadingContent = {
|
||||||
|
if (item?.favorite == true) {
|
||||||
|
FavoriteIndicator()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headlineContent = { TitleContent(item?.title, interactionSource) },
|
||||||
|
supportingContent =
|
||||||
|
item?.subtitle?.let { subtitle ->
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
item
|
||||||
|
?.data
|
||||||
|
?.runTimeTicks
|
||||||
|
?.ticks
|
||||||
|
?.roundMinutes
|
||||||
|
?.toString() ?: "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
scale = scale,
|
||||||
|
colors = listItemColors(),
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ListItemMovie(
|
||||||
|
dense: Boolean,
|
||||||
|
item: BaseItem?,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) {
|
||||||
|
val title =
|
||||||
|
remember(item, dense) {
|
||||||
|
if (dense && item != null && item.title != null && item.data.productionYear != null) {
|
||||||
|
"${item.title} (${ item.data.productionYear})"
|
||||||
|
} else {
|
||||||
|
item?.title ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val supportingContent =
|
||||||
|
remember(item, dense) {
|
||||||
|
if (!dense) {
|
||||||
|
@Composable {
|
||||||
|
Text(item?.subtitle ?: "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ListItemWrapper(
|
||||||
|
dense = dense,
|
||||||
|
selected = false,
|
||||||
|
enabled = true,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
leadingContent = {
|
||||||
|
if (item?.favorite == true) {
|
||||||
|
FavoriteIndicator()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headlineContent = { TitleContent(title, interactionSource) },
|
||||||
|
supportingContent = supportingContent,
|
||||||
|
trailingContent = {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
item
|
||||||
|
?.data
|
||||||
|
?.runTimeTicks
|
||||||
|
?.ticks
|
||||||
|
?.roundMinutes
|
||||||
|
?.toString() ?: "",
|
||||||
|
)
|
||||||
|
if (item?.played == true) {
|
||||||
|
WatchedIcon(padding = 0.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scale = scale,
|
||||||
|
colors = listItemColors(),
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ListItemSeries(
|
||||||
|
dense: Boolean,
|
||||||
|
item: BaseItem?,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) {
|
||||||
|
val title =
|
||||||
|
remember(item, dense) {
|
||||||
|
if (item != null) {
|
||||||
|
if (dense) {
|
||||||
|
"${item.title} (${item.subtitle})"
|
||||||
|
} else {
|
||||||
|
item.title ?: ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val supportingContent =
|
||||||
|
remember(item, dense) {
|
||||||
|
if (!dense) {
|
||||||
|
@Composable {
|
||||||
|
Text(item?.subtitle ?: "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val trailingContent =
|
||||||
|
remember(item) {
|
||||||
|
val unplayedItemCount = item?.data?.userData?.unplayedItemCount
|
||||||
|
if (unplayedItemCount != null && unplayedItemCount > 0) {
|
||||||
|
@Composable { Text(unplayedItemCount.toString()) }
|
||||||
|
} else if (item?.played == true) {
|
||||||
|
@Composable { WatchedIcon(padding = 0.dp) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ListItemWrapper(
|
||||||
|
dense = dense,
|
||||||
|
selected = false,
|
||||||
|
enabled = true,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
leadingContent = {
|
||||||
|
if (item?.favorite == true) {
|
||||||
|
FavoriteIndicator()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
TitleContent(title, interactionSource)
|
||||||
|
},
|
||||||
|
supportingContent = supportingContent,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
scale = scale,
|
||||||
|
colors = listItemColors(),
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ListItemAlbum(
|
||||||
|
dense: Boolean,
|
||||||
|
item: BaseItem?,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) {
|
||||||
|
val title =
|
||||||
|
remember(item, dense) {
|
||||||
|
if (dense && item != null && item.title != null && item.data.productionYear != null) {
|
||||||
|
"${item.title} (${item.data.productionYear})"
|
||||||
|
} else {
|
||||||
|
item?.title ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val supportingContent =
|
||||||
|
remember(item, dense) {
|
||||||
|
if (!dense && item?.subtitle != null) {
|
||||||
|
@Composable {
|
||||||
|
Text(item.subtitle ?: "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val overlineContent =
|
||||||
|
remember(item, dense) {
|
||||||
|
if (!dense && item?.data?.albumArtist != null) {
|
||||||
|
@Composable {
|
||||||
|
Text(item.data.albumArtist ?: "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val trailingContent =
|
||||||
|
remember(item) {
|
||||||
|
if (item?.type == BaseItemKind.MUSIC_ALBUM) {
|
||||||
|
@Composable {
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
item
|
||||||
|
.data
|
||||||
|
.runTimeTicks
|
||||||
|
?.ticks
|
||||||
|
?.roundMinutes
|
||||||
|
?.toString() ?: "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ListItemWrapper(
|
||||||
|
dense = dense,
|
||||||
|
selected = false,
|
||||||
|
enabled = true,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
leadingContent = {
|
||||||
|
if (item?.favorite == true) {
|
||||||
|
FavoriteIndicator()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headlineContent = { TitleContent(title, interactionSource) },
|
||||||
|
supportingContent = supportingContent,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
overlineContent = overlineContent,
|
||||||
|
scale = scale,
|
||||||
|
colors = listItemColors(),
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TitleContent(
|
||||||
|
title: String?,
|
||||||
|
interactionSource: MutableInteractionSource,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
|
var focusedAfterDelay by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val hideOverlayDelay = 500L
|
||||||
|
if (focused) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(hideOverlayDelay)
|
||||||
|
if (focused) {
|
||||||
|
focusedAfterDelay = true
|
||||||
|
} else {
|
||||||
|
focusedAfterDelay = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
focusedAfterDelay = false
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = title ?: "",
|
||||||
|
modifier = modifier.enableMarquee(focusedAfterDelay),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun listItemColors() =
|
||||||
|
ListItemDefaults.colors(
|
||||||
|
containerColor =
|
||||||
|
MaterialTheme.colorScheme
|
||||||
|
.surfaceColorAtElevation(1.dp)
|
||||||
|
.copy(alpha = .5f),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val scale = ListItemDefaults.scale(focusedScale = 1f, pressedScale = .95f)
|
||||||
|
|
@ -0,0 +1,899 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
||||||
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
|
import com.github.damontecres.wholphin.data.filter.DefaultFilterOptions
|
||||||
|
import com.github.damontecres.wholphin.data.filter.FilterValueOption
|
||||||
|
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||||
|
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.data.model.GetItemsFilterOverride
|
||||||
|
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
||||||
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
|
import com.github.damontecres.wholphin.services.MediaManagementService
|
||||||
|
import com.github.damontecres.wholphin.services.MediaReportService
|
||||||
|
import com.github.damontecres.wholphin.services.MusicService
|
||||||
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
|
import com.github.damontecres.wholphin.services.StreamChoiceService
|
||||||
|
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.data.AddPlaylistViewModel
|
||||||
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||||
|
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
|
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.setValueOnMain
|
||||||
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
|
import com.github.damontecres.wholphin.ui.toServerString
|
||||||
|
import com.github.damontecres.wholphin.ui.util.FilterUtils
|
||||||
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
|
import com.github.damontecres.wholphin.util.GetPersonsHandler
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
|
import org.jellyfin.sdk.model.api.request.GetPersonsRequest
|
||||||
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@HiltViewModel(assistedFactory = CollectionFolderViewModel.Factory::class)
|
||||||
|
class CollectionFolderViewModel
|
||||||
|
@AssistedInject
|
||||||
|
constructor(
|
||||||
|
private val savedStateHandle: SavedStateHandle,
|
||||||
|
api: ApiClient,
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
|
val serverRepository: ServerRepository,
|
||||||
|
private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
|
||||||
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
|
private val backdropService: BackdropService,
|
||||||
|
private val navigationManager: NavigationManager,
|
||||||
|
private val themeSongPlayer: ThemeSongPlayer,
|
||||||
|
private val userPreferencesService: UserPreferencesService,
|
||||||
|
private val mediaManagementService: MediaManagementService,
|
||||||
|
private val musicService: MusicService,
|
||||||
|
val streamChoiceService: StreamChoiceService,
|
||||||
|
val mediaReportService: MediaReportService,
|
||||||
|
@Assisted itemId: String,
|
||||||
|
@Assisted initialSortAndDirection: SortAndDirection?,
|
||||||
|
@Assisted("recursive") private val recursive: Boolean,
|
||||||
|
@Assisted private val collectionFilter: CollectionFolderFilter,
|
||||||
|
@Assisted("useSeriesForPrimary") private val useSeriesForPrimary: Boolean,
|
||||||
|
@Assisted defaultViewOptions: ViewOptions,
|
||||||
|
) : ItemViewModel(api) {
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(
|
||||||
|
itemId: String,
|
||||||
|
initialSortAndDirection: SortAndDirection?,
|
||||||
|
@Assisted("recursive") recursive: Boolean,
|
||||||
|
collectionFilter: CollectionFolderFilter,
|
||||||
|
@Assisted("useSeriesForPrimary") useSeriesForPrimary: Boolean,
|
||||||
|
defaultViewOptions: ViewOptions,
|
||||||
|
): CollectionFolderViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
val loading = MutableLiveData<DataLoadingState<List<BaseItem?>>>(DataLoadingState.Loading)
|
||||||
|
val backgroundLoading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
|
val sortAndDirection = MutableLiveData<SortAndDirection>()
|
||||||
|
val filter = MutableLiveData<GetItemsFilter>(GetItemsFilter())
|
||||||
|
val viewOptions = MutableStateFlow<ViewOptions>(defaultViewOptions)
|
||||||
|
|
||||||
|
var position: Int
|
||||||
|
get() = savedStateHandle.get<Int>("position") ?: 0
|
||||||
|
set(value) {
|
||||||
|
savedStateHandle["position"] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
super.itemId = itemId
|
||||||
|
try {
|
||||||
|
val item =
|
||||||
|
itemId.toUUIDOrNull()?.let {
|
||||||
|
fetchItem(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val libraryDisplayInfo =
|
||||||
|
serverRepository.currentUser.value?.let { user ->
|
||||||
|
libraryDisplayInfoDao.getItem(user, itemId)
|
||||||
|
}
|
||||||
|
this@CollectionFolderViewModel.viewOptions.value =
|
||||||
|
libraryDisplayInfo?.viewOptions ?: defaultViewOptions
|
||||||
|
|
||||||
|
val sortAndDirection =
|
||||||
|
if (collectionFilter.useSavedLibraryDisplayInfo) {
|
||||||
|
libraryDisplayInfo?.sortAndDirection
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
} ?: initialSortAndDirection ?: SortAndDirection.DEFAULT
|
||||||
|
|
||||||
|
val filterToUse =
|
||||||
|
if (collectionFilter.useSavedLibraryDisplayInfo && libraryDisplayInfo?.filter != null) {
|
||||||
|
collectionFilter.filter.merge(libraryDisplayInfo.filter)
|
||||||
|
} else {
|
||||||
|
collectionFilter.filter
|
||||||
|
}
|
||||||
|
|
||||||
|
loadResults(true, sortAndDirection, recursive, filterToUse, useSeriesForPrimary)
|
||||||
|
.join()
|
||||||
|
// onResumePage()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Error during init")
|
||||||
|
loading.setValueOnMain(DataLoadingState.Error(ex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaManagementService.deletedItemFlow
|
||||||
|
.onEach { deletedItem ->
|
||||||
|
refreshAfterDelete(position, deletedItem.item)
|
||||||
|
}.catch { ex ->
|
||||||
|
Timber.e(ex, "Error refreshing after deleted item")
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshAfterDelete(
|
||||||
|
position: Int,
|
||||||
|
deletedItem: BaseItem,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val pager =
|
||||||
|
((loading.value as? DataLoadingState.Success)?.data as? ApiRequestPager<*>)
|
||||||
|
position.let {
|
||||||
|
Timber.v("Item deleted: position=%s, id=%s", it, itemId)
|
||||||
|
val item = pager?.get(it)
|
||||||
|
// Exact item deleted (eg a movie) or deleted item was within the series
|
||||||
|
if (item?.id == deletedItem.id ||
|
||||||
|
equalsNotNull(item?.data?.id, deletedItem.data.seriesId)
|
||||||
|
) {
|
||||||
|
pager?.refreshPagesAfter(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Error refreshing after deleted item %s", itemId)
|
||||||
|
showToast(context, "Error refreshing after item deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveLibraryDisplayInfo(
|
||||||
|
newFilter: GetItemsFilter = this.filter.value!!,
|
||||||
|
newSort: SortAndDirection = this.sortAndDirection.value!!,
|
||||||
|
viewOptions: ViewOptions? = this.viewOptions.value,
|
||||||
|
) {
|
||||||
|
if (collectionFilter.useSavedLibraryDisplayInfo) {
|
||||||
|
serverRepository.currentUser.value?.let { user ->
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val libraryDisplayInfo =
|
||||||
|
LibraryDisplayInfo(
|
||||||
|
userId = user.rowId,
|
||||||
|
itemId = itemId,
|
||||||
|
sort = newSort.sort,
|
||||||
|
direction = newSort.direction,
|
||||||
|
filter = newFilter,
|
||||||
|
viewOptions = viewOptions,
|
||||||
|
)
|
||||||
|
libraryDisplayInfoDao.saveItem(libraryDisplayInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveViewOptions(viewOptions: ViewOptions) {
|
||||||
|
this.viewOptions.value = viewOptions
|
||||||
|
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
|
saveLibraryDisplayInfo(viewOptions = viewOptions)
|
||||||
|
if (!viewOptions.showBackdrop) {
|
||||||
|
backdropService.clearBackdrop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onFilterChange(
|
||||||
|
newFilter: GetItemsFilter,
|
||||||
|
recursive: Boolean,
|
||||||
|
) {
|
||||||
|
Timber.v("onFilterChange: filter=%s", newFilter)
|
||||||
|
saveLibraryDisplayInfo(newFilter, sortAndDirection.value!!)
|
||||||
|
loadResults(false, sortAndDirection.value!!, recursive, newFilter, useSeriesForPrimary)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSortChange(
|
||||||
|
sortAndDirection: SortAndDirection,
|
||||||
|
recursive: Boolean,
|
||||||
|
filter: GetItemsFilter,
|
||||||
|
) {
|
||||||
|
Timber.v(
|
||||||
|
"onSortChange: sort=%s, recursive=%s, filter=%s",
|
||||||
|
sortAndDirection,
|
||||||
|
recursive,
|
||||||
|
filter,
|
||||||
|
)
|
||||||
|
saveLibraryDisplayInfo(filter, sortAndDirection)
|
||||||
|
loadResults(true, sortAndDirection, recursive, filter, useSeriesForPrimary)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadResults(
|
||||||
|
resetState: Boolean,
|
||||||
|
sortAndDirection: SortAndDirection,
|
||||||
|
recursive: Boolean,
|
||||||
|
filter: GetItemsFilter,
|
||||||
|
useSeriesForPrimary: Boolean,
|
||||||
|
) = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (resetState) {
|
||||||
|
loading.value = DataLoadingState.Loading
|
||||||
|
}
|
||||||
|
backgroundLoading.value = LoadingState.Loading
|
||||||
|
this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection
|
||||||
|
this@CollectionFolderViewModel.filter.value = filter
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val newPager =
|
||||||
|
createPager(sortAndDirection, recursive, filter, useSeriesForPrimary).init()
|
||||||
|
if (newPager.isNotEmpty()) newPager.getBlocking(0)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
loading.value = DataLoadingState.Success(newPager)
|
||||||
|
backgroundLoading.value = LoadingState.Success
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(
|
||||||
|
ex,
|
||||||
|
"Exception while loading data: sort=%s, filter=%s",
|
||||||
|
sortAndDirection,
|
||||||
|
filter,
|
||||||
|
)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
loading.value = DataLoadingState.Error(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPager(
|
||||||
|
sortAndDirection: SortAndDirection,
|
||||||
|
recursive: Boolean,
|
||||||
|
filter: GetItemsFilter,
|
||||||
|
useSeriesForPrimary: Boolean,
|
||||||
|
): ApiRequestPager<out Any> =
|
||||||
|
when (filter.override) {
|
||||||
|
GetItemsFilterOverride.NONE -> {
|
||||||
|
val request =
|
||||||
|
createGetItemsRequest(
|
||||||
|
sortAndDirection = sortAndDirection,
|
||||||
|
recursive = recursive,
|
||||||
|
filter = filter,
|
||||||
|
)
|
||||||
|
val newPager =
|
||||||
|
ApiRequestPager(
|
||||||
|
api,
|
||||||
|
request,
|
||||||
|
GetItemsRequestHandler,
|
||||||
|
viewModelScope,
|
||||||
|
useSeriesForPrimary = useSeriesForPrimary,
|
||||||
|
)
|
||||||
|
newPager
|
||||||
|
}
|
||||||
|
|
||||||
|
GetItemsFilterOverride.PERSON -> {
|
||||||
|
val request =
|
||||||
|
filter.applyTo(
|
||||||
|
GetPersonsRequest(
|
||||||
|
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val newPager =
|
||||||
|
ApiRequestPager(
|
||||||
|
api,
|
||||||
|
request,
|
||||||
|
GetPersonsHandler,
|
||||||
|
viewModelScope,
|
||||||
|
useSeriesForPrimary = useSeriesForPrimary,
|
||||||
|
)
|
||||||
|
newPager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createGetItemsRequest(
|
||||||
|
sortAndDirection: SortAndDirection,
|
||||||
|
recursive: Boolean,
|
||||||
|
filter: GetItemsFilter,
|
||||||
|
): GetItemsRequest {
|
||||||
|
val item = item.value
|
||||||
|
val includeItemTypes =
|
||||||
|
item
|
||||||
|
?.data
|
||||||
|
?.collectionType
|
||||||
|
?.baseItemKinds
|
||||||
|
.orEmpty()
|
||||||
|
val request =
|
||||||
|
filter.applyTo(
|
||||||
|
GetItemsRequest(
|
||||||
|
parentId = item?.id,
|
||||||
|
enableImageTypes =
|
||||||
|
listOf(
|
||||||
|
ImageType.PRIMARY,
|
||||||
|
ImageType.THUMB,
|
||||||
|
ImageType.BACKDROP,
|
||||||
|
ImageType.LOGO,
|
||||||
|
),
|
||||||
|
includeItemTypes = includeItemTypes,
|
||||||
|
recursive = recursive,
|
||||||
|
excludeItemIds = item?.let { listOf(item.id) },
|
||||||
|
sortBy =
|
||||||
|
buildList {
|
||||||
|
if (sortAndDirection.sort != ItemSortBy.DEFAULT) {
|
||||||
|
add(sortAndDirection.sort)
|
||||||
|
if (sortAndDirection.sort != ItemSortBy.SORT_NAME) {
|
||||||
|
add(ItemSortBy.SORT_NAME)
|
||||||
|
}
|
||||||
|
if (item?.data?.collectionType == CollectionType.MOVIES) {
|
||||||
|
add(ItemSortBy.PRODUCTION_YEAR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sortOrder =
|
||||||
|
buildList {
|
||||||
|
if (sortAndDirection.sort != ItemSortBy.DEFAULT) {
|
||||||
|
add(sortAndDirection.direction)
|
||||||
|
if (sortAndDirection.sort != ItemSortBy.SORT_NAME) {
|
||||||
|
add(SortOrder.ASCENDING)
|
||||||
|
}
|
||||||
|
if (item?.data?.collectionType == CollectionType.MOVIES) {
|
||||||
|
add(SortOrder.ASCENDING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fields = SlimItemFields,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getFilterOptionValues(filterOption: ItemFilterBy<*>): List<FilterValueOption> =
|
||||||
|
FilterUtils.getFilterOptionValues(
|
||||||
|
api,
|
||||||
|
serverRepository.currentUser.value?.id,
|
||||||
|
itemUuid,
|
||||||
|
filterOption,
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun positionOfLetter(letter: Char): Int? =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val sort = sortAndDirection.value
|
||||||
|
val filter = filter.value
|
||||||
|
if (sort == null || filter == null) {
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
val request =
|
||||||
|
createGetItemsRequest(
|
||||||
|
sortAndDirection = sort,
|
||||||
|
recursive = recursive,
|
||||||
|
filter = filter,
|
||||||
|
).copy(
|
||||||
|
enableImageTypes = null,
|
||||||
|
fields = null,
|
||||||
|
nameLessThan = letter.toString(),
|
||||||
|
limit = 0,
|
||||||
|
enableTotalRecordCount = true,
|
||||||
|
)
|
||||||
|
val result by GetItemsRequestHandler.execute(api, request)
|
||||||
|
result.totalRecordCount
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWatched(
|
||||||
|
position: Int,
|
||||||
|
itemId: UUID,
|
||||||
|
played: Boolean,
|
||||||
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
|
favoriteWatchManager.setWatched(itemId, played)
|
||||||
|
(loading.value as? DataLoadingState.Success)?.let {
|
||||||
|
(it.data as? ApiRequestPager<*>)?.refreshItem(position, itemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFavorite(
|
||||||
|
position: Int,
|
||||||
|
itemId: UUID,
|
||||||
|
favorite: Boolean,
|
||||||
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
|
favoriteWatchManager.setFavorite(itemId, favorite)
|
||||||
|
(loading.value as? DataLoadingState.Success)?.let {
|
||||||
|
(it.data as? ApiRequestPager<*>)?.refreshItem(position, itemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateBackdrop(item: BaseItem) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
backdropService.submit(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateTo(destination: Destination) {
|
||||||
|
release()
|
||||||
|
navigationManager.navigateTo(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
themeSongPlayer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onResumePage() {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
item.value?.let {
|
||||||
|
Timber.v("onResumePage: %s", loading.value!!::class)
|
||||||
|
if (it.type == BaseItemKind.BOX_SET && loading.value !is DataLoadingState.Error) {
|
||||||
|
themeSongPlayer.playThemeFor(it.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteItem(
|
||||||
|
index: Int,
|
||||||
|
item: BaseItem,
|
||||||
|
) {
|
||||||
|
deleteItem(context, mediaManagementService, item) {
|
||||||
|
viewModelScope.launchDefault {
|
||||||
|
refreshAfterDelete(index, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun canDelete(
|
||||||
|
item: BaseItem,
|
||||||
|
appPreferences: AppPreferences,
|
||||||
|
): Boolean = mediaManagementService.canDelete(item, appPreferences)
|
||||||
|
|
||||||
|
fun addToQueue(
|
||||||
|
item: BaseItem,
|
||||||
|
index: Int,
|
||||||
|
) = addToQueue(api, musicService, item, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a collection folder as a grid
|
||||||
|
*
|
||||||
|
* This is the "Library" tab for Movies or TV shows
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun CollectionFolderView(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
itemId: UUID,
|
||||||
|
initialFilter: CollectionFolderFilter,
|
||||||
|
recursive: Boolean,
|
||||||
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
|
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,
|
||||||
|
) = CollectionFolderView(
|
||||||
|
preferences,
|
||||||
|
itemId.toServerString(),
|
||||||
|
initialFilter,
|
||||||
|
recursive,
|
||||||
|
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 CollectionFolderView(
|
||||||
|
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,
|
||||||
|
) = CollectionFolderView(
|
||||||
|
preferences,
|
||||||
|
itemId.toServerString(),
|
||||||
|
initialFilter,
|
||||||
|
recursive,
|
||||||
|
actions,
|
||||||
|
sortOptions,
|
||||||
|
playEnabled,
|
||||||
|
viewModelKey = viewModelKey,
|
||||||
|
defaultViewOptions = defaultViewOptions,
|
||||||
|
modifier = modifier,
|
||||||
|
initialSortAndDirection = initialSortAndDirection,
|
||||||
|
showTitle = showTitle,
|
||||||
|
positionCallback = positionCallback,
|
||||||
|
useSeriesForPrimary = useSeriesForPrimary,
|
||||||
|
filterOptions = filterOptions,
|
||||||
|
focusRequesterOnEmpty = focusRequesterOnEmpty,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CollectionFolderView(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
itemId: String,
|
||||||
|
initialFilter: CollectionFolderFilter,
|
||||||
|
recursive: Boolean,
|
||||||
|
actions: GridClickActions,
|
||||||
|
sortOptions: List<ItemSortBy>,
|
||||||
|
playEnabled: Boolean,
|
||||||
|
defaultViewOptions: ViewOptions,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModelKey: String? = itemId,
|
||||||
|
initialSortAndDirection: SortAndDirection? = null,
|
||||||
|
showTitle: Boolean = true,
|
||||||
|
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||||
|
useSeriesForPrimary: Boolean = true,
|
||||||
|
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||||
|
focusRequesterOnEmpty: FocusRequester? = null,
|
||||||
|
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||||
|
viewModel: CollectionFolderViewModel =
|
||||||
|
hiltViewModel<CollectionFolderViewModel, CollectionFolderViewModel.Factory>(
|
||||||
|
key = viewModelKey,
|
||||||
|
) {
|
||||||
|
it.create(
|
||||||
|
itemId = itemId,
|
||||||
|
initialSortAndDirection = initialSortAndDirection,
|
||||||
|
recursive = recursive,
|
||||||
|
collectionFilter = initialFilter,
|
||||||
|
useSeriesForPrimary = useSeriesForPrimary,
|
||||||
|
defaultViewOptions = defaultViewOptions,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val sortAndDirection by viewModel.sortAndDirection.observeAsState(SortAndDirection.DEFAULT)
|
||||||
|
val filter by viewModel.filter.observeAsState(initialFilter.filter)
|
||||||
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
|
val backgroundLoading by viewModel.backgroundLoading.observeAsState(LoadingState.Loading)
|
||||||
|
val item by viewModel.item.observeAsState()
|
||||||
|
val viewOptions by viewModel.viewOptions.collectAsState()
|
||||||
|
|
||||||
|
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
|
||||||
|
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||||
|
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||||
|
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||||
|
var showViewOptions by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val contextActions =
|
||||||
|
remember {
|
||||||
|
ContextMenuActions(
|
||||||
|
navigateTo = viewModel::navigateTo,
|
||||||
|
onClickWatch = { itemId, watched ->
|
||||||
|
viewModel.setWatched(viewModel.position, itemId, watched)
|
||||||
|
},
|
||||||
|
onClickFavorite = { itemId, favorite ->
|
||||||
|
viewModel.setFavorite(viewModel.position, itemId, favorite)
|
||||||
|
},
|
||||||
|
onClickAddPlaylist = { itemId ->
|
||||||
|
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
||||||
|
showPlaylistDialog.makePresent(itemId)
|
||||||
|
},
|
||||||
|
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||||
|
onDeleteItem = { viewModel.deleteItem(viewModel.position, it) },
|
||||||
|
onShowOverview = { overviewDialog = ItemDetailsDialogInfo(it) },
|
||||||
|
onChooseVersion = { _, _ ->
|
||||||
|
// Not supported on this page
|
||||||
|
},
|
||||||
|
onChooseTracks = { result ->
|
||||||
|
// Not supported on this page
|
||||||
|
},
|
||||||
|
onClearChosenStreams = {
|
||||||
|
// Not supported on this page
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val gridActions =
|
||||||
|
remember(actions) {
|
||||||
|
GridClickActions(
|
||||||
|
onClickItem = actions.onClickItem,
|
||||||
|
onLongClickItem =
|
||||||
|
actions.onLongClickItem ?: { position, item ->
|
||||||
|
showContextMenu =
|
||||||
|
ContextMenu.ForBaseItem(
|
||||||
|
fromLongClick = true,
|
||||||
|
item = item,
|
||||||
|
chosenStreams = null,
|
||||||
|
showGoTo = true,
|
||||||
|
showStreamChoices = false,
|
||||||
|
canDelete = viewModel.canDelete(item, preferences.appPreferences),
|
||||||
|
canRemoveContinueWatching = false,
|
||||||
|
canRemoveNextUp = false,
|
||||||
|
actions = contextActions,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
-> {
|
||||||
|
LoadingPage(modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataLoadingState.Error,
|
||||||
|
is DataLoadingState.Success<*>,
|
||||||
|
-> {
|
||||||
|
val title =
|
||||||
|
initialFilter.nameOverride
|
||||||
|
?: item?.name
|
||||||
|
?: item?.data?.collectionType?.name
|
||||||
|
?: stringResource(R.string.collection)
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
LifecycleResumeEffect(itemId) {
|
||||||
|
viewModel.onResumePage()
|
||||||
|
|
||||||
|
onPauseOrDispose {
|
||||||
|
viewModel.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (viewOptions.type == ViewOptionsType.GRID) {
|
||||||
|
CollectionFolderGrid(
|
||||||
|
preferences = preferences,
|
||||||
|
initialPosition = viewModel.position,
|
||||||
|
item = item,
|
||||||
|
title = title,
|
||||||
|
loadingState = state as DataLoadingState<List<BaseItem?>>,
|
||||||
|
sortAndDirection = sortAndDirection!!,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
focusRequesterOnEmpty = focusRequesterOnEmpty,
|
||||||
|
onClickItem = gridActions.onClickItem,
|
||||||
|
onLongClickItem = gridActions.onLongClickItem!!,
|
||||||
|
onSortChange = {
|
||||||
|
viewModel.onSortChange(it, recursive, filter)
|
||||||
|
},
|
||||||
|
filterOptions = filterOptions,
|
||||||
|
currentFilter = filter,
|
||||||
|
onFilterChange = {
|
||||||
|
viewModel.onFilterChange(it, recursive)
|
||||||
|
},
|
||||||
|
getPossibleFilterValues = {
|
||||||
|
viewModel.getFilterOptionValues(it)
|
||||||
|
},
|
||||||
|
showTitle = showTitle,
|
||||||
|
sortOptions = sortOptions,
|
||||||
|
positionCallback = { columns, position ->
|
||||||
|
viewModel.position = position
|
||||||
|
positionCallback?.invoke(columns, position)
|
||||||
|
},
|
||||||
|
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
|
||||||
|
viewOptions = viewOptions,
|
||||||
|
onChangeBackdrop = viewModel::updateBackdrop,
|
||||||
|
playEnabled = playEnabled,
|
||||||
|
onClickPlay = gridActions.onClickPlayRemoteButton!!,
|
||||||
|
onClickPlayAll = gridActions.onClickPlayAll!!,
|
||||||
|
onClickShowViewOptions = { showViewOptions = true },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
CollectionFolderList(
|
||||||
|
preferences = preferences,
|
||||||
|
initialPosition = viewModel.position,
|
||||||
|
item = item,
|
||||||
|
title = title,
|
||||||
|
loadingState = state as DataLoadingState<List<BaseItem?>>,
|
||||||
|
sortAndDirection = sortAndDirection!!,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
focusRequesterOnEmpty = focusRequesterOnEmpty,
|
||||||
|
onClickItem = gridActions.onClickItem,
|
||||||
|
onLongClickItem = gridActions.onLongClickItem!!,
|
||||||
|
onSortChange = {
|
||||||
|
viewModel.onSortChange(it, recursive, filter)
|
||||||
|
},
|
||||||
|
filterOptions = filterOptions,
|
||||||
|
currentFilter = filter,
|
||||||
|
onFilterChange = {
|
||||||
|
viewModel.onFilterChange(it, recursive)
|
||||||
|
},
|
||||||
|
getPossibleFilterValues = {
|
||||||
|
viewModel.getFilterOptionValues(it)
|
||||||
|
},
|
||||||
|
showTitle = showTitle,
|
||||||
|
sortOptions = sortOptions,
|
||||||
|
positionCallback = { columns, position ->
|
||||||
|
viewModel.position = position
|
||||||
|
positionCallback?.invoke(columns, position)
|
||||||
|
},
|
||||||
|
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
|
||||||
|
viewOptions = viewOptions,
|
||||||
|
onChangeBackdrop = viewModel::updateBackdrop,
|
||||||
|
playEnabled = playEnabled,
|
||||||
|
onClickPlay = gridActions.onClickPlayRemoteButton!!,
|
||||||
|
onClickPlayAll = gridActions.onClickPlayAll!!,
|
||||||
|
onClickShowViewOptions = { showViewOptions = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
backgroundLoading == LoadingState.Loading,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
CircularProgress(
|
||||||
|
Modifier
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.background.copy(alpha = .25f),
|
||||||
|
shape = CircleShape,
|
||||||
|
).size(64.dp)
|
||||||
|
.padding(4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
overviewDialog?.let { info ->
|
||||||
|
ItemDetailsDialog(
|
||||||
|
info = info,
|
||||||
|
showFilePath =
|
||||||
|
viewModel.serverRepository.currentUserDto.value
|
||||||
|
?.policy
|
||||||
|
?.isAdministrator == true,
|
||||||
|
onDismissRequest = { overviewDialog = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showContextMenu?.let { contextMenu ->
|
||||||
|
ContextMenuDialog(
|
||||||
|
onDismissRequest = { showContextMenu = null },
|
||||||
|
getMediaSource = null,
|
||||||
|
contextMenu = contextMenu,
|
||||||
|
preferredSubtitleLanguage = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showPlaylistDialog.compose { itemId ->
|
||||||
|
PlaylistDialog(
|
||||||
|
title = stringResource(R.string.add_to_playlist),
|
||||||
|
state = playlistState,
|
||||||
|
onDismissRequest = { showPlaylistDialog.makeAbsent() },
|
||||||
|
onClick = {
|
||||||
|
playlistViewModel.addToPlaylist(it.id, itemId)
|
||||||
|
showPlaylistDialog.makeAbsent()
|
||||||
|
},
|
||||||
|
createEnabled = true,
|
||||||
|
onCreatePlaylist = {
|
||||||
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
|
showPlaylistDialog.makeAbsent()
|
||||||
|
},
|
||||||
|
elevation = 3.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(showViewOptions) {
|
||||||
|
ViewOptionsDialog(
|
||||||
|
viewOptions = viewOptions,
|
||||||
|
defaultViewOptions = defaultViewOptions,
|
||||||
|
onDismissRequest = {
|
||||||
|
showViewOptions = false
|
||||||
|
viewModel.saveViewOptions(viewOptions)
|
||||||
|
},
|
||||||
|
onViewOptionsChange = viewModel::saveViewOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.NonRestartableComposable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.tv.material3.DenseListItem
|
||||||
|
import androidx.tv.material3.ListItem
|
||||||
|
import androidx.tv.material3.ListItemBorder
|
||||||
|
import androidx.tv.material3.ListItemColors
|
||||||
|
import androidx.tv.material3.ListItemDefaults
|
||||||
|
import androidx.tv.material3.ListItemGlow
|
||||||
|
import androidx.tv.material3.ListItemScale
|
||||||
|
import androidx.tv.material3.ListItemShape
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays either a [ListItem] or [DenseListItem] based on the dense parameter
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
fun ListItemWrapper(
|
||||||
|
dense: Boolean,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
headlineContent: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
overlineContent: (@Composable () -> Unit)? = null,
|
||||||
|
supportingContent: (@Composable () -> Unit)? = null,
|
||||||
|
leadingContent: (@Composable BoxScope.() -> Unit)? = null,
|
||||||
|
trailingContent: (@Composable () -> Unit)? = null,
|
||||||
|
tonalElevation: Dp = ListItemDefaults.TonalElevation,
|
||||||
|
shape: ListItemShape = ListItemDefaults.shape(),
|
||||||
|
colors: ListItemColors = ListItemDefaults.colors(),
|
||||||
|
scale: ListItemScale = ListItemDefaults.scale(),
|
||||||
|
border: ListItemBorder = ListItemDefaults.border(),
|
||||||
|
glow: ListItemGlow = ListItemDefaults.glow(),
|
||||||
|
interactionSource: MutableInteractionSource? = null,
|
||||||
|
) = if (dense) {
|
||||||
|
DenseListItem(
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick,
|
||||||
|
headlineContent = headlineContent,
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
overlineContent = overlineContent,
|
||||||
|
supportingContent = supportingContent,
|
||||||
|
leadingContent = leadingContent,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
tonalElevation = tonalElevation,
|
||||||
|
shape = shape,
|
||||||
|
colors = colors,
|
||||||
|
scale = scale,
|
||||||
|
border = border,
|
||||||
|
glow = glow,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ListItem(
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick,
|
||||||
|
headlineContent = headlineContent,
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
overlineContent = overlineContent,
|
||||||
|
supportingContent = supportingContent,
|
||||||
|
leadingContent = leadingContent,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
tonalElevation = tonalElevation,
|
||||||
|
shape = shape,
|
||||||
|
colors = colors,
|
||||||
|
scale = scale,
|
||||||
|
border = border,
|
||||||
|
glow = glow,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.ui.components
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
|
@ -9,6 +10,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -93,3 +95,28 @@ fun rememberLogoUrl(item: BaseItem?): String? {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SimpleTitleOrLogo(
|
||||||
|
item: BaseItem?,
|
||||||
|
showLogo: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val logoImageUrl = rememberLogoUrl(item)
|
||||||
|
var imageError by remember { mutableStateOf(false) }
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
if (showLogo && logoImageUrl != null && !imageError) {
|
||||||
|
AsyncImage(
|
||||||
|
model = logoImageUrl,
|
||||||
|
contentDescription = item?.title,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Title(item?.title, Modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ import org.jellyfin.sdk.model.api.ImageType
|
||||||
/**
|
/**
|
||||||
* A dialog that shows the controls for making changes to a [ViewOptions] object. The caller must manage the state of the [ViewOptions].
|
* A dialog that shows the controls for making changes to a [ViewOptions] object. The caller must manage the state of the [ViewOptions].
|
||||||
*
|
*
|
||||||
* It displays the [AppPreference] objects from [ViewOptions.OPTIONS]
|
* It displays the [AppPreference] objects from [ViewOptions.GRID_OPTIONS]
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ViewOptionsDialog(
|
fun ViewOptionsDialog(
|
||||||
|
|
@ -66,6 +66,14 @@ fun ViewOptionsDialog(
|
||||||
window.setGravity(Gravity.END)
|
window.setGravity(Gravity.END)
|
||||||
window.setDimAmount(0f)
|
window.setDimAmount(0f)
|
||||||
}
|
}
|
||||||
|
val options =
|
||||||
|
remember(viewOptions.type) {
|
||||||
|
when (viewOptions.type) {
|
||||||
|
ViewOptionsType.GRID -> ViewOptions.GRID_OPTIONS
|
||||||
|
ViewOptionsType.LIST -> ViewOptions.LIST_OPTIONS
|
||||||
|
ViewOptionsType.DENSE_LIST -> ViewOptions.LIST_OPTIONS
|
||||||
|
}
|
||||||
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = columnState,
|
state = columnState,
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
|
@ -86,7 +94,7 @@ fun ViewOptionsDialog(
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
items(ViewOptions.OPTIONS) { pref ->
|
items(options, key = { it.title }) { pref ->
|
||||||
pref as AppPreference<ViewOptions, Any>
|
pref as AppPreference<ViewOptions, Any>
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val value = pref.getter.invoke(viewOptions)
|
val value = pref.getter.invoke(viewOptions)
|
||||||
|
|
@ -122,8 +130,10 @@ data class ViewOptions(
|
||||||
val contentScale: PrefContentScale = PrefContentScale.FIT,
|
val contentScale: PrefContentScale = PrefContentScale.FIT,
|
||||||
val aspectRatio: AspectRatio = AspectRatio.TALL,
|
val aspectRatio: AspectRatio = AspectRatio.TALL,
|
||||||
val showDetails: Boolean = false,
|
val showDetails: Boolean = false,
|
||||||
|
val showBackdrop: Boolean = false,
|
||||||
val imageType: ViewOptionImageType = ViewOptionImageType.PRIMARY,
|
val imageType: ViewOptionImageType = ViewOptionImageType.PRIMARY,
|
||||||
val showTitles: Boolean = true,
|
val showTitles: Boolean = true,
|
||||||
|
val type: ViewOptionsType = ViewOptionsType.GRID,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val ViewOptionsColumns =
|
val ViewOptionsColumns =
|
||||||
|
|
@ -176,6 +186,13 @@ data class ViewOptions(
|
||||||
getter = { it.showDetails },
|
getter = { it.showDetails },
|
||||||
setter = { vo, value -> vo.copy(showDetails = value) },
|
setter = { vo, value -> vo.copy(showDetails = value) },
|
||||||
)
|
)
|
||||||
|
val ViewOptionsBackdrop =
|
||||||
|
AppSwitchPreference<ViewOptions>(
|
||||||
|
title = R.string.show_backdrop,
|
||||||
|
defaultValue = false,
|
||||||
|
getter = { it.showBackdrop },
|
||||||
|
setter = { vo, value -> vo.copy(showBackdrop = value) },
|
||||||
|
)
|
||||||
val ViewOptionsShowTitles =
|
val ViewOptionsShowTitles =
|
||||||
AppSwitchPreference<ViewOptions>(
|
AppSwitchPreference<ViewOptions>(
|
||||||
title = R.string.show_titles,
|
title = R.string.show_titles,
|
||||||
|
|
@ -202,22 +219,52 @@ data class ViewOptions(
|
||||||
valueToIndex = { it.ordinal },
|
valueToIndex = { it.ordinal },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val ViewOptionsTypePref =
|
||||||
|
AppChoicePreference<ViewOptions, ViewOptionsType>(
|
||||||
|
title = R.string.layout,
|
||||||
|
defaultValue = ViewOptionsType.GRID,
|
||||||
|
displayValues = R.array.view_options_types,
|
||||||
|
getter = { it.type },
|
||||||
|
setter = { viewOptions, value ->
|
||||||
|
val spacing =
|
||||||
|
when (value) {
|
||||||
|
ViewOptionsType.GRID -> 16
|
||||||
|
ViewOptionsType.LIST -> 4
|
||||||
|
ViewOptionsType.DENSE_LIST -> 2
|
||||||
|
}
|
||||||
|
viewOptions.copy(type = value, spacing = spacing)
|
||||||
|
},
|
||||||
|
indexToValue = { ViewOptionsType.entries[it] },
|
||||||
|
valueToIndex = { it.ordinal },
|
||||||
|
)
|
||||||
|
|
||||||
val ViewOptionsReset =
|
val ViewOptionsReset =
|
||||||
AppClickablePreference<ViewOptions>(
|
AppClickablePreference<ViewOptions>(
|
||||||
title = R.string.reset,
|
title = R.string.reset,
|
||||||
)
|
)
|
||||||
|
|
||||||
val OPTIONS =
|
val GRID_OPTIONS =
|
||||||
listOf(
|
listOf(
|
||||||
|
ViewOptionsTypePref,
|
||||||
ViewOptionsImageType,
|
ViewOptionsImageType,
|
||||||
ViewOptionsAspectRatio,
|
ViewOptionsAspectRatio,
|
||||||
ViewOptionsDetailHeader,
|
ViewOptionsDetailHeader,
|
||||||
|
ViewOptionsBackdrop,
|
||||||
ViewOptionsShowTitles,
|
ViewOptionsShowTitles,
|
||||||
ViewOptionsColumns,
|
ViewOptionsColumns,
|
||||||
ViewOptionsSpacing,
|
ViewOptionsSpacing,
|
||||||
ViewOptionsContentScale,
|
ViewOptionsContentScale,
|
||||||
ViewOptionsReset,
|
ViewOptionsReset,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val LIST_OPTIONS =
|
||||||
|
listOf(
|
||||||
|
ViewOptionsTypePref,
|
||||||
|
ViewOptionsDetailHeader,
|
||||||
|
ViewOptionsBackdrop,
|
||||||
|
ViewOptionsSpacing,
|
||||||
|
ViewOptionsReset,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,3 +296,9 @@ enum class ViewOptionImageType(
|
||||||
THUMB(ImageType.THUMB),
|
THUMB(ImageType.THUMB),
|
||||||
// BANNER(ImageType.BANNER),
|
// BANNER(ImageType.BANNER),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class ViewOptionsType {
|
||||||
|
GRID,
|
||||||
|
LIST,
|
||||||
|
DENSE_LIST,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import com.github.damontecres.wholphin.data.filter.DefaultFilterOptions
|
||||||
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||||
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.CollectionFolderView
|
||||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
|
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
|
||||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsWide
|
import com.github.damontecres.wholphin.ui.components.ViewOptionsWide
|
||||||
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
||||||
|
|
@ -41,7 +41,7 @@ fun CollectionFolderGeneric(
|
||||||
ViewOptionsWide
|
ViewOptionsWide
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = { index, item ->
|
onClickItem = { index, item ->
|
||||||
preferencesViewModel.navigationManager.navigateTo(item.destination(index))
|
preferencesViewModel.navigationManager.navigateTo(item.destination(index))
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||||
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.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
import com.github.damontecres.wholphin.ui.components.CollectionFolderView
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.TabRow
|
import com.github.damontecres.wholphin.ui.components.TabRow
|
||||||
import com.github.damontecres.wholphin.ui.components.ViewOptions
|
import com.github.damontecres.wholphin.ui.components.ViewOptions
|
||||||
|
|
@ -165,7 +165,7 @@ fun CollectionFolderLiveTv(
|
||||||
else -> {
|
else -> {
|
||||||
val folderIndex = selectedTabIndex - 2
|
val folderIndex = selectedTabIndex - 2
|
||||||
if (folderIndex in folders.indices) {
|
if (folderIndex in folders.indices) {
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
itemId = folders[folderIndex].id,
|
itemId = folders[folderIndex].id,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import com.github.damontecres.wholphin.R
|
||||||
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.ui.components.CollectionFolderGrid
|
import com.github.damontecres.wholphin.ui.components.CollectionFolderView
|
||||||
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.RecommendedMovie
|
import com.github.damontecres.wholphin.ui.components.RecommendedMovie
|
||||||
|
|
@ -106,7 +106,7 @@ fun CollectionFolderMovie(
|
||||||
|
|
||||||
// Library
|
// Library
|
||||||
1 -> {
|
1 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = { _, item ->
|
onClickItem = { _, item ->
|
||||||
preferencesViewModel.navigationManager.navigateTo(item.destination())
|
preferencesViewModel.navigationManager.navigateTo(item.destination())
|
||||||
|
|
@ -138,7 +138,7 @@ fun CollectionFolderMovie(
|
||||||
|
|
||||||
// Collections
|
// Collections
|
||||||
2 -> {
|
2 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = { _, item ->
|
onClickItem = { _, item ->
|
||||||
preferencesViewModel.navigationManager.navigateTo(item.destination())
|
preferencesViewModel.navigationManager.navigateTo(item.destination())
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
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.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
import com.github.damontecres.wholphin.ui.components.CollectionFolderView
|
||||||
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.GridClickActions
|
||||||
|
|
@ -204,7 +204,7 @@ fun CollectionFolderMusic(
|
||||||
|
|
||||||
// Albums
|
// Albums
|
||||||
1 -> {
|
1 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
actions = actions,
|
actions = actions,
|
||||||
itemId = destination.itemId,
|
itemId = destination.itemId,
|
||||||
|
|
@ -234,7 +234,7 @@ fun CollectionFolderMusic(
|
||||||
|
|
||||||
// Artists
|
// Artists
|
||||||
2 -> {
|
2 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
actions = actions,
|
actions = actions,
|
||||||
itemId = destination.itemId,
|
itemId = destination.itemId,
|
||||||
|
|
@ -276,7 +276,7 @@ fun CollectionFolderMusic(
|
||||||
|
|
||||||
// Songs
|
// Songs
|
||||||
4 -> {
|
4 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
actions = actions,
|
actions = actions,
|
||||||
itemId = destination.itemId,
|
itemId = destination.itemId,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||||
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.ui.components.CollectionFolderGrid
|
import com.github.damontecres.wholphin.ui.components.CollectionFolderView
|
||||||
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.GridClickActions
|
||||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsWide
|
import com.github.damontecres.wholphin.ui.components.ViewOptionsWide
|
||||||
|
|
@ -50,7 +50,7 @@ fun CollectionFolderPhotoAlbum(
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
var showHeader by remember { mutableStateOf(true) }
|
var showHeader by remember { mutableStateOf(true) }
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
actions =
|
actions =
|
||||||
remember {
|
remember {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||||
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.CollectionFolderView
|
||||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare
|
import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare
|
||||||
import com.github.damontecres.wholphin.ui.data.PlaylistSortOptions
|
import com.github.damontecres.wholphin.ui.data.PlaylistSortOptions
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
|
|
@ -25,7 +25,7 @@ fun CollectionFolderPlaylist(
|
||||||
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
var showHeader by remember { mutableStateOf(true) }
|
var showHeader by remember { mutableStateOf(true) }
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = { _, item -> preferencesViewModel.navigationManager.navigateTo(item.destination()) },
|
onClickItem = { _, item -> preferencesViewModel.navigationManager.navigateTo(item.destination()) },
|
||||||
itemId = itemId,
|
itemId = itemId,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||||
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.CollectionFolderView
|
||||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
|
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
|
||||||
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
|
|
@ -25,7 +25,7 @@ fun CollectionFolderRecordings(
|
||||||
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
var showHeader by remember { mutableStateOf(true) }
|
var showHeader by remember { mutableStateOf(true) }
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = { _, item -> preferencesViewModel.navigationManager.navigateTo(item.destination()) },
|
onClickItem = { _, item -> preferencesViewModel.navigationManager.navigateTo(item.destination()) },
|
||||||
itemId = itemId,
|
itemId = itemId,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ 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.ui.components.CollectionFolderGrid
|
import com.github.damontecres.wholphin.ui.components.CollectionFolderView
|
||||||
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.RecommendedTvShow
|
import com.github.damontecres.wholphin.ui.components.RecommendedTvShow
|
||||||
|
|
@ -113,7 +113,7 @@ fun CollectionFolderTv(
|
||||||
|
|
||||||
// Library
|
// Library
|
||||||
1 -> {
|
1 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
itemId = destination.itemId,
|
itemId = destination.itemId,
|
||||||
initialFilter =
|
initialFilter =
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ 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.data.model.GetItemsFilterOverride
|
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.CollectionFolderView
|
||||||
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.GridClickActions
|
||||||
import com.github.damontecres.wholphin.ui.components.TabRow
|
import com.github.damontecres.wholphin.ui.components.TabRow
|
||||||
|
|
@ -121,7 +121,7 @@ fun FavoritesPage(
|
||||||
when (selectedTabIndex) {
|
when (selectedTabIndex) {
|
||||||
// Movies
|
// Movies
|
||||||
0 -> {
|
0 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
actions = actions,
|
actions = actions,
|
||||||
itemId = "${NavDrawerItem.Favorites.id}_movies",
|
itemId = "${NavDrawerItem.Favorites.id}_movies",
|
||||||
|
|
@ -153,7 +153,7 @@ fun FavoritesPage(
|
||||||
|
|
||||||
// TV
|
// TV
|
||||||
1 -> {
|
1 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
actions = actions,
|
actions = actions,
|
||||||
itemId = "${NavDrawerItem.Favorites.id}_series",
|
itemId = "${NavDrawerItem.Favorites.id}_series",
|
||||||
|
|
@ -185,7 +185,7 @@ fun FavoritesPage(
|
||||||
|
|
||||||
// Episodes
|
// Episodes
|
||||||
2 -> {
|
2 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
actions = actions,
|
actions = actions,
|
||||||
itemId = "${NavDrawerItem.Favorites.id}_episodes",
|
itemId = "${NavDrawerItem.Favorites.id}_episodes",
|
||||||
|
|
@ -218,7 +218,7 @@ fun FavoritesPage(
|
||||||
|
|
||||||
// Videos
|
// Videos
|
||||||
3 -> {
|
3 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
actions = actions,
|
actions = actions,
|
||||||
itemId = "${NavDrawerItem.Favorites.id}_videos",
|
itemId = "${NavDrawerItem.Favorites.id}_videos",
|
||||||
|
|
@ -250,7 +250,7 @@ fun FavoritesPage(
|
||||||
|
|
||||||
// Playlists
|
// Playlists
|
||||||
4 -> {
|
4 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
actions = actions,
|
actions = actions,
|
||||||
itemId = "${NavDrawerItem.Favorites.id}_playlists",
|
itemId = "${NavDrawerItem.Favorites.id}_playlists",
|
||||||
|
|
@ -282,7 +282,7 @@ fun FavoritesPage(
|
||||||
|
|
||||||
// People
|
// People
|
||||||
5 -> {
|
5 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderView(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
actions = actions,
|
actions = actions,
|
||||||
itemId = "${NavDrawerItem.Favorites.id}_people",
|
itemId = "${NavDrawerItem.Favorites.id}_people",
|
||||||
|
|
|
||||||
|
|
@ -703,6 +703,14 @@
|
||||||
<item>@string/image_type_thumb</item>
|
<item>@string/image_type_thumb</item>
|
||||||
<!-- <item>Banner</item>-->
|
<!-- <item>Banner</item>-->
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string name="view_options_type_grid">Grid</string>
|
||||||
|
<string name="view_options_type_list">List</string>
|
||||||
|
<string name="view_options_type_list_compact">Compact List</string>
|
||||||
|
<string-array name="view_options_types">
|
||||||
|
<item>@string/view_options_type_grid</item>
|
||||||
|
<item>@string/view_options_type_list</item>
|
||||||
|
<item>@string/view_options_type_list_compact</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string name="backdrop_style_dynamic">Image with dynamic color</string>
|
<string name="backdrop_style_dynamic">Image with dynamic color</string>
|
||||||
<string name="backdrop_style_image">Image only</string>
|
<string name="backdrop_style_image">Image only</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue