From bbeb42dab934b6ecc8d686d76ab929dd6c5dd57b Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:15:16 -0500 Subject: [PATCH] Keep grid controls accessible if error occurs (#714) ## Description If an error occurs while fetching data for the grid, leave the controls accessible. This is useful in case a particular sort or filter causes an error because the app remembers those values between page loads. Essentially the error message is shown in place of the grid content instead of the entire tab. ### Related issues Fixes #712 --- .../ui/components/CollectionFolderGrid.kt | 357 +++++++++--------- 1 file changed, 184 insertions(+), 173 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 ce8db419..3fa8ffa5 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 @@ -89,10 +89,10 @@ import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.tryRequestFocus 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.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -151,9 +151,8 @@ class CollectionFolderViewModel ): CollectionFolderViewModel } - val loading = MutableLiveData(LoadingState.Loading) + val loading = MutableLiveData>>(DataLoadingState.Loading) val backgroundLoading = MutableLiveData(LoadingState.Loading) - val pager = MutableLiveData>(listOf()) val sortAndDirection = MutableLiveData() val filter = MutableLiveData(GetItemsFilter()) val viewOptions = MutableLiveData() @@ -165,40 +164,40 @@ class CollectionFolderViewModel } init { - viewModelScope.launch( - LoadingExceptionHandler( - loading, - context.getString(R.string.error_loading_collection, itemId), - ) + Dispatchers.IO, - ) { + viewModelScope.launchIO { super.itemId = itemId - itemId.toUUIDOrNull()?.let { - fetchItem(it) + try { + itemId.toUUIDOrNull()?.let { + fetchItem(it) + } + + val libraryDisplayInfo = + serverRepository.currentUser.value?.let { user -> + libraryDisplayInfoDao.getItem(user, itemId) + } + this@CollectionFolderViewModel.viewOptions.setValueOnMain( + 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) + } catch (ex: Exception) { + Timber.e(ex, "Error during init") + loading.setValueOnMain(DataLoadingState.Error(ex)) } - - val libraryDisplayInfo = - serverRepository.currentUser.value?.let { user -> - libraryDisplayInfoDao.getItem(user, itemId) - } - this@CollectionFolderViewModel.viewOptions.setValueOnMain( - 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) } } @@ -269,8 +268,7 @@ class CollectionFolderViewModel viewModelScope.launch(Dispatchers.IO) { withContext(Dispatchers.Main) { if (resetState) { - pager.value = listOf() - loading.value = LoadingState.Loading + loading.value = DataLoadingState.Loading } backgroundLoading.value = LoadingState.Loading this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection @@ -281,8 +279,7 @@ class CollectionFolderViewModel createPager(sortAndDirection, recursive, filter, useSeriesForPrimary).init() if (newPager.isNotEmpty()) newPager.getBlocking(0) withContext(Dispatchers.Main) { - pager.value = newPager - loading.value = LoadingState.Success + loading.value = DataLoadingState.Success(newPager) backgroundLoading.value = LoadingState.Success } } catch (ex: Exception) { @@ -293,8 +290,7 @@ class CollectionFolderViewModel filter, ) withContext(Dispatchers.Main) { - loading.value = LoadingState.Error(ex) - pager.value = listOf() + loading.value = DataLoadingState.Error(ex) } } } @@ -498,7 +494,9 @@ class CollectionFolderViewModel played: Boolean, ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { favoriteWatchManager.setWatched(itemId, played) - (pager.value as? ApiRequestPager<*>)?.refreshItem(position, itemId) + (loading.value as? DataLoadingState.Success)?.let { + (it.data as? ApiRequestPager<*>)?.refreshItem(position, itemId) + } } fun setFavorite( @@ -507,7 +505,9 @@ class CollectionFolderViewModel favorite: Boolean, ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { favoriteWatchManager.setFavorite(itemId, favorite) - (pager.value as? ApiRequestPager<*>)?.refreshItem(position, itemId) + (loading.value as? DataLoadingState.Success)?.let { + (it.data as? ApiRequestPager<*>)?.refreshItem(position, itemId) + } } fun updateBackdrop(item: BaseItem) { @@ -598,7 +598,6 @@ fun CollectionFolderGrid( val loading by viewModel.loading.observeAsState(LoadingState.Loading) val backgroundLoading by viewModel.backgroundLoading.observeAsState(LoadingState.Loading) val item by viewModel.item.observeAsState() - val pager by viewModel.pager.observeAsState() val viewOptions by viewModel.viewOptions.observeAsState(defaultViewOptions) var moreDialog by remember { mutableStateOf>(Optional.absent()) } @@ -606,95 +605,91 @@ fun CollectionFolderGrid( val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) when (val state = loading) { - is LoadingState.Error -> { - ErrorMessage(state) - } - - LoadingState.Loading, - LoadingState.Pending, + DataLoadingState.Loading, + DataLoadingState.Pending, -> { LoadingPage() } - LoadingState.Success -> { - pager?.let { pager -> - val title = - initialFilter.nameOverride - ?: item?.name - ?: item?.data?.collectionType?.name - ?: stringResource(R.string.collection) - Box(modifier = modifier) { - CollectionFolderGridContent( - preferences = preferences, - initialPosition = viewModel.position, - item = item, - title = title, - pager = pager, - sortAndDirection = sortAndDirection!!, - modifier = Modifier.fillMaxSize(), - focusRequesterOnEmpty = focusRequesterOnEmpty, - onClickItem = onClickItem, - onLongClickItem = { position, item -> - moreDialog.makePresent(PositionItem(position, item)) - }, - 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, - defaultViewOptions = defaultViewOptions, - onSaveViewOptions = { viewModel.saveViewOptions(it) }, - onChangeBackdrop = viewModel::updateBackdrop, - playEnabled = playEnabled, - onClickPlay = { _, item -> - viewModel.navigationManager.navigateTo(Destination.Playback(item)) - }, - onClickPlayAll = { shuffle -> - itemId.toUUIDOrNull()?.let { - viewModel.navigationManager.navigateTo( - Destination.PlaybackList( - itemId = it, - startIndex = 0, - shuffle = shuffle, - recursive = recursive, - sortAndDirection = sortAndDirection, - filter = filter, - ), - ) - } - }, - ) + is DataLoadingState.Error, + is DataLoadingState.Success<*>, + -> { + val title = + initialFilter.nameOverride + ?: item?.name + ?: item?.data?.collectionType?.name + ?: stringResource(R.string.collection) + Box(modifier = modifier) { + CollectionFolderGridContent( + preferences = preferences, + initialPosition = viewModel.position, + item = item, + title = title, + loadingState = state as DataLoadingState>, + sortAndDirection = sortAndDirection!!, + modifier = Modifier.fillMaxSize(), + focusRequesterOnEmpty = focusRequesterOnEmpty, + onClickItem = onClickItem, + onLongClickItem = { position, item -> + moreDialog.makePresent(PositionItem(position, item)) + }, + 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, + defaultViewOptions = defaultViewOptions, + onSaveViewOptions = { viewModel.saveViewOptions(it) }, + onChangeBackdrop = viewModel::updateBackdrop, + playEnabled = playEnabled, + onClickPlay = { _, item -> + viewModel.navigationManager.navigateTo(Destination.Playback(item)) + }, + onClickPlayAll = { shuffle -> + itemId.toUUIDOrNull()?.let { + viewModel.navigationManager.navigateTo( + Destination.PlaybackList( + itemId = it, + startIndex = 0, + shuffle = shuffle, + recursive = recursive, + sortAndDirection = sortAndDirection, + filter = filter, + ), + ) + } + }, + ) - 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), - ) - } + 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), + ) } } } @@ -755,7 +750,7 @@ fun CollectionFolderGridContent( preferences: UserPreferences, item: BaseItem?, title: String, - pager: List, + loadingState: DataLoadingState>, sortAndDirection: SortAndDirection, onClickItem: (Int, BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit, @@ -781,13 +776,14 @@ fun CollectionFolderGridContent( ) { val context = LocalContext.current + val pager = (loadingState as? DataLoadingState.Success)?.data var showHeader by rememberSaveable { mutableStateOf(true) } var showViewOptions by rememberSaveable { mutableStateOf(false) } var viewOptions by remember { mutableStateOf(viewOptions) } val headerRowFocusRequester = remember { FocusRequester() } val gridFocusRequester = remember { FocusRequester() } - if (pager.isNotEmpty()) { + if (pager?.isNotEmpty() == true) { RequestOrRestoreFocus(gridFocusRequester) } else { LaunchedEffect(Unit) { @@ -797,7 +793,7 @@ fun CollectionFolderGridContent( var backdropImageUrl by remember { mutableStateOf(null) } var position by rememberInt(initialPosition) - val focusedItem = pager.getOrNull(position) + val focusedItem = pager?.getOrNull(position) if (viewOptions.showDetails) { LaunchedEffect(focusedItem) { focusedItem?.let(onChangeBackdrop) @@ -810,7 +806,7 @@ fun CollectionFolderGridContent( modifier = Modifier.fillMaxSize(), ) { AnimatedVisibility( - showHeader, + showHeader || loadingState !is DataLoadingState.Success, enter = slideInVertically() + fadeIn(), exit = slideOutVertically() + fadeOut(), ) { @@ -871,7 +867,7 @@ fun CollectionFolderGridContent( ) } } - if (playEnabled) { + if (playEnabled && pager?.isNotEmpty() == true) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, @@ -903,47 +899,62 @@ fun CollectionFolderGridContent( .padding(16.dp), ) } - CardGrid( - pager = pager, - 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, onClick, onLongClick, mod -> - GridCard( - item = item, - onClick = onClick, - onLongClick = onLongClick, - imageContentScale = viewOptions.contentScale.scale, - imageAspectRatio = viewOptions.aspectRatio.ratio, - imageType = viewOptions.imageType, - showTitle = viewOptions.showTitles, - modifier = mod, + when (val state = loadingState) { + DataLoadingState.Pending, + DataLoadingState.Loading, + -> { + // This shouldn't happen, so just show placeholder + Text("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, onClick, onLongClick, mod -> + GridCard( + item = item, + onClick = onClick, + onLongClick = onLongClick, + imageContentScale = viewOptions.contentScale.scale, + imageAspectRatio = viewOptions.aspectRatio.ratio, + imageType = viewOptions.imageType, + showTitle = viewOptions.showTitles, + modifier = mod, + ) + }, + columns = viewOptions.columns, + spacing = viewOptions.spacing.dp, ) - }, - columns = viewOptions.columns, - spacing = viewOptions.spacing.dp, - ) - AnimatedVisibility(showViewOptions) { - ViewOptionsDialog( - viewOptions = viewOptions, - defaultViewOptions = defaultViewOptions, - onDismissRequest = { - showViewOptions = false - onSaveViewOptions.invoke(viewOptions) - }, - onViewOptionsChange = { viewOptions = it }, - ) + AnimatedVisibility(showViewOptions) { + ViewOptionsDialog( + viewOptions = viewOptions, + defaultViewOptions = defaultViewOptions, + onDismissRequest = { + showViewOptions = false + onSaveViewOptions.invoke(viewOptions) + }, + onViewOptionsChange = { viewOptions = it }, + ) + } + } } } }