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
This commit is contained in:
Ray 2026-01-16 17:15:16 -05:00 committed by GitHub
parent 88de065fe0
commit bbeb42dab9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -89,10 +89,10 @@ import com.github.damontecres.wholphin.ui.setValueOnMain
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.tryRequestFocus
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.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.GetPersonsHandler import com.github.damontecres.wholphin.util.GetPersonsHandler
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@ -151,9 +151,8 @@ class CollectionFolderViewModel
): CollectionFolderViewModel ): CollectionFolderViewModel
} }
val loading = MutableLiveData<LoadingState>(LoadingState.Loading) val loading = MutableLiveData<DataLoadingState<List<BaseItem?>>>(DataLoadingState.Loading)
val backgroundLoading = MutableLiveData<LoadingState>(LoadingState.Loading) val backgroundLoading = MutableLiveData<LoadingState>(LoadingState.Loading)
val pager = MutableLiveData<List<BaseItem?>>(listOf())
val sortAndDirection = MutableLiveData<SortAndDirection>() val sortAndDirection = MutableLiveData<SortAndDirection>()
val filter = MutableLiveData<GetItemsFilter>(GetItemsFilter()) val filter = MutableLiveData<GetItemsFilter>(GetItemsFilter())
val viewOptions = MutableLiveData<ViewOptions>() val viewOptions = MutableLiveData<ViewOptions>()
@ -165,13 +164,9 @@ class CollectionFolderViewModel
} }
init { init {
viewModelScope.launch( viewModelScope.launchIO {
LoadingExceptionHandler(
loading,
context.getString(R.string.error_loading_collection, itemId),
) + Dispatchers.IO,
) {
super.itemId = itemId super.itemId = itemId
try {
itemId.toUUIDOrNull()?.let { itemId.toUUIDOrNull()?.let {
fetchItem(it) fetchItem(it)
} }
@ -199,6 +194,10 @@ class CollectionFolderViewModel
} }
loadResults(true, sortAndDirection, recursive, filterToUse, useSeriesForPrimary) loadResults(true, sortAndDirection, recursive, filterToUse, useSeriesForPrimary)
} catch (ex: Exception) {
Timber.e(ex, "Error during init")
loading.setValueOnMain(DataLoadingState.Error(ex))
}
} }
} }
@ -269,8 +268,7 @@ class CollectionFolderViewModel
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (resetState) { if (resetState) {
pager.value = listOf() loading.value = DataLoadingState.Loading
loading.value = LoadingState.Loading
} }
backgroundLoading.value = LoadingState.Loading backgroundLoading.value = LoadingState.Loading
this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection
@ -281,8 +279,7 @@ class CollectionFolderViewModel
createPager(sortAndDirection, recursive, filter, useSeriesForPrimary).init() createPager(sortAndDirection, recursive, filter, useSeriesForPrimary).init()
if (newPager.isNotEmpty()) newPager.getBlocking(0) if (newPager.isNotEmpty()) newPager.getBlocking(0)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
pager.value = newPager loading.value = DataLoadingState.Success(newPager)
loading.value = LoadingState.Success
backgroundLoading.value = LoadingState.Success backgroundLoading.value = LoadingState.Success
} }
} catch (ex: Exception) { } catch (ex: Exception) {
@ -293,8 +290,7 @@ class CollectionFolderViewModel
filter, filter,
) )
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
loading.value = LoadingState.Error(ex) loading.value = DataLoadingState.Error(ex)
pager.value = listOf()
} }
} }
} }
@ -498,7 +494,9 @@ class CollectionFolderViewModel
played: Boolean, played: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setWatched(itemId, played) 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( fun setFavorite(
@ -507,7 +505,9 @@ class CollectionFolderViewModel
favorite: Boolean, favorite: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setFavorite(itemId, favorite) 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) { fun updateBackdrop(item: BaseItem) {
@ -598,7 +598,6 @@ fun CollectionFolderGrid(
val loading by viewModel.loading.observeAsState(LoadingState.Loading) val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val backgroundLoading by viewModel.backgroundLoading.observeAsState(LoadingState.Loading) val backgroundLoading by viewModel.backgroundLoading.observeAsState(LoadingState.Loading)
val item by viewModel.item.observeAsState() val item by viewModel.item.observeAsState()
val pager by viewModel.pager.observeAsState()
val viewOptions by viewModel.viewOptions.observeAsState(defaultViewOptions) val viewOptions by viewModel.viewOptions.observeAsState(defaultViewOptions)
var moreDialog by remember { mutableStateOf<Optional<PositionItem>>(Optional.absent()) } var moreDialog by remember { mutableStateOf<Optional<PositionItem>>(Optional.absent()) }
@ -606,18 +605,15 @@ fun CollectionFolderGrid(
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
when (val state = loading) { when (val state = loading) {
is LoadingState.Error -> { DataLoadingState.Loading,
ErrorMessage(state) DataLoadingState.Pending,
}
LoadingState.Loading,
LoadingState.Pending,
-> { -> {
LoadingPage() LoadingPage()
} }
LoadingState.Success -> { is DataLoadingState.Error,
pager?.let { pager -> is DataLoadingState.Success<*>,
-> {
val title = val title =
initialFilter.nameOverride initialFilter.nameOverride
?: item?.name ?: item?.name
@ -629,7 +625,7 @@ fun CollectionFolderGrid(
initialPosition = viewModel.position, initialPosition = viewModel.position,
item = item, item = item,
title = title, title = title,
pager = pager, loadingState = state as DataLoadingState<List<BaseItem?>>,
sortAndDirection = sortAndDirection!!, sortAndDirection = sortAndDirection!!,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
focusRequesterOnEmpty = focusRequesterOnEmpty, focusRequesterOnEmpty = focusRequesterOnEmpty,
@ -698,7 +694,6 @@ fun CollectionFolderGrid(
} }
} }
} }
}
moreDialog.compose { (position, item) -> moreDialog.compose { (position, item) ->
DialogPopup( DialogPopup(
showDialog = true, showDialog = true,
@ -755,7 +750,7 @@ fun CollectionFolderGridContent(
preferences: UserPreferences, preferences: UserPreferences,
item: BaseItem?, item: BaseItem?,
title: String, title: String,
pager: List<BaseItem?>, loadingState: DataLoadingState<List<BaseItem?>>,
sortAndDirection: SortAndDirection, sortAndDirection: SortAndDirection,
onClickItem: (Int, BaseItem) -> Unit, onClickItem: (Int, BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit,
@ -781,13 +776,14 @@ fun CollectionFolderGridContent(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val pager = (loadingState as? DataLoadingState.Success)?.data
var showHeader by rememberSaveable { mutableStateOf(true) } var showHeader by rememberSaveable { mutableStateOf(true) }
var showViewOptions by rememberSaveable { mutableStateOf(false) } var showViewOptions by rememberSaveable { mutableStateOf(false) }
var viewOptions by remember { mutableStateOf(viewOptions) } var viewOptions by remember { mutableStateOf(viewOptions) }
val headerRowFocusRequester = remember { FocusRequester() } val headerRowFocusRequester = remember { FocusRequester() }
val gridFocusRequester = remember { FocusRequester() } val gridFocusRequester = remember { FocusRequester() }
if (pager.isNotEmpty()) { if (pager?.isNotEmpty() == true) {
RequestOrRestoreFocus(gridFocusRequester) RequestOrRestoreFocus(gridFocusRequester)
} else { } else {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -797,7 +793,7 @@ fun CollectionFolderGridContent(
var backdropImageUrl by remember { mutableStateOf<String?>(null) } var backdropImageUrl by remember { mutableStateOf<String?>(null) }
var position by rememberInt(initialPosition) var position by rememberInt(initialPosition)
val focusedItem = pager.getOrNull(position) val focusedItem = pager?.getOrNull(position)
if (viewOptions.showDetails) { if (viewOptions.showDetails) {
LaunchedEffect(focusedItem) { LaunchedEffect(focusedItem) {
focusedItem?.let(onChangeBackdrop) focusedItem?.let(onChangeBackdrop)
@ -810,7 +806,7 @@ fun CollectionFolderGridContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
AnimatedVisibility( AnimatedVisibility(
showHeader, showHeader || loadingState !is DataLoadingState.Success,
enter = slideInVertically() + fadeIn(), enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut(), exit = slideOutVertically() + fadeOut(),
) { ) {
@ -871,7 +867,7 @@ fun CollectionFolderGridContent(
) )
} }
} }
if (playEnabled) { if (playEnabled && pager?.isNotEmpty() == true) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -903,8 +899,21 @@ fun CollectionFolderGridContent(
.padding(16.dp), .padding(16.dp),
) )
} }
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<List<BaseItem?>> -> {
CardGrid( CardGrid(
pager = pager, pager = state.data,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = onLongClickItem, onLongClickItem = onLongClickItem,
onClickPlay = onClickPlay, onClickPlay = onClickPlay,
@ -948,6 +957,8 @@ fun CollectionFolderGridContent(
} }
} }
} }
}
}
data class PositionItem( data class PositionItem(
val position: Int, val position: Int,