Tweak library selection (#8513)

* Tweak library selection

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

* Cleanup

(cherry picked from commit 3f34fa1f588d3b9a0562415d907e4ca6e01f7715)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
This commit is contained in:
AntsyLich 2022-11-19 09:33:38 +06:00 committed by Jobobby04
parent d12e0954b6
commit 2321e6b0d8
3 changed files with 151 additions and 44 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(null) }
val genres by lazy { libraryManga.manga.genre }
return libraryManga.manga.title.contains(constraint, true) ||

View File

@ -11,10 +11,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastAll
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.UnsortedPreferences
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.category.interactor.GetCategories
@ -245,7 +250,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()
}
@ -324,8 +329,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()) {
@ -366,7 +371,7 @@ class LibraryPresenter(
)
}
return this.mapValues { entry -> entry.value.filter(filterFn) }
return this.mapValues { entry -> entry.value.fastFilter(filterFn) }
}
/**
@ -514,7 +519,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
}
@ -613,8 +618,8 @@ class LibraryPresenter(
.groupBy { it.mangaId }
.forEach ab@{ (mangaId, chapters) ->
val mergedManga = mergedMangas[mangaId] ?: return@ab
val downloadChapters = chapters.filterNot { chapter ->
downloadManager.queue.any { chapter.id == it.chapter.id } ||
val downloadChapters = chapters.fastFilterNot { chapter ->
downloadManager.queue.fastAny { chapter.id == it.chapter.id } ||
downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
@ -631,8 +636,8 @@ class LibraryPresenter(
// SY <--
val chapters = getNextChapters.await(manga.id)
.filterNot { chapter ->
downloadManager.queue.any { chapter.id == it.chapter.id } ||
.fastFilterNot { chapter ->
downloadManager.queue.fastAny { chapter.id == it.chapter.id } ||
downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
@ -839,21 +844,23 @@ class LibraryPresenter(
// SY -->
@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()
}
val items = produceState(initialValue = unfiltered, unfiltered, searchQuery) {
val items = produceState(initialValue = unfiltered, unfiltered, searchQuery, categoryId) {
value = withIOContext {
filterLibrary(unfiltered, searchQuery)
filterLibrary(unfiltered, searchQuery, categoryId)
}
}
return items.value
}
suspend fun filterLibrary(unfiltered: List<LibraryItem>, query: String?): List<LibraryItem> {
suspend fun filterLibrary(unfiltered: List<LibraryItem>, query: String?, categoryId: Long): List<LibraryItem> {
return if (unfiltered.isNotEmpty() && !query.isNullOrBlank()) {
// Prepare filter object
val parsedQuery = searchEngine.parseQuery(query)
@ -865,7 +872,7 @@ class LibraryPresenter(
}
val sources = unfiltered
.distinctBy { it.libraryManga.manga.source }
.mapNotNull { sourceManager.get(it.libraryManga.manga.source) }
.fastMapNotNull { sourceManager.get(it.libraryManga.manga.source) }
.associateBy { it.id }
unfiltered.asFlow().cancellable().filter { item ->
val mangaId = item.libraryManga.manga.id
@ -900,8 +907,11 @@ class LibraryPresenter(
source = sources[sourceId],
)
}
}.toList()
}.toList().also { queriedMangaMap[categoryId] = it }
} else {
if (query.isNullOrBlank()) {
queriedMangaMap.clear()
}
unfiltered
}
}
@ -930,12 +940,12 @@ class LibraryPresenter(
(source?.name?.contains(query, true) == true) ||
(sourceIdString != null && sourceIdString == query) ||
(loggedServices.isNotEmpty() && tracks != null && filterTracks(query, tracks)) ||
(genre.any { it.contains(query, true) }) ||
(searchTags?.any { it.name.contains(query, true) } == true) ||
(searchTitles?.any { it.title.contains(query, true) } == true)
(genre.fastAny { it.contains(query, true) }) ||
(searchTags?.fastAny { it.name.contains(query, true) } == true) ||
(searchTitles?.fastAny { it.title.contains(query, true) } == true)
}
is Namespace -> {
searchTags != null && searchTags.any {
searchTags != null && searchTags.fastAny {
val tag = queryComponent.tag
(it.namespace.equals(queryComponent.namespace, true) && tag?.run { it.name.contains(tag.asQuery(), true) } == true) ||
(tag == null && it.namespace.equals(queryComponent.namespace, true))
@ -954,14 +964,14 @@ class LibraryPresenter(
(source?.name?.contains(query, true) != true) &&
(sourceIdString != null && sourceIdString != query) &&
(loggedServices.isEmpty() || loggedServices.isNotEmpty() && tracks == null || tracks != null && !filterTracks(query, tracks)) &&
(genre.none { it.contains(query, true) }) &&
(searchTags?.any { it.name.contains(query, true) } != true) &&
(searchTitles?.any { it.title.contains(query, true) } != true)
(!genre.fastAny { it.contains(query, true) }) &&
(searchTags?.fastAny { it.name.contains(query, true) } != true) &&
(searchTitles?.fastAny { it.title.contains(query, true) } != true)
)
}
is Namespace -> {
val searchedTag = queryComponent.tag?.asQuery()
searchTags == null || (queryComponent.namespace.isBlank() && searchedTag.isNullOrBlank()) || searchTags.all { mangaTag ->
searchTags == null || (queryComponent.namespace.isBlank() && searchedTag.isNullOrBlank()) || searchTags.fastAll { mangaTag ->
if (queryComponent.namespace.isBlank() && !searchedTag.isNullOrBlank()) {
!mangaTag.name.contains(searchedTag, true)
} else if (searchedTag.isNullOrBlank()) {
@ -980,7 +990,7 @@ class LibraryPresenter(
}
private fun filterTracks(constraint: String, tracks: List<Track>): Boolean {
return tracks.any {
return tracks.fastAny {
val trackService = trackManager.getService(it.syncId)
if (trackService != null) {
val status = trackService.getStatus(it.status.toInt())
@ -1023,6 +1033,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
@ -1035,16 +1059,21 @@ class LibraryPresenter(
add(manga)
return@apply
}
val items = loadedManga[manga.category].orEmpty().run {
filterLibrary(this, 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)
}
}
@ -1054,11 +1083,12 @@ class LibraryPresenter(
presenterScope.launchIO {
state.selection = state.selection.toMutableList().apply {
val categoryId = categories.getOrNull(index)?.id ?: -1
val items = loadedManga[categoryId].orEmpty().run {
filterLibrary(this, 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)
}
}
@ -1068,11 +1098,9 @@ class LibraryPresenter(
presenterScope.launchIO {
state.selection = selection.toMutableList().apply {
val categoryId = categories[index].id
val items = loadedManga[categoryId].orEmpty().run {
filterLibrary(this, 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)