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:
AntsyLich 2022-11-19 09:33:38 +06:00 committed by GitHub
parent d12ea86b55
commit 3f34fa1f58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 138 additions and 28 deletions

View file

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

View file

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

View file

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