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 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() } val sourceName by lazy { sourceManager.getOrStub(libraryManga.manga.source).getNameForMangaInfo() }
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

@ -13,6 +13,10 @@ 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.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.category.interactor.GetCategories import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.SetMangaCategories import eu.kanade.domain.category.interactor.SetMangaCategories
@ -155,7 +159,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()
} }
@ -230,8 +234,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()) {
@ -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 -> 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
} }
@ -418,7 +422,7 @@ class LibraryPresenter(
presenterScope.launchNonCancellable { presenterScope.launchNonCancellable {
mangas.forEach { manga -> mangas.forEach { manga ->
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.any { chapter.id == it.chapter.id } ||
downloadManager.isChapterDownloaded( downloadManager.isChapterDownloaded(
chapter.name, chapter.name,
@ -542,12 +546,20 @@ class LibraryPresenter(
@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()
} }
return remember(unfiltered, searchQuery) { 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 * 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
@ -576,16 +602,22 @@ class LibraryPresenter(
add(manga) add(manga)
return@apply return@apply
} }
val items = loadedManga[manga.category].orEmpty().apply {
if (searchQuery.isNullOrBlank()) toList() else filter { it.filter(searchQuery!!) } val items = getMangaForCategoryWithQuery(manga.category, 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)
} }
} }
@ -593,11 +625,12 @@ class LibraryPresenter(
fun selectAll(index: Int) { fun selectAll(index: Int) {
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().apply {
if (searchQuery.isNullOrBlank()) toList() else filter { it.filter(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)
} }
} }
@ -605,11 +638,9 @@ class LibraryPresenter(
fun invertSelection(index: Int) { fun invertSelection(index: Int) {
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().apply { val items = getMangaForCategoryWithQuery(categoryId, searchQuery).fastMap { it.libraryManga }
if (searchQuery.isNullOrBlank()) toList() else filter { it.filter(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)