Potentially fix library IndexOutOfBound crash (#2341)

This commit is contained in:
AntsyLich
2025-08-03 23:08:19 +06:00
committed by GitHub
parent 33e0121a2a
commit c4407eda0e
3 changed files with 40 additions and 21 deletions

View File

@@ -31,7 +31,7 @@ fun LibraryContent(
searchQuery: String?,
selection: Set<Long>,
contentPadding: PaddingValues,
currentPage: () -> Int,
currentPage: Int,
hasActiveFilters: Boolean,
showPageTabs: Boolean,
onChangeCurrentPage: (Int) -> Unit,
@@ -53,8 +53,7 @@ fun LibraryContent(
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
),
) {
val coercedCurrentPage = remember { currentPage().coerceAtMost(categories.lastIndex) }
val pagerState = rememberPagerState(coercedCurrentPage) { categories.size }
val pagerState = rememberPagerState(currentPage) { categories.size }
val scope = rememberCoroutineScope()
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }

View File

@@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.library
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastMap
@@ -39,6 +37,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
import mihon.core.common.utils.mutate
import tachiyomi.core.common.preference.CheckboxState
import tachiyomi.core.common.preference.TriState
@@ -86,10 +85,10 @@ class LibraryScreenModel(
private val trackerManager: TrackerManager = Injekt.get(),
) : StateScreenModel<LibraryScreenModel.State>(State()) {
var activeCategoryIndex: Int by libraryPreferences.lastUsedCategory().asState(screenModelScope)
val activeCategory: Category get() = state.value.displayedCategories[activeCategoryIndex]
init {
mutableState.update { state ->
state.copy(activeCategoryIndex = libraryPreferences.lastUsedCategory().get())
}
screenModelScope.launchIO {
combine(
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
@@ -98,12 +97,14 @@ class LibraryScreenModel(
combine(getTracksPerManga.subscribe(), getTrackingFiltersFlow(), ::Pair),
getLibraryItemPreferencesFlow(),
) { searchQuery, categories, favorites, (tracksMap, trackingFilters), itemPreferences ->
val showSystemCategory = favorites.any { it.libraryManga.categories.contains(0) }
val filteredFavorites = favorites
.applyFilters(tracksMap, trackingFilters, itemPreferences)
.let { if (searchQuery == null) it else it.filter { m -> m.matches(searchQuery) } }
LibraryData(
isInitialized = true,
showSystemCategory = showSystemCategory,
categories = categories,
favorites = filteredFavorites,
tracksMap = tracksMap,
@@ -125,7 +126,7 @@ class LibraryScreenModel(
.distinctUntilChanged()
.map { data ->
data.favorites
.applyGrouping(data.categories)
.applyGrouping(data.categories, data.showSystemCategory)
.applySort(data.favoritesById, data.tracksMap, data.loggedInTrackerIds)
}
.collectLatest {
@@ -256,6 +257,7 @@ class LibraryScreenModel(
private fun List<LibraryItem>.applyGrouping(
categories: List<Category>,
showSystemCategory: Boolean,
): Map<Category, List</* LibraryItem */ Long>> {
val groupCache = mutableMapOf</* Category */ Long, MutableList</* LibraryItem */ Long>>()
forEach { item ->
@@ -263,7 +265,6 @@ class LibraryScreenModel(
groupCache.getOrPut(categoryId) { mutableListOf() }.add(item.id)
}
}
val showSystemCategory = groupCache.containsKey(0L)
return categories.filter { showSystemCategory || !it.isSystemCategory }
.associateWith { groupCache[it.id]?.toList().orEmpty() }
}
@@ -571,7 +572,8 @@ class LibraryScreenModel(
}
fun getRandomLibraryItemForCurrentCategory(): LibraryItem? {
return state.value.getItemsForCategoryId(activeCategory.id).randomOrNull()
val state = state.value
return state.getItemsForCategoryId(state.activeCategory.id).randomOrNull()
}
fun showSettingsDialog() {
@@ -629,7 +631,7 @@ class LibraryScreenModel(
lastSelectionCategory = null
mutableState.update { state ->
val newSelection = state.selection.mutate { list ->
state.getItemsForCategoryId(activeCategory.id).map { it.id }.let(list::addAll)
state.getItemsForCategoryId(state.activeCategory.id).map { it.id }.let(list::addAll)
}
state.copy(selection = newSelection)
}
@@ -639,7 +641,7 @@ class LibraryScreenModel(
lastSelectionCategory = null
mutableState.update { state ->
val newSelection = state.selection.mutate { list ->
val itemIds = state.getItemsForCategoryId(activeCategory.id).fastMap { it.id }
val itemIds = state.getItemsForCategoryId(state.activeCategory.id).fastMap { it.id }
val (toRemove, toAdd) = itemIds.partition { it in list }
list.removeAll(toRemove)
list.addAll(toAdd)
@@ -652,6 +654,15 @@ class LibraryScreenModel(
mutableState.update { it.copy(searchQuery = query) }
}
fun updateActiveCategoryIndex(index: Int) {
val newIndex = mutableState.updateAndGet { state ->
state.copy(activeCategoryIndex = index)
}
.coercedActiveCategoryIndex
libraryPreferences.lastUsedCategory().set(newIndex)
}
fun openChangeCategoryDialog() {
screenModelScope.launchIO {
// Create a copy of selected manga
@@ -714,6 +725,7 @@ class LibraryScreenModel(
@Immutable
data class LibraryData(
val isInitialized: Boolean = false,
val showSystemCategory: Boolean = false,
val categories: List<Category> = emptyList(),
val favorites: List<LibraryItem> = emptyList(),
val tracksMap: Map</* Manga */ Long, List<Track>> = emptyMap(),
@@ -734,16 +746,24 @@ class LibraryScreenModel(
val showMangaContinueButton: Boolean = false,
val dialog: Dialog? = null,
val libraryData: LibraryData = LibraryData(),
private val activeCategoryIndex: Int = 0,
private val groupedFavorites: Map<Category, List</* LibraryItem */ Long>> = emptyMap(),
) {
val displayedCategories: List<Category> = groupedFavorites.keys.toList()
val coercedActiveCategoryIndex = activeCategoryIndex.coerceIn(
minimumValue = 0,
maximumValue = displayedCategories.lastIndex.coerceAtLeast(0),
)
val activeCategory: Category by lazy { displayedCategories[coercedActiveCategoryIndex] }
val isLibraryEmpty = libraryData.favorites.isEmpty()
val selectionMode = selection.isNotEmpty()
val selectedManga by lazy { selection.mapNotNull { libraryData.favoritesById[it]?.libraryManga?.manga } }
val displayedCategories = groupedFavorites.keys.toList()
fun getItemsForCategoryId(categoryId: Long): List<LibraryItem> {
val category = displayedCategories.find { it.id == categoryId } ?: return emptyList()
return getItemsForCategory(category)

View File

@@ -111,7 +111,7 @@ data object LibraryTab : Tab {
val title = state.getToolbarTitle(
defaultTitle = stringResource(MR.strings.label_library),
defaultCategoryTitle = stringResource(MR.strings.label_default),
page = screenModel.activeCategoryIndex,
page = state.coercedActiveCategoryIndex,
)
LibraryToolbar(
hasActiveFilters = state.hasActiveFilters,
@@ -121,7 +121,7 @@ data object LibraryTab : Tab {
onClickSelectAll = screenModel::selectAll,
onClickInvertSelection = screenModel::invertSelection,
onClickFilter = screenModel::showSettingsDialog,
onClickRefresh = { onClickRefresh(screenModel.activeCategory) },
onClickRefresh = { onClickRefresh(state.activeCategory) },
onClickGlobalUpdate = { onClickRefresh(null) },
onClickOpenRandomManga = {
scope.launch {
@@ -183,10 +183,10 @@ data object LibraryTab : Tab {
searchQuery = state.searchQuery,
selection = state.selection,
contentPadding = contentPadding,
currentPage = { screenModel.activeCategoryIndex },
currentPage = state.coercedActiveCategoryIndex,
hasActiveFilters = state.hasActiveFilters,
showPageTabs = state.showCategoryTabs || !state.searchQuery.isNullOrEmpty(),
onChangeCurrentPage = { screenModel.activeCategoryIndex = it },
onChangeCurrentPage = screenModel::updateActiveCategoryIndex,
onClickManga = { navigator.push(MangaScreen(it)) },
onContinueReadingClicked = { it: LibraryManga ->
scope.launchIO {
@@ -206,7 +206,7 @@ data object LibraryTab : Tab {
screenModel.toggleRangeSelection(category, manga)
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
onRefresh = { onClickRefresh(screenModel.activeCategory) },
onRefresh = { onClickRefresh(state.activeCategory) },
onGlobalSearchClicked = {
navigator.push(GlobalSearchScreen(screenModel.state.value.searchQuery ?: ""))
},
@@ -225,7 +225,7 @@ data object LibraryTab : Tab {
LibrarySettingsDialog(
onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel,
category = screenModel.activeCategory,
category = state.activeCategory,
)
}
is LibraryScreenModel.Dialog.ChangeCategory -> {