mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Tweak library selection (#8513)
* Tweak library selection Also use the new `fast*` extensions functions in other places of library presenter * Cleanup
This commit is contained in:
		| @@ -1,6 +1,9 @@ | ||||
| package eu.kanade.core.util | ||||
|  | ||||
| import androidx.compose.ui.util.fastForEach | ||||
| import java.util.concurrent.ConcurrentHashMap | ||||
| import kotlin.contracts.ExperimentalContracts | ||||
| import kotlin.contracts.contract | ||||
|  | ||||
| fun <T : R, R : Any> List<T>.insertSeparators( | ||||
|     generator: (T?, T?) -> R?, | ||||
| @@ -33,3 +36,79 @@ fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) { | ||||
|         remove(value) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns a list containing only elements matching the given [predicate]. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @Suppress("BanInlineOptIn") | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> { | ||||
|     contract { callsInPlace(predicate) } | ||||
|     val destination = ArrayList<T>() | ||||
|     fastForEach { if (predicate(it)) destination.add(it) } | ||||
|     return destination | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns a list containing all elements not matching the given [predicate]. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @Suppress("BanInlineOptIn") | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> { | ||||
|     contract { callsInPlace(predicate) } | ||||
|     val destination = ArrayList<T>() | ||||
|     fastForEach { if (!predicate(it)) destination.add(it) } | ||||
|     return destination | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns a list containing only the non-null results of applying the | ||||
|  * given [transform] function to each element in the original collection. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @Suppress("BanInlineOptIn") | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> { | ||||
|     contract { callsInPlace(transform) } | ||||
|     val destination = ArrayList<R>() | ||||
|     fastForEach { element -> | ||||
|         transform(element)?.let { destination.add(it) } | ||||
|     } | ||||
|     return destination | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Splits the original collection into pair of lists, | ||||
|  * where *first* list contains elements for which [predicate] yielded `true`, | ||||
|  * while *second* list contains elements for which [predicate] yielded `false`. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @Suppress("BanInlineOptIn") | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T> List<T>.fastPartition(predicate: (T) -> Boolean): Pair<List<T>, List<T>> { | ||||
|     contract { callsInPlace(predicate) } | ||||
|     val first = ArrayList<T>() | ||||
|     val second = ArrayList<T>() | ||||
|     fastForEach { | ||||
|         if (predicate(it)) { | ||||
|             first.add(it) | ||||
|         } else { | ||||
|             second.add(it) | ||||
|         } | ||||
|     } | ||||
|     return Pair(first, second) | ||||
| } | ||||
|   | ||||
| @@ -18,12 +18,12 @@ class LibraryItem( | ||||
|     var sourceLanguage = "" | ||||
|  | ||||
|     /** | ||||
|      * Filters a manga depending on a query. | ||||
|      * Checks if a query matches the manga | ||||
|      * | ||||
|      * @param constraint the query to apply. | ||||
|      * @return true if the manga should be included, false otherwise. | ||||
|      * @param constraint the query to check. | ||||
|      * @return true if the manga matches the query, false otherwise. | ||||
|      */ | ||||
|     fun filter(constraint: String): Boolean { | ||||
|     fun matches(constraint: String): Boolean { | ||||
|         val sourceName by lazy { sourceManager.getOrStub(libraryManga.manga.source).getNameForMangaInfo() } | ||||
|         val genres by lazy { libraryManga.manga.genre } | ||||
|         return libraryManga.manga.title.contains(constraint, true) || | ||||
|   | ||||
| @@ -13,6 +13,10 @@ import androidx.compose.ui.util.fastAny | ||||
| import androidx.compose.ui.util.fastMap | ||||
| import eu.kanade.core.prefs.CheckboxState | ||||
| import eu.kanade.core.prefs.PreferenceMutableState | ||||
| import eu.kanade.core.util.fastFilter | ||||
| import eu.kanade.core.util.fastFilterNot | ||||
| import eu.kanade.core.util.fastMapNotNull | ||||
| import eu.kanade.core.util.fastPartition | ||||
| import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.domain.category.interactor.GetCategories | ||||
| import eu.kanade.domain.category.interactor.SetMangaCategories | ||||
| @@ -155,7 +159,7 @@ class LibraryPresenter( | ||||
|         val filterBookmarked = libraryPreferences.filterBookmarked().get() | ||||
|         val filterCompleted = libraryPreferences.filterCompleted().get() | ||||
|  | ||||
|         val loggedInTrackServices = trackManager.services.filter { trackService -> trackService.isLogged } | ||||
|         val loggedInTrackServices = trackManager.services.fastFilter { trackService -> trackService.isLogged } | ||||
|             .associate { trackService -> | ||||
|                 trackService.id to libraryPreferences.filterTracking(trackService.id.toInt()).get() | ||||
|             } | ||||
| @@ -230,8 +234,8 @@ class LibraryPresenter( | ||||
|  | ||||
|             val mangaTracks = trackMap[item.libraryManga.id].orEmpty() | ||||
|  | ||||
|             val exclude = mangaTracks.filter { it in excludedTracks } | ||||
|             val include = mangaTracks.filter { it in includedTracks } | ||||
|             val exclude = mangaTracks.fastFilter { it in excludedTracks } | ||||
|             val include = mangaTracks.fastFilter { it in includedTracks } | ||||
|  | ||||
|             // TODO: Simplify the filter logic | ||||
|             if (includedTracks.isNotEmpty() && excludedTracks.isNotEmpty()) { | ||||
| @@ -256,7 +260,7 @@ class LibraryPresenter( | ||||
|                 ) | ||||
|         } | ||||
|  | ||||
|         return this.mapValues { entry -> entry.value.filter(filterFn) } | ||||
|         return this.mapValues { entry -> entry.value.fastFilter(filterFn) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -355,7 +359,7 @@ class LibraryPresenter( | ||||
|  | ||||
|         return combine(getCategories.subscribe(), libraryMangasFlow) { categories, libraryManga -> | ||||
|             val displayCategories = if (libraryManga.isNotEmpty() && libraryManga.containsKey(0).not()) { | ||||
|                 categories.filterNot { it.isSystemCategory } | ||||
|                 categories.fastFilterNot { it.isSystemCategory } | ||||
|             } else { | ||||
|                 categories | ||||
|             } | ||||
| @@ -418,7 +422,7 @@ class LibraryPresenter( | ||||
|         presenterScope.launchNonCancellable { | ||||
|             mangas.forEach { manga -> | ||||
|                 val chapters = getNextChapters.await(manga.id) | ||||
|                     .filterNot { chapter -> | ||||
|                     .fastFilterNot { chapter -> | ||||
|                         downloadManager.queue.any { chapter.id == it.chapter.id } || | ||||
|                             downloadManager.isChapterDownloaded( | ||||
|                                 chapter.name, | ||||
| @@ -542,12 +546,20 @@ class LibraryPresenter( | ||||
|  | ||||
|     @Composable | ||||
|     fun getMangaForCategory(page: Int): List<LibraryItem> { | ||||
|         val unfiltered = remember(categories, loadedManga, page) { | ||||
|             val categoryId = categories.getOrNull(page)?.id ?: -1 | ||||
|         val categoryId = remember(categories, page) { | ||||
|             categories.getOrNull(page)?.id ?: -1 | ||||
|         } | ||||
|         val unfiltered = remember(loadedManga, categoryId) { | ||||
|             loadedManga[categoryId] ?: emptyList() | ||||
|         } | ||||
|         return remember(unfiltered, searchQuery) { | ||||
|             if (searchQuery.isNullOrBlank()) unfiltered else unfiltered.filter { it.filter(searchQuery!!) } | ||||
|             if (searchQuery.isNullOrBlank()) { | ||||
|                 queriedMangaMap.clear() | ||||
|                 unfiltered | ||||
|             } else { | ||||
|                 unfiltered.fastFilter { it.matches(searchQuery!!) } | ||||
|                     .also { queriedMangaMap[categoryId] = it } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -565,6 +577,20 @@ class LibraryPresenter( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Map is cleared out via [getMangaForCategory] when [searchQuery] is null or blank | ||||
|      */ | ||||
|     private val queriedMangaMap: MutableMap<Long, List<LibraryItem>> = mutableMapOf() | ||||
|  | ||||
|     /** | ||||
|      * Used by select all, inverse and range selection. | ||||
|      * | ||||
|      * If current query is empty then we get manga list from [loadedManga] otherwise from [queriedMangaMap] | ||||
|      */ | ||||
|     private fun getMangaForCategoryWithQuery(categoryId: Long, query: String?): List<LibraryItem> { | ||||
|         return if (query.isNullOrBlank()) loadedManga[categoryId].orEmpty() else queriedMangaMap[categoryId].orEmpty() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Selects all mangas between and including the given manga and the last pressed manga from the | ||||
|      * same category as the given manga | ||||
| @@ -576,16 +602,22 @@ class LibraryPresenter( | ||||
|                 add(manga) | ||||
|                 return@apply | ||||
|             } | ||||
|             val items = loadedManga[manga.category].orEmpty().apply { | ||||
|                 if (searchQuery.isNullOrBlank()) toList() else filter { it.filter(searchQuery!!) } | ||||
|             }.fastMap { it.libraryManga } | ||||
|  | ||||
|             val items = getMangaForCategoryWithQuery(manga.category, searchQuery) | ||||
|                 .fastMap { it.libraryManga } | ||||
|             val lastMangaIndex = items.indexOf(lastSelected) | ||||
|             val curMangaIndex = items.indexOf(manga) | ||||
|  | ||||
|             val selectedIds = fastMap { it.id } | ||||
|             val newSelections = when (lastMangaIndex >= curMangaIndex + 1) { | ||||
|                 true -> items.subList(curMangaIndex, lastMangaIndex) | ||||
|                 false -> items.subList(lastMangaIndex, curMangaIndex + 1) | ||||
|             }.filterNot { it.id in selectedIds } | ||||
|             val selectionRange = when { | ||||
|                 lastMangaIndex < curMangaIndex -> IntRange(lastMangaIndex, curMangaIndex) | ||||
|                 curMangaIndex < lastMangaIndex -> IntRange(curMangaIndex, lastMangaIndex) | ||||
|                 // We shouldn't reach this point | ||||
|                 else -> return@apply | ||||
|             } | ||||
|             val newSelections = selectionRange.mapNotNull { index -> | ||||
|                 items[index].takeUnless { it.id in selectedIds } | ||||
|             } | ||||
|             addAll(newSelections) | ||||
|         } | ||||
|     } | ||||
| @@ -593,11 +625,12 @@ class LibraryPresenter( | ||||
|     fun selectAll(index: Int) { | ||||
|         state.selection = state.selection.toMutableList().apply { | ||||
|             val categoryId = categories.getOrNull(index)?.id ?: -1 | ||||
|             val items = loadedManga[categoryId].orEmpty().apply { | ||||
|                 if (searchQuery.isNullOrBlank()) toList() else filter { it.filter(searchQuery!!) } | ||||
|             }.fastMap { it.libraryManga } | ||||
|             val selectedIds = fastMap { it.id } | ||||
|             val newSelections = items.filterNot { it.id in selectedIds } | ||||
|             val newSelections = getMangaForCategoryWithQuery(categoryId, searchQuery) | ||||
|                 .fastMapNotNull { item -> | ||||
|                     item.libraryManga.takeUnless { it.id in selectedIds } | ||||
|                 } | ||||
|  | ||||
|             addAll(newSelections) | ||||
|         } | ||||
|     } | ||||
| @@ -605,11 +638,9 @@ class LibraryPresenter( | ||||
|     fun invertSelection(index: Int) { | ||||
|         state.selection = selection.toMutableList().apply { | ||||
|             val categoryId = categories[index].id | ||||
|             val items = loadedManga[categoryId].orEmpty().apply { | ||||
|                 if (searchQuery.isNullOrBlank()) toList() else filter { it.filter(searchQuery!!) } | ||||
|             }.fastMap { it.libraryManga } | ||||
|             val items = getMangaForCategoryWithQuery(categoryId, searchQuery).fastMap { it.libraryManga } | ||||
|             val selectedIds = fastMap { it.id } | ||||
|             val (toRemove, toAdd) = items.partition { it.id in selectedIds } | ||||
|             val (toRemove, toAdd) = items.fastPartition { it.id in selectedIds } | ||||
|             val toRemoveIds = toRemove.fastMap { it.id } | ||||
|             removeAll { it.id in toRemoveIds } | ||||
|             addAll(toAdd) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user