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.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity 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.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp 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.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.AspectRatios 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.cards.GridCard
import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.CardGrid import com.github.damontecres.wholphin.ui.detail.CardGrid
import com.github.damontecres.wholphin.ui.detail.GridItemDetails import com.github.damontecres.wholphin.ui.detail.GridItemDetails
import com.github.damontecres.wholphin.ui.main.HomePageHeader import com.github.damontecres.wholphin.ui.main.HomePageHeader
import com.github.damontecres.wholphin.ui.playback.scale 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.ui.util.ScrollToTopBringIntoViewSpec
import com.github.damontecres.wholphin.util.DataLoadingState
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.ItemSortBy
@ -64,76 +52,25 @@ import kotlin.time.Duration
@Composable @Composable
fun CollectionFolderGrid( fun CollectionFolderGrid(
preferences: UserPreferences, preferences: UserPreferences,
item: BaseItem?, collectionType: CollectionType?,
title: String, focusedItem: BaseItem?,
items: DataLoadingState<List<BaseItem?>>, items: List<BaseItem?>,
sortAndDirection: SortAndDirection, sortAndDirection: SortAndDirection,
onClickItem: (Int, BaseItem) -> Unit, onClickItem: (Int, BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit,
onSortChange: (SortAndDirection) -> Unit,
letterPosition: suspend (Char) -> Int, letterPosition: suspend (Char) -> Int,
sortOptions: List<ItemSortBy>,
playEnabled: Boolean,
getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List<FilterValueOption>,
viewOptions: ViewOptions, viewOptions: ViewOptions,
onClickPlayAll: (shuffle: Boolean) -> Unit,
onClickPlay: (Int, BaseItem) -> Unit, onClickPlay: (Int, BaseItem) -> Unit,
onChangeBackdrop: (BaseItem) -> Unit,
onClickShowViewOptions: () -> Unit,
initialPosition: Int, initialPosition: Int,
gridFocusRequester: FocusRequester,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showTitle: Boolean = true,
positionCallback: ((columns: Int, position: Int) -> Unit)? = null, 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) { Box(modifier = modifier) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(0.dp), verticalArrangement = Arrangement.spacedBy(0.dp),
modifier = Modifier.fillMaxSize(), 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 defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
val density = LocalDensity.current val density = LocalDensity.current
AnimatedVisibility(viewOptions.showDetails) { AnimatedVisibility(viewOptions.showDetails) {
@ -147,62 +84,43 @@ fun CollectionFolderGrid(
.padding(HeaderUtils.padding), .padding(HeaderUtils.padding),
) )
} }
when (val state = items) { CardGrid(
DataLoadingState.Pending, pager = items,
DataLoadingState.Loading, onClickItem = onClickItem,
-> { onLongClickItem = onLongClickItem,
// This shouldn't happen, so just show placeholder onClickPlay = onClickPlay,
Text(stringResource(R.string.loading)) letterPosition = letterPosition,
} gridFocusRequester = gridFocusRequester,
showJumpButtons = false, // TODO add preference
is DataLoadingState.Error -> { showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME,
ErrorMessage(state.message, state.exception) modifier = Modifier.fillMaxSize(),
} initialPosition = initialPosition,
positionCallback = positionCallback,
is DataLoadingState.Success<List<BaseItem?>> -> { cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
CardGrid( GridCard(
pager = state.data, item = item,
onClickItem = onClickItem, onClick = onClick,
onLongClickItem = onLongClickItem, onLongClick = onLongClick,
onClickPlay = onClickPlay, imageContentScale = viewOptions.contentScale.scale,
letterPosition = letterPosition, imageAspectRatio = viewOptions.aspectRatio.ratio,
gridFocusRequester = gridFocusRequester, imageType = viewOptions.imageType,
showJumpButtons = false, // TODO add preference showTitle = viewOptions.showTitles,
showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME, fillWidth = widthPx,
modifier = Modifier.fillMaxSize(), modifier = mod,
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
}
},
) )
} },
} 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.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -51,10 +50,7 @@ import coil3.compose.useExistingImageAsPlaceholder
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.crossfade import coil3.request.crossfade
import com.github.damontecres.wholphin.R 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.BaseItem
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.LocalImageUrlService import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.cards.FavoriteIndicator 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.rememberInt
import com.github.damontecres.wholphin.ui.roundMinutes import com.github.damontecres.wholphin.ui.roundMinutes
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.DataLoadingState
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
@ -79,60 +74,30 @@ import org.jellyfin.sdk.model.extensions.ticks
@Composable @Composable
fun CollectionFolderList( fun CollectionFolderList(
preferences: UserPreferences, preferences: UserPreferences,
item: BaseItem?, collectionType: CollectionType?,
title: String, focusedItem: BaseItem?,
items: DataLoadingState<List<BaseItem?>>, items: List<BaseItem?>,
sortAndDirection: SortAndDirection, sortAndDirection: SortAndDirection,
onClickItem: (Int, BaseItem) -> Unit, onClickItem: (Int, BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit,
onSortChange: (SortAndDirection) -> Unit,
letterPosition: suspend (Char) -> Int, letterPosition: suspend (Char) -> Int,
sortOptions: List<ItemSortBy>,
playEnabled: Boolean,
getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List<FilterValueOption>,
viewOptions: ViewOptions, viewOptions: ViewOptions,
onClickPlayAll: (shuffle: Boolean) -> Unit,
onClickPlay: (Int, BaseItem) -> Unit, onClickPlay: (Int, BaseItem) -> Unit,
onChangeBackdrop: (BaseItem) -> Unit,
onClickShowViewOptions: () -> Unit,
initialPosition: Int, initialPosition: Int,
gridFocusRequester: FocusRequester,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showTitle: Boolean = true,
positionCallback: ((columns: Int, position: Int) -> Unit)? = null, 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 context = LocalContext.current
val scope = rememberCoroutineScope() 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() val listState = rememberLazyListState()
var contentHashFocus by remember { mutableStateOf(false) } var contentHashFocus by remember { mutableStateOf(false) }
var position by rememberInt(initialPosition) var position by rememberInt(initialPosition)
val positionFocusRequester = remember { FocusRequester() } val positionFocusRequester = remember { FocusRequester() }
val focusedItem = pager?.getOrNull(position) val showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME
if (viewOptions.showBackdrop) {
LaunchedEffect(focusedItem) { var showHeader by remember { mutableStateOf(true) }
focusedItem?.let(onChangeBackdrop)
}
}
fun jumpTo(newPosition: Int) { fun jumpTo(newPosition: Int) {
scope.launch { scope.launch {
@ -147,125 +112,93 @@ fun CollectionFolderList(
} }
Column(modifier = modifier) { Column(modifier = modifier) {
CollectionFolderHeader( val topPadding by animateDpAsState(
showHeader = showHeader || items !is DataLoadingState.Success, targetValue =
showTitle = showTitle, when {
playEnabled = playEnabled && pager?.isNotEmpty() == true, showHeader -> 8.dp
title = title,
sortAndDirection = sortAndDirection, // showClock-> TODO()
onSortChange = onSortChange, else -> 48.dp
sortOptions = sortOptions, },
getPossibleFilterValues = getPossibleFilterValues,
onClickShowViewOptions = onClickShowViewOptions,
onClickPlayAll = onClickPlayAll,
currentFilter = currentFilter,
filterOptions = filterOptions,
onFilterChange = onFilterChange,
modifier = Modifier.focusRequester(headerRowFocusRequester),
) )
when (val state = items) { Row(
DataLoadingState.Pending, horizontalArrangement = Arrangement.spacedBy(8.dp),
DataLoadingState.Loading, modifier =
-> { Modifier
// This shouldn't happen, so just show placeholder .padding(top = topPadding, bottom = 16.dp)
Text(stringResource(R.string.loading)) .onFocusChanged {
} contentHashFocus = it.hasFocus
},
is DataLoadingState.Error -> { ) {
ErrorMessage(state.message, state.exception) AnimatedVisibility(
} visible = viewOptions.showDetails,
enter = fadeIn() + expandHorizontally(expandFrom = Alignment.Start),
is DataLoadingState.Success<List<BaseItem?>> -> { exit = fadeOut() + shrinkHorizontally(shrinkTowards = Alignment.Start),
val pager = state.data ) {
val topPadding by animateDpAsState( CollectionFolderListDetails(
targetValue = item = focusedItem,
when { showLogo = true,
showHeader -> 8.dp
// showClock-> TODO()
else -> 48.dp
},
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier =
Modifier Modifier
.padding(top = topPadding, bottom = 16.dp) .fillMaxWidth(.33f)
.onFocusChanged { .fillMaxHeight()
contentHashFocus = it.hasFocus .background(
}, color = Color.Transparent, // MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
) { shape = RoundedCornerShape(16.dp),
AnimatedVisibility( ).padding(8.dp),
visible = viewOptions.showDetails, )
enter = fadeIn() + expandHorizontally(expandFrom = Alignment.Start), }
exit = fadeOut() + shrinkHorizontally(shrinkTowards = Alignment.Start), CollectionFolderContent(
) { items = items,
CollectionFolderListDetails( viewOptions = viewOptions,
item = focusedItem, collectionType = collectionType,
showLogo = true, listState = listState,
modifier = position = position,
Modifier positionFocusRequester = positionFocusRequester,
.fillMaxWidth(.33f) onPositionChange = {
.fillMaxHeight() showHeader = it < 1
.background( positionCallback?.invoke(1, it)
color = Color.Transparent, // MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp), position = it
shape = RoundedCornerShape(16.dp), },
).padding(8.dp), onClickItem = onClickItem,
) onLongClickItem = onLongClickItem,
} modifier =
CollectionFolderContent( Modifier
items = state.data, .weight(1f)
viewOptions = viewOptions, .focusRequester(gridFocusRequester),
collectionType = item?.data?.collectionType, )
listState = listState, val letters = stringResource(R.string.jump_letters)
position = position, // Letters
positionFocusRequester = positionFocusRequester, val currentLetter =
onPositionChange = { remember(focusedItem) {
showHeader = it < 1 focusedItem
positionCallback?.invoke(1, it) ?.sortName
position = it ?.firstOrNull()
}, ?.uppercaseChar()
onClickItem = onClickItem, ?.let {
onLongClickItem = onLongClickItem, when (it) {
modifier = in '0'..'9' -> '#'
Modifier in 'A'..'Z' -> it
.weight(1f) else -> null
.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()) { ?: letters[0]
AlphabetButtons(
letters = letters,
currentLetter = currentLetter,
modifier =
Modifier
.align(Alignment.CenterVertically)
.padding(start = 16.dp),
letterClicked = {
scope.launchIO {
val position = letterPosition.invoke(it)
jumpTo(position)
}
},
)
}
} }
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.animation.AnimatedVisibility
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -18,6 +20,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel 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.launchDefault
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination 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.showToast
import com.github.damontecres.wholphin.ui.toServerString 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.ui.util.FilterUtils
import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.DataLoadingState
@ -149,10 +154,12 @@ class CollectionFolderViewModel
viewModelScope.launchIO { viewModelScope.launchIO {
try { try {
val item = val item =
api.userLibraryApi itemId.toUUIDOrNull()?.let {
.getItem(itemId.toUUID()) api.userLibraryApi
.content .getItem(it)
.let(::BaseItem) .content
.let(::BaseItem)
}
val libraryDisplayInfo = val libraryDisplayInfo =
serverRepository.currentUser?.let { user -> serverRepository.currentUser?.let { user ->
@ -184,7 +191,11 @@ class CollectionFolderViewModel
// onResumePage() // onResumePage()
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.e(ex, "Error during init") Timber.e(ex, "Error during init")
_state.update { it.copy(item = DataLoadingState.Error(ex)) } _state.update {
it.copy(
item = DataLoadingState.Error(ex),
)
}
} }
} }
mediaManagementService.deletedItemFlow mediaManagementService.deletedItemFlow
@ -313,7 +324,12 @@ class CollectionFolderViewModel
sortAndDirection, sortAndDirection,
filter, 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( data class CollectionFolderState(
val item: DataLoadingState<BaseItem> = DataLoadingState.Loading, val item: DataLoadingState<BaseItem?> = DataLoadingState.Loading,
val items: DataLoadingState<List<BaseItem?>> = DataLoadingState.Loading, val items: DataLoadingState<List<BaseItem?>> = DataLoadingState.Loading,
val backgroundLoading: LoadingState = LoadingState.Loading, val backgroundLoading: LoadingState = LoadingState.Loading,
val sortAndDirection: SortAndDirection = SortAndDirection.DEFAULT, 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) { is DataLoadingState.Error -> {
DataLoadingState.Loading, ErrorMessage(st, Modifier.fillMaxSize())
DataLoadingState.Pending, }
-> {
LoadingPage(modifier)
}
is DataLoadingState.Error, is DataLoadingState.Success<BaseItem?> -> {
is DataLoadingState.Success<*>, val item = st.successValue
-> { val title =
val item = st.successValue initialFilter.nameOverride
val title = ?: item?.name
initialFilter.nameOverride ?: item?.data?.collectionType?.name
?: item?.name ?: stringResource(R.string.collection)
?: item?.data?.collectionType?.name Column(modifier = Modifier.fillMaxSize()) {
?: stringResource(R.string.collection) val headerRowFocusRequester = remember { FocusRequester() }
Box(modifier = modifier) { LifecycleResumeEffect(itemId) {
LifecycleResumeEffect(itemId) { viewModel.onResumePage()
viewModel.onResumePage()
onPauseOrDispose { onPauseOrDispose {
viewModel.release() viewModel.release()
}
} }
} var showHeader by rememberSaveable { mutableStateOf(true) }
if (state.viewOptions.type == ViewOptionsType.GRID) { val gridFocusRequester = remember { FocusRequester() }
CollectionFolderGrid( val pager = remember(state.items) { state.items.successValue }
preferences = preferences, var position by rememberInt(viewModel.position)
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 },
)
}
AnimatedVisibility( val focusedItem = pager?.getOrNull(position)
state.backgroundLoading == LoadingState.Loading, if (state.viewOptions.showBackdrop) {
modifier = LaunchedEffect(focusedItem) {
Modifier focusedItem?.let(viewModel::updateBackdrop)
.align(Alignment.Center) }
.padding(16.dp), }
) { CollectionFolderHeader(
CircularProgress( showHeader = showHeader || state.items !is DataLoadingState.Success,
Modifier showTitle = showTitle,
.background( playEnabled = playEnabled && state.items.successValue?.isNotEmpty() == true,
MaterialTheme.colorScheme.background.copy(alpha = .25f), title = title,
shape = CircleShape, sortAndDirection = state.sortAndDirection,
).size(64.dp) onSortChange = {
.padding(4.dp), 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() .firstOrNull()
if (nextUp != null) { if (nextUp != null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
navigateTo(Destination.Playback(nextUp.id, 0L)) navigateTo(Destination.Playback(BaseItem(nextUp)))
} }
} else { } else {
showToast( showToast(