Fix some crashes due to bad UI state (#617)

## Description
Fixes two possible crashes:
- When saving view options for a favorite page tab
- When there are no results on a grid page/tab
This commit is contained in:
Ray 2026-01-02 00:24:17 -05:00 committed by GitHub
parent 0958578082
commit 83a543ad7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 203 additions and 186 deletions

View file

@ -169,6 +169,7 @@ class CollectionFolderViewModel
context.getString(R.string.error_loading_collection, itemId),
) + Dispatchers.IO,
) {
super.itemId = itemId
itemId.toUUIDOrNull()?.let {
fetchItem(it)
}
@ -206,7 +207,7 @@ class CollectionFolderViewModel
) {
if (collectionFilter.useSavedLibraryDisplayInfo) {
serverRepository.currentUser.value?.let { user ->
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launchIO {
val libraryDisplayInfo =
LibraryDisplayInfo(
userId = user.rowId,

View file

@ -196,210 +196,224 @@ fun <T : CardGridItem> CardGrid(
}
}
var longPressing by remember { mutableStateOf(false) }
Row(
// horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier =
modifier
.fillMaxSize()
.onKeyEvent {
if (DEBUG) Timber.d("onKeyEvent: ${it.nativeKeyEvent}")
if (useBackToJump && it.key == Key.Back && it.nativeKeyEvent.isLongPress) {
longPressing = true
val newPosition = previouslyFocusedIndex
if (DEBUG) Timber.d("Back long pressed: newPosition=$newPosition")
if (newPosition > 0) {
focusOn(newPosition)
scope.launch(ExceptionHandler()) {
gridState.scrollToItem(newPosition, -columns)
firstFocus.tryRequestFocus()
}
}
return@onKeyEvent true
} else if (it.type == KeyEventType.KeyUp) {
if (longPressing && it.key == Key.Back) {
longPressing = false
return@onKeyEvent true
}
longPressing = false
}
if (it.type != KeyEventType.KeyUp) {
return@onKeyEvent false
} else if (useBackToJump && it.key == Key.Back && focusedIndex > 0) {
jumpToTop()
return@onKeyEvent true
} else if (isPlayKeyUp(it)) {
val item = pager.getOrNull(focusedIndex)
if (item?.playable == true) {
Timber.v("Clicked play on ${item.id}")
onClickPlay.invoke(focusedIndex, item)
}
return@onKeyEvent true
} else if (useJumpRemoteButtons && isForwardButton(it)) {
jump(jump1)
return@onKeyEvent true
} else if (useJumpRemoteButtons && isBackwardButton(it)) {
jump(-jump1)
return@onKeyEvent true
} else {
return@onKeyEvent false
}
},
) {
if (showJumpButtons && pager.isNotEmpty()) {
JumpButtons(
jump1 = jump1,
jump2 = jump2,
jumpClick = { jump(it) },
modifier = Modifier.align(Alignment.CenterVertically),
if (pager.isEmpty()) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier.fillMaxSize(),
) {
Text(
text = stringResource(R.string.no_results),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
)
}
Box(
modifier = Modifier.weight(1f),
) {
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
horizontalArrangement = Arrangement.spacedBy(spacing),
verticalArrangement = Arrangement.spacedBy(spacing),
state = gridState,
contentPadding = PaddingValues(16.dp),
modifier =
Modifier
.fillMaxSize()
.focusGroup()
.focusRestorer(firstFocus)
.focusProperties {
onExit = {
// Leaving the grid, so "forget" the position
// focusedIndex = -1
}
onEnter = {
if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) {
focusedIndex = startPosition
} else {
var longPressing by remember { mutableStateOf(false) }
Row(
// horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier =
modifier
.fillMaxSize()
.onKeyEvent {
if (DEBUG) Timber.d("onKeyEvent: ${it.nativeKeyEvent}")
if (useBackToJump && it.key == Key.Back && it.nativeKeyEvent.isLongPress) {
longPressing = true
val newPosition = previouslyFocusedIndex
if (DEBUG) Timber.d("Back long pressed: newPosition=$newPosition")
if (newPosition > 0) {
focusOn(newPosition)
scope.launch(ExceptionHandler()) {
gridState.scrollToItem(newPosition, -columns)
firstFocus.tryRequestFocus()
}
}
},
) {
items(pager.size) { index ->
val mod =
if ((index == focusedIndex) or (focusedIndex < 0 && index == 0)) {
if (DEBUG) Timber.d("Adding firstFocus to focusedIndex $index")
Modifier
.focusRequester(firstFocus)
.focusRequester(gridFocusRequester)
.focusRequester(alphabetFocusRequester)
} else {
Modifier
return@onKeyEvent true
} else if (it.type == KeyEventType.KeyUp) {
if (longPressing && it.key == Key.Back) {
longPressing = false
return@onKeyEvent true
}
longPressing = false
}
val item = pager[index]
cardContent(
item,
{
if (item != null) {
focusedIndex = index
onClickItem.invoke(index, item)
if (it.type != KeyEventType.KeyUp) {
return@onKeyEvent false
} else if (useBackToJump && it.key == Key.Back && focusedIndex > 0) {
jumpToTop()
return@onKeyEvent true
} else if (isPlayKeyUp(it)) {
val item = pager.getOrNull(focusedIndex)
if (item?.playable == true) {
Timber.v("Clicked play on ${item.id}")
onClickPlay.invoke(focusedIndex, item)
}
},
{ if (item != null) onLongClickItem.invoke(index, item) },
mod
.ifElse(index == 0, Modifier.focusRequester(zeroFocus))
.onFocusChanged { focusState ->
if (DEBUG) {
Timber.v(
"$index isFocused=${focusState.isFocused}",
)
return@onKeyEvent true
} else if (useJumpRemoteButtons && isForwardButton(it)) {
jump(jump1)
return@onKeyEvent true
} else if (useJumpRemoteButtons && isBackwardButton(it)) {
jump(-jump1)
return@onKeyEvent true
} else {
return@onKeyEvent false
}
},
) {
if (showJumpButtons && pager.isNotEmpty()) {
JumpButtons(
jump1 = jump1,
jump2 = jump2,
jumpClick = { jump(it) },
modifier = Modifier.align(Alignment.CenterVertically),
)
}
Box(
modifier = Modifier.weight(1f),
) {
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
horizontalArrangement = Arrangement.spacedBy(spacing),
verticalArrangement = Arrangement.spacedBy(spacing),
state = gridState,
contentPadding = PaddingValues(16.dp),
modifier =
Modifier
.fillMaxSize()
.focusGroup()
.focusRestorer(firstFocus)
.focusProperties {
onExit = {
// Leaving the grid, so "forget" the position
// focusedIndex = -1
}
if (focusState.isFocused) {
// Focused, so set that up
focusOn(index)
positionCallback?.invoke(columns, index)
} else if (focusedIndex == index) {
onEnter = {
if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) {
focusedIndex = startPosition
}
}
},
) {
items(pager.size) { index ->
val mod =
if ((index == focusedIndex) or (focusedIndex < 0 && index == 0)) {
if (DEBUG) Timber.d("Adding firstFocus to focusedIndex $index")
Modifier
.focusRequester(firstFocus)
.focusRequester(gridFocusRequester)
.focusRequester(alphabetFocusRequester)
} else {
Modifier
}
val item = pager[index]
cardContent(
item,
{
if (item != null) {
focusedIndex = index
onClickItem.invoke(index, item)
}
},
{ if (item != null) onLongClickItem.invoke(index, item) },
mod
.ifElse(index == 0, Modifier.focusRequester(zeroFocus))
.onFocusChanged { focusState ->
if (DEBUG) {
Timber.v(
"$index isFocused=${focusState.isFocused}",
)
}
if (focusState.isFocused) {
// Focused, so set that up
focusOn(index)
positionCallback?.invoke(columns, index)
} else if (focusedIndex == index) {
// savedFocusedIndex = index
// // Was focused on this, so mark unfocused
// focusedIndex = -1
}
},
)
}
},
)
}
}
}
if (pager.isEmpty()) {
if (pager.isEmpty()) {
// focusedIndex = -1
Box(modifier = Modifier.fillMaxSize()) {
Text(
text = stringResource(R.string.no_results),
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.align(Alignment.Center),
)
Box(modifier = Modifier.fillMaxSize()) {
Text(
text = stringResource(R.string.no_results),
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.align(Alignment.Center),
)
}
}
}
if (showFooter) {
// Footer
Box(
modifier =
Modifier
.align(Alignment.BottomCenter)
.background(AppColors.TransparentBlack50),
) {
val index = (focusedIndex + 1).takeIf { it > 0 } ?: "?"
if (showFooter) {
// Footer
Box(
modifier =
Modifier
.align(Alignment.BottomCenter)
.background(AppColors.TransparentBlack50),
) {
val index = (focusedIndex + 1).takeIf { it > 0 } ?: "?"
// if (focusedIndex >= 0) {
// focusedIndex + 1
// } else {
// max(savedFocusedIndex, focusedIndexOnExit) + 1
// }
Text(
modifier = Modifier.padding(4.dp),
color = MaterialTheme.colorScheme.onBackground,
text = "$index / ${pager.size}",
)
Text(
modifier = Modifier.padding(4.dp),
color = MaterialTheme.colorScheme.onBackground,
text = "$index / ${pager.size}",
)
}
}
}
}
val context = LocalContext.current
val letters = context.getString(R.string.jump_letters)
// Letters
val currentLetter =
remember(focusedIndex) {
pager
.getOrNull(focusedIndex)
?.sortName
?.first()
?.uppercaseChar()
?.let {
if (it >= '0' && it <= '9') {
'#'
} else if (it >= 'A' && it <= 'Z') {
it
} else {
null
}
}
?: letters[0]
}
if (showLetterButtons && pager.isNotEmpty()) {
AlphabetButtons(
letters = letters,
currentLetter = currentLetter,
modifier =
Modifier
.align(Alignment.CenterVertically)
.padding(end = 16.dp),
// Add end padding to push away from edge
letterClicked = { letter ->
scope.launch(ExceptionHandler()) {
val jumpPosition =
withContext(Dispatchers.IO) {
letterPosition.invoke(letter)
val context = LocalContext.current
val letters = context.getString(R.string.jump_letters)
// Letters
val currentLetter =
remember(focusedIndex) {
pager
.getOrNull(focusedIndex)
?.sortName
?.first()
?.uppercaseChar()
?.let {
if (it >= '0' && it <= '9') {
'#'
} else if (it >= 'A' && it <= 'Z') {
it
} else {
null
}
Timber.d("Alphabet jump to $jumpPosition")
if (jumpPosition >= 0) {
pager.getOrNull(jumpPosition)
gridState.scrollToItem(jumpPosition)
focusOn(jumpPosition)
alphabetFocus = true
}
}
},
)
?: letters[0]
}
if (showLetterButtons && pager.isNotEmpty()) {
AlphabetButtons(
letters = letters,
currentLetter = currentLetter,
modifier =
Modifier
.align(Alignment.CenterVertically)
.padding(end = 16.dp),
// Add end padding to push away from edge
letterClicked = { letter ->
scope.launch(ExceptionHandler()) {
val jumpPosition =
withContext(Dispatchers.IO) {
letterPosition.invoke(letter)
}
Timber.d("Alphabet jump to $jumpPosition")
if (jumpPosition >= 0) {
pager.getOrNull(jumpPosition)
gridState.scrollToItem(jumpPosition)
focusOn(jumpPosition)
alphabetFocus = true
}
}
},
)
}
}
}
}

View file

@ -27,7 +27,7 @@ abstract class ItemViewModel(
) : ViewModel() {
val item = MutableLiveData<BaseItem?>(null)
lateinit var itemId: String
lateinit var itemUuid: UUID
var itemUuid: UUID? = null
suspend fun fetchItem(itemId: UUID): BaseItem =
withContext(Dispatchers.IO) {

View file

@ -155,8 +155,10 @@ class PersonViewModel
fun setFavorite(favorite: Boolean) {
viewModelScope.launchIO {
favoriteWatchManager.setFavorite(itemUuid, favorite)
fetchAndSetItem(itemUuid)
itemUuid?.let {
favoriteWatchManager.setFavorite(it, favorite)
fetchAndSetItem(it)
}
}
}
}