mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
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:
parent
88de065fe0
commit
bbeb42dab9
1 changed files with 184 additions and 173 deletions
|
|
@ -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>(LoadingState.Loading)
|
||||
val loading = MutableLiveData<DataLoadingState<List<BaseItem?>>>(DataLoadingState.Loading)
|
||||
val backgroundLoading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||
val pager = MutableLiveData<List<BaseItem?>>(listOf())
|
||||
val sortAndDirection = MutableLiveData<SortAndDirection>()
|
||||
val filter = MutableLiveData<GetItemsFilter>(GetItemsFilter())
|
||||
val viewOptions = MutableLiveData<ViewOptions>()
|
||||
|
|
@ -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<PositionItem>>(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<List<BaseItem?>>,
|
||||
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<BaseItem?>,
|
||||
loadingState: DataLoadingState<List<BaseItem?>>,
|
||||
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<String?>(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<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, 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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue