From 3ccf255d4e0541cc0ed3cf7495d061c5328b4b84 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sun, 3 Aug 2025 23:08:19 +0600 Subject: [PATCH] Potentially fix library `IndexOutOfBound` crash (mihonapp/mihon#2341) (cherry picked from commit c4407eda0eed5a7faed47d4470d79e6b1512b5c2) --- .../library/components/LibraryContent.kt | 7 +-- .../ui/library/LibraryScreenModel.kt | 51 +++++++++++++------ .../kanade/tachiyomi/ui/library/LibraryTab.kt | 12 ++--- 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt index a0e6095f3..24dd70072 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt @@ -31,7 +31,7 @@ fun LibraryContent( searchQuery: String?, selection: Set, contentPadding: PaddingValues, - currentPage: () -> Int, + currentPage: Int, hasActiveFilters: Boolean, showPageTabs: Boolean, onChangeCurrentPage: (Int) -> Unit, @@ -53,10 +53,7 @@ fun LibraryContent( end = contentPadding.calculateEndPadding(LocalLayoutDirection.current), ), ) { - // SY --> - val coercedCurrentPage = remember(categories) { currentPage().coerceIn(0, categories.lastIndex) } - // SY <-- - val pagerState = rememberPagerState(coercedCurrentPage) { categories.size } + val pagerState = rememberPagerState(currentPage) { categories.size } val scope = rememberCoroutineScope() var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt index 3095c643b..b255ffbae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt @@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.ui.library import android.app.Application import android.content.Context 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.fastAny import androidx.compose.ui.util.fastFilter @@ -81,6 +79,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.runBlocking import mihon.core.common.utils.mutate import tachiyomi.core.common.i18n.stringResource @@ -162,9 +161,6 @@ class LibraryScreenModel( // KMK <-- ) : StateScreenModel(State()) { - var activeCategoryIndex: Int by libraryPreferences.lastUsedCategory().asState(screenModelScope) - val activeCategory: Category get() = state.value.displayedCategories[activeCategoryIndex] - // SY --> val favoritesSync = FavoritesSyncHelper(preferences.context) val recommendationSearch = RecommendationSearchHelper(preferences.context) @@ -173,6 +169,9 @@ class LibraryScreenModel( // SY <-- init { + mutableState.update { state -> + state.copy(activeCategoryIndex = libraryPreferences.lastUsedCategory().get()) + } screenModelScope.launchIO { combine( combine( @@ -191,6 +190,7 @@ class LibraryScreenModel( // KMK <-- getLibraryItemPreferencesFlow(), ) { (searchQuery, categories, favorites), (tracksMap, trackingFilters), (includedCategories, excludedCategories), itemPreferences -> + val showSystemCategory = favorites.any { it.libraryManga.categories.contains(0) } val filteredFavorites = favorites .applyFilters( tracksMap, @@ -217,6 +217,7 @@ class LibraryScreenModel( LibraryData( isInitialized = true, + showSystemCategory = showSystemCategory, categories = categories, favorites = filteredFavorites, tracksMap = tracksMap, @@ -263,6 +264,7 @@ class LibraryScreenModel( data.favorites .applyGrouping( data.categories, + data.showSystemCategory, // KMK --> if (filterCategory && includedCategories.isNotEmpty()) { LibraryGroup.UNGROUPED @@ -532,6 +534,7 @@ class LibraryScreenModel( private fun List.applyGrouping( categories: List, + showSystemCategory: Boolean, // KMK --> groupType: Int, showHiddenCategories: Boolean, @@ -547,7 +550,6 @@ class LibraryScreenModel( groupCache.getOrPut(categoryId) { mutableListOf() }.add(item.id) } } - val showSystemCategory = groupCache.containsKey(0L) return categories.filter { showSystemCategory || !it.isSystemCategory } // KMK --> .filterNot { !showHiddenCategories && it.hidden } @@ -1091,7 +1093,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() { @@ -1322,7 +1325,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) } @@ -1332,7 +1335,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.toSet()) list.addAll(toAdd) @@ -1345,6 +1348,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 @@ -1583,6 +1595,7 @@ class LibraryScreenModel( @Immutable data class LibraryData( val isInitialized: Boolean = false, + val showSystemCategory: Boolean = false, val categories: List = emptyList(), val favorites: List = emptyList(), val tracksMap: Map> = emptyMap(), @@ -1603,6 +1616,7 @@ class LibraryScreenModel( val showMangaContinueButton: Boolean = false, val dialog: Dialog? = null, val libraryData: LibraryData = LibraryData(), + private val activeCategoryIndex: Int = 0, private val groupedFavorites: Map> = emptyMap(), // SY --> val showSyncExh: Boolean = false, @@ -1615,18 +1629,25 @@ class LibraryScreenModel( val excludedCategories: ImmutableSet = persistentSetOf(), // KMK <-- ) { + /** + * The grouped tabs which is displayed above the library screen. + * They can be actual [Category] or [Source], [Track]... + */ + val displayedCategories: List = 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 } } - /** - * The grouped tabs which is displayed above the library screen. - * They can be actual [Category] or [Source], [Track]... - */ - val displayedCategories = groupedFavorites.keys.toList() - // SY --> val showCleanTitles: Boolean by lazy { selectedManga.fastAny { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index 7b19ceb2e..f5a7d491a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt @@ -146,7 +146,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, @@ -156,7 +156,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 { @@ -303,10 +303,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 { @@ -326,7 +326,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 ?: "")) }, @@ -345,7 +345,7 @@ data object LibraryTab : Tab { LibrarySettingsDialog( onDismissRequest = onDismissRequest, screenModel = settingsScreenModel, - category = screenModel.activeCategory, + category = state.activeCategory, // SY --> hasCategories = state.libraryData.categories.fastAny { !it.isSystemCategory }, // SY <--