Fixes favorites page not loading (#1462)

## Description
- Fixes a bug introduced in #1453 which broke the favorite page
- Follow up to #1428 to refactor the collection folder code to hoist
state up another level
- Fixes resuming playback from series details play button

### Related issues
Fixes #1313
Fixes #1461
Follow up to #1453 & #1428 

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-05-25 20:05:44 -04:00 committed by GitHub
parent 510160aceb
commit 23856ffcf8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 294 additions and 396 deletions

View file

@ -16,21 +16,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.getValue
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.focus.focusRequester
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
@ -44,17 +36,13 @@ 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.AspectRatios
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
import com.github.damontecres.wholphin.ui.cards.GridCard
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.CardGrid
import com.github.damontecres.wholphin.ui.detail.GridItemDetails
import com.github.damontecres.wholphin.ui.main.HomePageHeader
import com.github.damontecres.wholphin.ui.playback.scale
import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec
import com.github.damontecres.wholphin.util.DataLoadingState
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.ItemSortBy
@ -64,76 +52,25 @@ import kotlin.time.Duration
@Composable
fun CollectionFolderGrid(
preferences: UserPreferences,
item: BaseItem?,
title: String,
items: DataLoadingState<List<BaseItem?>>,
collectionType: CollectionType?,
focusedItem: BaseItem?,
items: 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,
gridFocusRequester: FocusRequester,
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 pager = (items as? DataLoadingState.Success)?.data
var showHeader by rememberSaveable { mutableStateOf(true) }
val headerRowFocusRequester = remember { FocusRequester() }
val gridFocusRequester = remember { FocusRequester() }
if (pager?.isNotEmpty() == true) {
RequestOrRestoreFocus(gridFocusRequester)
} else {
LaunchedEffect(Unit) {
(focusRequesterOnEmpty ?: headerRowFocusRequester).tryRequestFocus()
}
}
var backdropImageUrl by remember { mutableStateOf<String?>(null) }
var position by rememberInt(initialPosition)
val focusedItem = pager?.getOrNull(position)
if (viewOptions.showBackdrop) {
LaunchedEffect(focusedItem) {
focusedItem?.let(onChangeBackdrop)
}
}
Box(modifier = modifier) {
Column(
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier = Modifier.fillMaxSize(),
) {
CollectionFolderHeader(
showHeader = showHeader || items !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),
)
val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
val density = LocalDensity.current
AnimatedVisibility(viewOptions.showDetails) {
@ -147,62 +84,43 @@ fun CollectionFolderGrid(
.padding(HeaderUtils.padding),
)
}
when (val state = items) {
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?>> -> {
CardGrid(
pager = state.data,
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
onClickPlay = onClickPlay,
letterPosition = letterPosition,
gridFocusRequester = gridFocusRequester,
showJumpButtons = false, // TODO add preference
showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME,
modifier = Modifier.fillMaxSize(),
initialPosition = initialPosition,
positionCallback = { columns, newPosition ->
showHeader = newPosition < columns
position = newPosition
positionCallback?.invoke(columns, newPosition)
},
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
GridCard(
item = item,
onClick = onClick,
onLongClick = onLongClick,
imageContentScale = viewOptions.contentScale.scale,
imageAspectRatio = viewOptions.aspectRatio.ratio,
imageType = viewOptions.imageType,
showTitle = viewOptions.showTitles,
fillWidth = widthPx,
modifier = mod,
)
},
columns = viewOptions.columns,
spacing = viewOptions.spacing.dp,
bringIntoViewSpec =
remember(viewOptions) {
val spacingPx = with(density) { viewOptions.spacing.dp.toPx() }
if (viewOptions.showDetails) {
ScrollToTopBringIntoViewSpec(spacingPx)
} else {
defaultBringIntoViewSpec
}
},
CardGrid(
pager = items,
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
onClickPlay = onClickPlay,
letterPosition = letterPosition,
gridFocusRequester = gridFocusRequester,
showJumpButtons = false, // TODO add preference
showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME,
modifier = Modifier.fillMaxSize(),
initialPosition = initialPosition,
positionCallback = positionCallback,
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
GridCard(
item = item,
onClick = onClick,
onLongClick = onLongClick,
imageContentScale = viewOptions.contentScale.scale,
imageAspectRatio = viewOptions.aspectRatio.ratio,
imageType = viewOptions.imageType,
showTitle = viewOptions.showTitles,
fillWidth = widthPx,
modifier = mod,
)
}
}
},
columns = viewOptions.columns,
spacing = viewOptions.spacing.dp,
bringIntoViewSpec =
remember(viewOptions) {
val spacingPx = with(density) { viewOptions.spacing.dp.toPx() }
if (viewOptions.showDetails) {
ScrollToTopBringIntoViewSpec(spacingPx)
} else {
defaultBringIntoViewSpec
}
},
)
}
}
}

View file

@ -28,7 +28,6 @@ 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
@ -51,10 +50,7 @@ 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.cards.FavoriteIndicator
@ -67,7 +63,6 @@ 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
@ -79,60 +74,30 @@ import org.jellyfin.sdk.model.extensions.ticks
@Composable
fun CollectionFolderList(
preferences: UserPreferences,
item: BaseItem?,
title: String,
items: DataLoadingState<List<BaseItem?>>,
collectionType: CollectionType?,
focusedItem: BaseItem?,
items: 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,
gridFocusRequester: FocusRequester,
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 = (items 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) {
LaunchedEffect(viewOptions.type) {
gridFocusRequester.tryRequestFocus()
}
} 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)
}
}
val showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME
var showHeader by remember { mutableStateOf(true) }
fun jumpTo(newPosition: Int) {
scope.launch {
@ -147,125 +112,93 @@ fun CollectionFolderList(
}
Column(modifier = modifier) {
CollectionFolderHeader(
showHeader = showHeader || items !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),
val topPadding by animateDpAsState(
targetValue =
when {
showHeader -> 8.dp
// showClock-> TODO()
else -> 48.dp
},
)
when (val state = items) {
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),
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
.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]
.fillMaxWidth(.33f)
.fillMaxHeight()
.background(
color = Color.Transparent, // MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
shape = RoundedCornerShape(16.dp),
).padding(8.dp),
)
}
CollectionFolderContent(
items = items,
viewOptions = viewOptions,
collectionType = 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
}
}
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)
}
},
)
}
?: letters[0]
}
if (showLetterButtons && items.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)
}
},
)
}
}
}

View file

@ -4,11 +4,13 @@ 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.Column
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.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -18,6 +20,7 @@ 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.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -60,8 +63,10 @@ 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.rememberInt
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.FilterUtils
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.DataLoadingState
@ -149,10 +154,12 @@ class CollectionFolderViewModel
viewModelScope.launchIO {
try {
val item =
api.userLibraryApi
.getItem(itemId.toUUID())
.content
.let(::BaseItem)
itemId.toUUIDOrNull()?.let {
api.userLibraryApi
.getItem(it)
.content
.let(::BaseItem)
}
val libraryDisplayInfo =
serverRepository.currentUser?.let { user ->
@ -184,7 +191,11 @@ class CollectionFolderViewModel
// onResumePage()
} catch (ex: Exception) {
Timber.e(ex, "Error during init")
_state.update { it.copy(item = DataLoadingState.Error(ex)) }
_state.update {
it.copy(
item = DataLoadingState.Error(ex),
)
}
}
}
mediaManagementService.deletedItemFlow
@ -313,7 +324,12 @@ class CollectionFolderViewModel
sortAndDirection,
filter,
)
_state.update { it.copy(items = DataLoadingState.Error(ex)) }
_state.update {
it.copy(
items = DataLoadingState.Error(ex),
backgroundLoading = LoadingState.Pending,
)
}
}
}
@ -516,7 +532,7 @@ class CollectionFolderViewModel
}
data class CollectionFolderState(
val item: DataLoadingState<BaseItem> = DataLoadingState.Loading,
val item: DataLoadingState<BaseItem?> = DataLoadingState.Loading,
val items: DataLoadingState<List<BaseItem?>> = DataLoadingState.Loading,
val backgroundLoading: LoadingState = LoadingState.Loading,
val sortAndDirection: SortAndDirection = SortAndDirection.DEFAULT,
@ -738,122 +754,153 @@ fun CollectionFolderView(
},
)
}
Box(modifier = modifier) {
when (val st = state.item) {
DataLoadingState.Loading,
DataLoadingState.Pending,
-> {
LoadingPage(Modifier.fillMaxSize())
}
when (val st = state.item) {
DataLoadingState.Loading,
DataLoadingState.Pending,
-> {
LoadingPage(modifier)
}
is DataLoadingState.Error -> {
ErrorMessage(st, Modifier.fillMaxSize())
}
is DataLoadingState.Error,
is DataLoadingState.Success<*>,
-> {
val item = st.successValue
val title =
initialFilter.nameOverride
?: item?.name
?: item?.data?.collectionType?.name
?: stringResource(R.string.collection)
Box(modifier = modifier) {
LifecycleResumeEffect(itemId) {
viewModel.onResumePage()
is DataLoadingState.Success<BaseItem?> -> {
val item = st.successValue
val title =
initialFilter.nameOverride
?: item?.name
?: item?.data?.collectionType?.name
?: stringResource(R.string.collection)
Column(modifier = Modifier.fillMaxSize()) {
val headerRowFocusRequester = remember { FocusRequester() }
LifecycleResumeEffect(itemId) {
viewModel.onResumePage()
onPauseOrDispose {
viewModel.release()
onPauseOrDispose {
viewModel.release()
}
}
}
if (state.viewOptions.type == ViewOptionsType.GRID) {
CollectionFolderGrid(
preferences = preferences,
initialPosition = viewModel.position,
item = item,
title = title,
items = state.items,
sortAndDirection = state.sortAndDirection,
modifier = Modifier.fillMaxSize(),
focusRequesterOnEmpty = focusRequesterOnEmpty,
onClickItem = gridActions.onClickItem,
onLongClickItem = gridActions.onLongClickItem!!,
onSortChange = {
viewModel.onSortChange(it, recursive, state.filter)
},
filterOptions = filterOptions,
currentFilter = state.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 = state.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,
items = state.items,
sortAndDirection = state.sortAndDirection,
modifier = Modifier.fillMaxSize(),
focusRequesterOnEmpty = focusRequesterOnEmpty,
onClickItem = gridActions.onClickItem,
onLongClickItem = gridActions.onLongClickItem!!,
onSortChange = {
viewModel.onSortChange(it, recursive, state.filter)
},
filterOptions = filterOptions,
currentFilter = state.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 = state.viewOptions,
onChangeBackdrop = viewModel::updateBackdrop,
playEnabled = playEnabled,
onClickPlay = gridActions.onClickPlayRemoteButton!!,
onClickPlayAll = gridActions.onClickPlayAll!!,
onClickShowViewOptions = { showViewOptions = true },
)
}
var showHeader by rememberSaveable { mutableStateOf(true) }
val gridFocusRequester = remember { FocusRequester() }
val pager = remember(state.items) { state.items.successValue }
var position by rememberInt(viewModel.position)
AnimatedVisibility(
state.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),
val focusedItem = pager?.getOrNull(position)
if (state.viewOptions.showBackdrop) {
LaunchedEffect(focusedItem) {
focusedItem?.let(viewModel::updateBackdrop)
}
}
CollectionFolderHeader(
showHeader = showHeader || state.items !is DataLoadingState.Success,
showTitle = showTitle,
playEnabled = playEnabled && state.items.successValue?.isNotEmpty() == true,
title = title,
sortAndDirection = state.sortAndDirection,
onSortChange = {
viewModel.onSortChange(it, recursive, state.filter)
},
sortOptions = sortOptions,
currentFilter = state.filter,
onFilterChange = {
viewModel.onFilterChange(it, recursive)
},
getPossibleFilterValues = {
viewModel.getFilterOptionValues(it)
},
filterOptions = filterOptions,
onClickPlayAll = gridActions.onClickPlayAll!!,
onClickShowViewOptions = { showViewOptions = true },
modifier = Modifier.focusRequester(headerRowFocusRequester),
)
when (val pager = state.items) {
is DataLoadingState.Error -> {
LaunchedEffect(Unit) {
(focusRequesterOnEmpty ?: headerRowFocusRequester).tryRequestFocus()
}
ErrorMessage(pager, Modifier.fillMaxSize())
}
DataLoadingState.Loading,
DataLoadingState.Pending,
-> {
LoadingPage(Modifier.fillMaxSize())
}
is DataLoadingState.Success<List<BaseItem?>> -> {
LaunchedEffect(Unit) {
if (pager.data.isNotEmpty()) {
gridFocusRequester.tryRequestFocus()
}
}
Box(Modifier.fillMaxSize()) {
if (state.viewOptions.type == ViewOptionsType.GRID) {
CollectionFolderGrid(
preferences = preferences,
collectionType = item?.data?.collectionType,
initialPosition = viewModel.position,
items = pager.data,
sortAndDirection = state.sortAndDirection,
modifier = Modifier.fillMaxSize(),
gridFocusRequester = gridFocusRequester,
onClickItem = gridActions.onClickItem,
onLongClickItem = gridActions.onLongClickItem!!,
positionCallback = { columns, pos ->
viewModel.position = pos
showHeader = pos < columns
position = pos
positionCallback?.invoke(columns, pos)
},
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
viewOptions = state.viewOptions,
onClickPlay = gridActions.onClickPlayRemoteButton!!,
focusedItem = focusedItem,
)
} else {
CollectionFolderList(
preferences = preferences,
collectionType = item?.data?.collectionType,
initialPosition = viewModel.position,
items = pager.data,
sortAndDirection = state.sortAndDirection,
modifier = Modifier.fillMaxSize(),
gridFocusRequester = gridFocusRequester,
onClickItem = gridActions.onClickItem,
onLongClickItem = gridActions.onLongClickItem!!,
positionCallback = { columns, pos ->
viewModel.position = pos
showHeader = pos < columns
position = pos
positionCallback?.invoke(columns, pos)
},
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
viewOptions = state.viewOptions,
onClickPlay = gridActions.onClickPlayRemoteButton!!,
focusedItem = focusedItem,
)
}
androidx.compose.animation.AnimatedVisibility(
state.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),
)
}
}
}
}
}
}
}

View file

@ -492,7 +492,7 @@ class SeriesViewModel
.firstOrNull()
if (nextUp != null) {
withContext(Dispatchers.Main) {
navigateTo(Destination.Playback(nextUp.id, 0L))
navigateTo(Destination.Playback(BaseItem(nextUp)))
}
} else {
showToast(