Parcourir la source

Tweak library selection (#8513)

* Tweak library selection

Also use the new `fast*` extensions functions in other places of library presenter

* Cleanup
AntsyLich il y a 2 ans
Parent
commit
3f34fa1f58

+ 79 - 0
app/src/main/java/eu/kanade/core/util/CollectionUtils.kt

@@ -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)
+}

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt

@@ -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) ||

+ 55 - 24
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt

@@ -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)