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 package eu.kanade.core.util
import androidx.compose.ui.util.fastForEach
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
fun <T : R, R : Any> List<T>.insertSeparators( fun <T : R, R : Any> List<T>.insertSeparators(
generator: (T?, T?) -> R?, generator: (T?, T?) -> R?,
@ -33,3 +36,79 @@ fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
remove(value) 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 = "" var sourceLanguage = ""
/** /**
* Filters a manga depending on a query. * Checks if a query matches the manga
* *
* @param constraint the query to apply. * @param constraint the query to check.
* @return true if the manga should be included, false otherwise. * @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 sourceName by lazy { sourceManager.getOrStub(libraryManga.manga.source).getNameForMangaInfo(null) }
val genres by lazy { libraryManga.manga.genre } val genres by lazy { libraryManga.manga.genre }
return libraryManga.manga.title.contains(constraint, true) || 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.runtime.setValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import eu.kanade.core.prefs.CheckboxState import eu.kanade.core.prefs.CheckboxState
import eu.kanade.core.prefs.PreferenceMutableState 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.UnsortedPreferences
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.category.interactor.GetCategories import eu.kanade.domain.category.interactor.GetCategories
@ -245,7 +250,7 @@ class LibraryPresenter(
val filterBookmarked = libraryPreferences.filterBookmarked().get() val filterBookmarked = libraryPreferences.filterBookmarked().get()
val filterCompleted = libraryPreferences.filterCompleted().get() val filterCompleted = libraryPreferences.filterCompleted().get()
val loggedInTrackServices = trackManager.services.filter { trackService -> trackService.isLogged } val loggedInTrackServices = trackManager.services.fastFilter { trackService -> trackService.isLogged }
.associate { trackService -> .associate { trackService ->
trackService.id to libraryPreferences.filterTracking(trackService.id.toInt()).get() trackService.id to libraryPreferences.filterTracking(trackService.id.toInt()).get()
} }
@ -324,8 +329,8 @@ class LibraryPresenter(
val mangaTracks = trackMap[item.libraryManga.id].orEmpty() val mangaTracks = trackMap[item.libraryManga.id].orEmpty()
val exclude = mangaTracks.filter { it in excludedTracks } val exclude = mangaTracks.fastFilter { it in excludedTracks }
val include = mangaTracks.filter { it in includedTracks } val include = mangaTracks.fastFilter { it in includedTracks }
// TODO: Simplify the filter logic // TODO: Simplify the filter logic
if (includedTracks.isNotEmpty() && excludedTracks.isNotEmpty()) { 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 -> return combine(getCategories.subscribe(), libraryMangasFlow) { categories, libraryManga ->
val displayCategories = if (libraryManga.isNotEmpty() && libraryManga.containsKey(0).not()) { val displayCategories = if (libraryManga.isNotEmpty() && libraryManga.containsKey(0).not()) {
categories.filterNot { it.isSystemCategory } categories.fastFilterNot { it.isSystemCategory }
} else { } else {
categories categories
} }
@ -613,8 +618,8 @@ class LibraryPresenter(
.groupBy { it.mangaId } .groupBy { it.mangaId }
.forEach ab@{ (mangaId, chapters) -> .forEach ab@{ (mangaId, chapters) ->
val mergedManga = mergedMangas[mangaId] ?: return@ab val mergedManga = mergedMangas[mangaId] ?: return@ab
val downloadChapters = chapters.filterNot { chapter -> val downloadChapters = chapters.fastFilterNot { chapter ->
downloadManager.queue.any { chapter.id == it.chapter.id } || downloadManager.queue.fastAny { chapter.id == it.chapter.id } ||
downloadManager.isChapterDownloaded( downloadManager.isChapterDownloaded(
chapter.name, chapter.name,
chapter.scanlator, chapter.scanlator,
@ -631,8 +636,8 @@ class LibraryPresenter(
// SY <-- // SY <--
val chapters = getNextChapters.await(manga.id) val chapters = getNextChapters.await(manga.id)
.filterNot { chapter -> .fastFilterNot { chapter ->
downloadManager.queue.any { chapter.id == it.chapter.id } || downloadManager.queue.fastAny { chapter.id == it.chapter.id } ||
downloadManager.isChapterDownloaded( downloadManager.isChapterDownloaded(
chapter.name, chapter.name,
chapter.scanlator, chapter.scanlator,
@ -839,21 +844,23 @@ class LibraryPresenter(
// SY --> // SY -->
@Composable @Composable
fun getMangaForCategory(page: Int): List<LibraryItem> { fun getMangaForCategory(page: Int): List<LibraryItem> {
val unfiltered = remember(categories, loadedManga, page) { val categoryId = remember(categories, page) {
val categoryId = categories.getOrNull(page)?.id ?: -1 categories.getOrNull(page)?.id ?: -1
}
val unfiltered = remember(loadedManga, categoryId) {
loadedManga[categoryId] ?: emptyList() loadedManga[categoryId] ?: emptyList()
} }
val items = produceState(initialValue = unfiltered, unfiltered, searchQuery) { val items = produceState(initialValue = unfiltered, unfiltered, searchQuery, categoryId) {
value = withIOContext { value = withIOContext {
filterLibrary(unfiltered, searchQuery) filterLibrary(unfiltered, searchQuery, categoryId)
} }
} }
return items.value 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()) { return if (unfiltered.isNotEmpty() && !query.isNullOrBlank()) {
// Prepare filter object // Prepare filter object
val parsedQuery = searchEngine.parseQuery(query) val parsedQuery = searchEngine.parseQuery(query)
@ -865,7 +872,7 @@ class LibraryPresenter(
} }
val sources = unfiltered val sources = unfiltered
.distinctBy { it.libraryManga.manga.source } .distinctBy { it.libraryManga.manga.source }
.mapNotNull { sourceManager.get(it.libraryManga.manga.source) } .fastMapNotNull { sourceManager.get(it.libraryManga.manga.source) }
.associateBy { it.id } .associateBy { it.id }
unfiltered.asFlow().cancellable().filter { item -> unfiltered.asFlow().cancellable().filter { item ->
val mangaId = item.libraryManga.manga.id val mangaId = item.libraryManga.manga.id
@ -900,8 +907,11 @@ class LibraryPresenter(
source = sources[sourceId], source = sources[sourceId],
) )
} }
}.toList() }.toList().also { queriedMangaMap[categoryId] = it }
} else { } else {
if (query.isNullOrBlank()) {
queriedMangaMap.clear()
}
unfiltered unfiltered
} }
} }
@ -930,12 +940,12 @@ class LibraryPresenter(
(source?.name?.contains(query, true) == true) || (source?.name?.contains(query, true) == true) ||
(sourceIdString != null && sourceIdString == query) || (sourceIdString != null && sourceIdString == query) ||
(loggedServices.isNotEmpty() && tracks != null && filterTracks(query, tracks)) || (loggedServices.isNotEmpty() && tracks != null && filterTracks(query, tracks)) ||
(genre.any { it.contains(query, true) }) || (genre.fastAny { it.contains(query, true) }) ||
(searchTags?.any { it.name.contains(query, true) } == true) || (searchTags?.fastAny { it.name.contains(query, true) } == true) ||
(searchTitles?.any { it.title.contains(query, true) } == true) (searchTitles?.fastAny { it.title.contains(query, true) } == true)
} }
is Namespace -> { is Namespace -> {
searchTags != null && searchTags.any { searchTags != null && searchTags.fastAny {
val tag = queryComponent.tag val tag = queryComponent.tag
(it.namespace.equals(queryComponent.namespace, true) && tag?.run { it.name.contains(tag.asQuery(), true) } == true) || (it.namespace.equals(queryComponent.namespace, true) && tag?.run { it.name.contains(tag.asQuery(), true) } == true) ||
(tag == null && it.namespace.equals(queryComponent.namespace, true)) (tag == null && it.namespace.equals(queryComponent.namespace, true))
@ -954,14 +964,14 @@ class LibraryPresenter(
(source?.name?.contains(query, true) != true) && (source?.name?.contains(query, true) != true) &&
(sourceIdString != null && sourceIdString != query) && (sourceIdString != null && sourceIdString != query) &&
(loggedServices.isEmpty() || loggedServices.isNotEmpty() && tracks == null || tracks != null && !filterTracks(query, tracks)) && (loggedServices.isEmpty() || loggedServices.isNotEmpty() && tracks == null || tracks != null && !filterTracks(query, tracks)) &&
(genre.none { it.contains(query, true) }) && (!genre.fastAny { it.contains(query, true) }) &&
(searchTags?.any { it.name.contains(query, true) } != true) && (searchTags?.fastAny { it.name.contains(query, true) } != true) &&
(searchTitles?.any { it.title.contains(query, true) } != true) (searchTitles?.fastAny { it.title.contains(query, true) } != true)
) )
} }
is Namespace -> { is Namespace -> {
val searchedTag = queryComponent.tag?.asQuery() 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()) { if (queryComponent.namespace.isBlank() && !searchedTag.isNullOrBlank()) {
!mangaTag.name.contains(searchedTag, true) !mangaTag.name.contains(searchedTag, true)
} else if (searchedTag.isNullOrBlank()) { } else if (searchedTag.isNullOrBlank()) {
@ -980,7 +990,7 @@ class LibraryPresenter(
} }
private fun filterTracks(constraint: String, tracks: List<Track>): Boolean { private fun filterTracks(constraint: String, tracks: List<Track>): Boolean {
return tracks.any { return tracks.fastAny {
val trackService = trackManager.getService(it.syncId) val trackService = trackManager.getService(it.syncId)
if (trackService != null) { if (trackService != null) {
val status = trackService.getStatus(it.status.toInt()) 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 * Selects all mangas between and including the given manga and the last pressed manga from the
* same category as the given manga * same category as the given manga
@ -1035,16 +1059,21 @@ class LibraryPresenter(
add(manga) add(manga)
return@apply return@apply
} }
val items = loadedManga[manga.category].orEmpty().run { val items = getMangaForCategoryWithQuery(manga.category, searchQuery)
filterLibrary(this, searchQuery) .fastMap { it.libraryManga }
}.fastMap { it.libraryManga }
val lastMangaIndex = items.indexOf(lastSelected) val lastMangaIndex = items.indexOf(lastSelected)
val curMangaIndex = items.indexOf(manga) val curMangaIndex = items.indexOf(manga)
val selectedIds = fastMap { it.id } val selectedIds = fastMap { it.id }
val newSelections = when (lastMangaIndex >= curMangaIndex + 1) { val selectionRange = when {
true -> items.subList(curMangaIndex, lastMangaIndex) lastMangaIndex < curMangaIndex -> IntRange(lastMangaIndex, curMangaIndex)
false -> items.subList(lastMangaIndex, curMangaIndex + 1) curMangaIndex < lastMangaIndex -> IntRange(curMangaIndex, lastMangaIndex)
}.filterNot { it.id in selectedIds } // We shouldn't reach this point
else -> return@apply
}
val newSelections = selectionRange.mapNotNull { index ->
items[index].takeUnless { it.id in selectedIds }
}
addAll(newSelections) addAll(newSelections)
} }
} }
@ -1054,11 +1083,12 @@ class LibraryPresenter(
presenterScope.launchIO { presenterScope.launchIO {
state.selection = state.selection.toMutableList().apply { state.selection = state.selection.toMutableList().apply {
val categoryId = categories.getOrNull(index)?.id ?: -1 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 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) addAll(newSelections)
} }
} }
@ -1068,11 +1098,9 @@ class LibraryPresenter(
presenterScope.launchIO { presenterScope.launchIO {
state.selection = selection.toMutableList().apply { state.selection = selection.toMutableList().apply {
val categoryId = categories[index].id val categoryId = categories[index].id
val items = loadedManga[categoryId].orEmpty().run { val items = getMangaForCategoryWithQuery(categoryId, searchQuery).fastMap { it.libraryManga }
filterLibrary(this, searchQuery)
}.fastMap { it.libraryManga }
val selectedIds = fastMap { it.id } 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 } val toRemoveIds = toRemove.fastMap { it.id }
removeAll { it.id in toRemoveIds } removeAll { it.id in toRemoveIds }
addAll(toAdd) addAll(toAdd)