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

View file

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

View file

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