From 23856ffcf87fbcd85661ac71e61da5f8d1fd89c9 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 25 May 2026 20:05:44 -0400 Subject: [PATCH] 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 --- .../ui/components/CollectionFolderGrid.kt | 162 +++------- .../ui/components/CollectionFolderList.kt | 245 ++++++--------- .../ui/components/CollectionFolderView.kt | 281 ++++++++++-------- .../ui/detail/series/SeriesViewModel.kt | 2 +- 4 files changed, 294 insertions(+), 396 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index 00b43336..07a9fba7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -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>, + collectionType: CollectionType?, + focusedItem: BaseItem?, + items: List, sortAndDirection: SortAndDirection, onClickItem: (Int, BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit, - onSortChange: (SortAndDirection) -> Unit, letterPosition: suspend (Char) -> Int, - sortOptions: List, - playEnabled: Boolean, - getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List, 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> = 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(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> -> { - 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 + } + }, + ) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt index f34b2b6d..3c69e5ce 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt @@ -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>, + collectionType: CollectionType?, + focusedItem: BaseItem?, + items: List, sortAndDirection: SortAndDirection, onClickItem: (Int, BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit, - onSortChange: (SortAndDirection) -> Unit, letterPosition: suspend (Char) -> Int, - sortOptions: List, - playEnabled: Boolean, - getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List, 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> = 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> -> { - 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) + } + }, + ) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt index 3c7cbac2..018fc7e4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt @@ -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 = DataLoadingState.Loading, + val item: DataLoadingState = DataLoadingState.Loading, val items: DataLoadingState> = 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 -> { + 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> -> { + 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), + ) + } + } + } + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index 106d13ed..bdce9a20 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -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(