Potentially fix library IndexOutOfBound crash (mihonapp/mihon#2341)

(cherry picked from commit c4407eda0eed5a7faed47d4470d79e6b1512b5c2)
This commit is contained in:
AntsyLich 2025-08-03 23:08:19 +06:00 committed by Cuong-Tran
parent 3485297434
commit 3ccf255d4e
3 changed files with 44 additions and 26 deletions

View file

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

View file

@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.ui.library
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFilter import androidx.compose.ui.util.fastFilter
@ -81,6 +79,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import mihon.core.common.utils.mutate import mihon.core.common.utils.mutate
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
@ -162,9 +161,6 @@ class LibraryScreenModel(
// KMK <-- // KMK <--
) : StateScreenModel<LibraryScreenModel.State>(State()) { ) : StateScreenModel<LibraryScreenModel.State>(State()) {
var activeCategoryIndex: Int by libraryPreferences.lastUsedCategory().asState(screenModelScope)
val activeCategory: Category get() = state.value.displayedCategories[activeCategoryIndex]
// SY --> // SY -->
val favoritesSync = FavoritesSyncHelper(preferences.context) val favoritesSync = FavoritesSyncHelper(preferences.context)
val recommendationSearch = RecommendationSearchHelper(preferences.context) val recommendationSearch = RecommendationSearchHelper(preferences.context)
@ -173,6 +169,9 @@ class LibraryScreenModel(
// SY <-- // SY <--
init { init {
mutableState.update { state ->
state.copy(activeCategoryIndex = libraryPreferences.lastUsedCategory().get())
}
screenModelScope.launchIO { screenModelScope.launchIO {
combine( combine(
combine( combine(
@ -191,6 +190,7 @@ class LibraryScreenModel(
// KMK <-- // KMK <--
getLibraryItemPreferencesFlow(), getLibraryItemPreferencesFlow(),
) { (searchQuery, categories, favorites), (tracksMap, trackingFilters), (includedCategories, excludedCategories), itemPreferences -> ) { (searchQuery, categories, favorites), (tracksMap, trackingFilters), (includedCategories, excludedCategories), itemPreferences ->
val showSystemCategory = favorites.any { it.libraryManga.categories.contains(0) }
val filteredFavorites = favorites val filteredFavorites = favorites
.applyFilters( .applyFilters(
tracksMap, tracksMap,
@ -217,6 +217,7 @@ class LibraryScreenModel(
LibraryData( LibraryData(
isInitialized = true, isInitialized = true,
showSystemCategory = showSystemCategory,
categories = categories, categories = categories,
favorites = filteredFavorites, favorites = filteredFavorites,
tracksMap = tracksMap, tracksMap = tracksMap,
@ -263,6 +264,7 @@ class LibraryScreenModel(
data.favorites data.favorites
.applyGrouping( .applyGrouping(
data.categories, data.categories,
data.showSystemCategory,
// KMK --> // KMK -->
if (filterCategory && includedCategories.isNotEmpty()) { if (filterCategory && includedCategories.isNotEmpty()) {
LibraryGroup.UNGROUPED LibraryGroup.UNGROUPED
@ -532,6 +534,7 @@ class LibraryScreenModel(
private fun List<LibraryItem>.applyGrouping( private fun List<LibraryItem>.applyGrouping(
categories: List<Category>, categories: List<Category>,
showSystemCategory: Boolean,
// KMK --> // KMK -->
groupType: Int, groupType: Int,
showHiddenCategories: Boolean, showHiddenCategories: Boolean,
@ -547,7 +550,6 @@ class LibraryScreenModel(
groupCache.getOrPut(categoryId) { mutableListOf() }.add(item.id) groupCache.getOrPut(categoryId) { mutableListOf() }.add(item.id)
} }
} }
val showSystemCategory = groupCache.containsKey(0L)
return categories.filter { showSystemCategory || !it.isSystemCategory } return categories.filter { showSystemCategory || !it.isSystemCategory }
// KMK --> // KMK -->
.filterNot { !showHiddenCategories && it.hidden } .filterNot { !showHiddenCategories && it.hidden }
@ -1091,7 +1093,8 @@ class LibraryScreenModel(
} }
fun getRandomLibraryItemForCurrentCategory(): LibraryItem? { fun getRandomLibraryItemForCurrentCategory(): LibraryItem? {
return state.value.getItemsForCategoryId(activeCategory.id).randomOrNull() val state = state.value
return state.getItemsForCategoryId(state.activeCategory.id).randomOrNull()
} }
fun showSettingsDialog() { fun showSettingsDialog() {
@ -1322,7 +1325,7 @@ class LibraryScreenModel(
lastSelectionCategory = null lastSelectionCategory = null
mutableState.update { state -> mutableState.update { state ->
val newSelection = state.selection.mutate { list -> 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) state.copy(selection = newSelection)
} }
@ -1332,7 +1335,7 @@ class LibraryScreenModel(
lastSelectionCategory = null lastSelectionCategory = null
mutableState.update { state -> mutableState.update { state ->
val newSelection = state.selection.mutate { list -> 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 } val (toRemove, toAdd) = itemIds.partition { it in list }
list.removeAll(toRemove.toSet()) list.removeAll(toRemove.toSet())
list.addAll(toAdd) list.addAll(toAdd)
@ -1345,6 +1348,15 @@ class LibraryScreenModel(
mutableState.update { it.copy(searchQuery = query) } 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() { fun openChangeCategoryDialog() {
screenModelScope.launchIO { screenModelScope.launchIO {
// Create a copy of selected manga // Create a copy of selected manga
@ -1583,6 +1595,7 @@ class LibraryScreenModel(
@Immutable @Immutable
data class LibraryData( data class LibraryData(
val isInitialized: Boolean = false, val isInitialized: Boolean = false,
val showSystemCategory: Boolean = false,
val categories: List<Category> = emptyList(), val categories: List<Category> = emptyList(),
val favorites: List<LibraryItem> = emptyList(), val favorites: List<LibraryItem> = emptyList(),
val tracksMap: Map</* Manga */ Long, List<Track>> = emptyMap(), val tracksMap: Map</* Manga */ Long, List<Track>> = emptyMap(),
@ -1603,6 +1616,7 @@ class LibraryScreenModel(
val showMangaContinueButton: Boolean = false, val showMangaContinueButton: Boolean = false,
val dialog: Dialog? = null, val dialog: Dialog? = null,
val libraryData: LibraryData = LibraryData(), val libraryData: LibraryData = LibraryData(),
private val activeCategoryIndex: Int = 0,
private val groupedFavorites: Map<Category, List</* LibraryItem */ Long>> = emptyMap(), private val groupedFavorites: Map<Category, List</* LibraryItem */ Long>> = emptyMap(),
// SY --> // SY -->
val showSyncExh: Boolean = false, val showSyncExh: Boolean = false,
@ -1615,18 +1629,25 @@ class LibraryScreenModel(
val excludedCategories: ImmutableSet<Long> = persistentSetOf(), val excludedCategories: ImmutableSet<Long> = persistentSetOf(),
// KMK <-- // KMK <--
) { ) {
/**
* The grouped tabs which is displayed above the library screen.
* They can be actual [Category] or [Source], [Track]...
*/
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 isLibraryEmpty = libraryData.favorites.isEmpty()
val selectionMode = selection.isNotEmpty() val selectionMode = selection.isNotEmpty()
val selectedManga by lazy { selection.mapNotNull { libraryData.favoritesById[it]?.libraryManga?.manga } } val selectedManga by lazy { selection.mapNotNull { libraryData.favoritesById[it]?.libraryManga?.manga } }
/**
* The grouped tabs which is displayed above the library screen.
* They can be actual [Category] or [Source], [Track]...
*/
val displayedCategories = groupedFavorites.keys.toList()
// SY --> // SY -->
val showCleanTitles: Boolean by lazy { val showCleanTitles: Boolean by lazy {
selectedManga.fastAny { selectedManga.fastAny {

View file

@ -146,7 +146,7 @@ data object LibraryTab : Tab {
val title = state.getToolbarTitle( val title = state.getToolbarTitle(
defaultTitle = stringResource(MR.strings.label_library), defaultTitle = stringResource(MR.strings.label_library),
defaultCategoryTitle = stringResource(MR.strings.label_default), defaultCategoryTitle = stringResource(MR.strings.label_default),
page = screenModel.activeCategoryIndex, page = state.coercedActiveCategoryIndex,
) )
LibraryToolbar( LibraryToolbar(
hasActiveFilters = state.hasActiveFilters, hasActiveFilters = state.hasActiveFilters,
@ -156,7 +156,7 @@ data object LibraryTab : Tab {
onClickSelectAll = screenModel::selectAll, onClickSelectAll = screenModel::selectAll,
onClickInvertSelection = screenModel::invertSelection, onClickInvertSelection = screenModel::invertSelection,
onClickFilter = screenModel::showSettingsDialog, onClickFilter = screenModel::showSettingsDialog,
onClickRefresh = { onClickRefresh(screenModel.activeCategory) }, onClickRefresh = { onClickRefresh(state.activeCategory) },
onClickGlobalUpdate = { onClickRefresh(null) }, onClickGlobalUpdate = { onClickRefresh(null) },
onClickOpenRandomManga = { onClickOpenRandomManga = {
scope.launch { scope.launch {
@ -303,10 +303,10 @@ data object LibraryTab : Tab {
searchQuery = state.searchQuery, searchQuery = state.searchQuery,
selection = state.selection, selection = state.selection,
contentPadding = contentPadding, contentPadding = contentPadding,
currentPage = { screenModel.activeCategoryIndex }, currentPage = state.coercedActiveCategoryIndex,
hasActiveFilters = state.hasActiveFilters, hasActiveFilters = state.hasActiveFilters,
showPageTabs = state.showCategoryTabs || !state.searchQuery.isNullOrEmpty(), showPageTabs = state.showCategoryTabs || !state.searchQuery.isNullOrEmpty(),
onChangeCurrentPage = { screenModel.activeCategoryIndex = it }, onChangeCurrentPage = screenModel::updateActiveCategoryIndex,
onClickManga = { navigator.push(MangaScreen(it)) }, onClickManga = { navigator.push(MangaScreen(it)) },
onContinueReadingClicked = { it: LibraryManga -> onContinueReadingClicked = { it: LibraryManga ->
scope.launchIO { scope.launchIO {
@ -326,7 +326,7 @@ data object LibraryTab : Tab {
screenModel.toggleRangeSelection(category, manga) screenModel.toggleRangeSelection(category, manga)
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}, },
onRefresh = { onClickRefresh(screenModel.activeCategory) }, onRefresh = { onClickRefresh(state.activeCategory) },
onGlobalSearchClicked = { onGlobalSearchClicked = {
navigator.push(GlobalSearchScreen(screenModel.state.value.searchQuery ?: "")) navigator.push(GlobalSearchScreen(screenModel.state.value.searchQuery ?: ""))
}, },
@ -345,7 +345,7 @@ data object LibraryTab : Tab {
LibrarySettingsDialog( LibrarySettingsDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel, screenModel = settingsScreenModel,
category = screenModel.activeCategory, category = state.activeCategory,
// SY --> // SY -->
hasCategories = state.libraryData.categories.fastAny { !it.isSystemCategory }, hasCategories = state.libraryData.categories.fastAny { !it.isSystemCategory },
// SY <-- // SY <--