Library search is now asynchronous

Library will now display progress bar while loading
More optimizations to library search
This commit is contained in:
NerdNumber9 2019-04-12 06:50:24 -04:00
parent cadd389658
commit f772501159
6 changed files with 192 additions and 43 deletions

View File

@ -7,6 +7,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import exh.isLewdSource
import exh.metadata.sql.tables.SearchMetadataTable
import exh.search.SearchEngine
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.toList
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
@ -17,15 +21,16 @@ import uy.kohesive.injekt.injectLazy
*/
class LibraryCategoryAdapter(val view: LibraryCategoryView) :
FlexibleAdapter<LibraryItem>(null, view, true) {
// --> EH
// EXH -->
private val db: DatabaseHelper by injectLazy()
private val searchEngine = SearchEngine()
private var lastFilterJob: Job? = null
// Keep compatibility as searchText field was replaced when we upgraded FlexibleAdapter
var searchText
get() = getFilter(String::class.java) ?: ""
set(value) { setFilter(value) }
// <-- EH
// EXH <--
/**
* The list of manga in this category.
@ -37,11 +42,11 @@ class LibraryCategoryAdapter(val view: LibraryCategoryView) :
*
* @param list the list to set.
*/
fun setItems(list: List<LibraryItem>) {
suspend fun setItems(cScope: CoroutineScope, list: List<LibraryItem>) {
// A copy of manga always unfiltered.
mangas = list.toList()
performFilter()
performFilter(cScope)
}
/**
@ -53,45 +58,66 @@ class LibraryCategoryAdapter(val view: LibraryCategoryView) :
return currentItems.indexOfFirst { it.manga.id == manga.id }
}
fun performFilter() {
if(searchText.isNotBlank()) {
// EXH -->
try {
val startTime = System.currentTimeMillis()
// EXH -->
// Note that we cannot use FlexibleAdapter's built in filtering system as we cannot cancel it
// (well technically we can cancel it by invoking filterItems again but that doesn't work when
// we want to perform a no-op filter)
suspend fun performFilter(cScope: CoroutineScope) {
lastFilterJob?.cancel()
if(mangas.isNotEmpty() && searchText.isNotBlank()) {
val savedSearchText = searchText
val parsedQuery = searchEngine.parseQuery(searchText)
val sqlQuery = searchEngine.queryToSql(parsedQuery)
val queryResult = db.lowLevel().rawQuery(RawQuery.builder()
.query(sqlQuery.first)
.args(*sqlQuery.second.toTypedArray())
.build())
val job = cScope.launch(Dispatchers.IO) {
val newManga = try {
// Prepare filter object
val parsedQuery = searchEngine.parseQuery(savedSearchText)
val sqlQuery = searchEngine.queryToSql(parsedQuery)
val queryResult = db.lowLevel().rawQuery(RawQuery.builder()
.query(sqlQuery.first)
.args(*sqlQuery.second.toTypedArray())
.build())
val convertedResult = ArrayList<Long>(queryResult.count)
val mangaIdCol = queryResult.getColumnIndex(SearchMetadataTable.COL_MANGA_ID)
queryResult.moveToFirst()
while(queryResult.count > 0 && !queryResult.isAfterLast) {
convertedResult += queryResult.getLong(mangaIdCol)
queryResult.moveToNext()
}
if(!isActive) return@launch // Fail early when cancelled
val out = mangas.filter {
if(isLewdSource(it.manga.source)) {
convertedResult.binarySearch(it.manga.id) >= 0
} else {
it.filter(searchText)
val convertedResult = LongArray(queryResult.count)
if(convertedResult.isNotEmpty()) {
val mangaIdCol = queryResult.getColumnIndex(SearchMetadataTable.COL_MANGA_ID)
queryResult.moveToFirst()
while (!queryResult.isAfterLast) {
if(!isActive) return@launch // Fail early when cancelled
convertedResult[queryResult.position] = queryResult.getLong(mangaIdCol)
queryResult.moveToNext()
}
}
if(!isActive) return@launch // Fail early when cancelled
// Flow the mangas to allow cancellation of this filter operation
mangas.asFlow().filter { item ->
if(isLewdSource(item.manga.source)) {
convertedResult.binarySearch(item.manga.id ?: -1) >= 0
} else {
item.filter(savedSearchText)
}
}.toList()
} catch (e: Exception) {
// Do not catch cancellations
if(e is CancellationException) throw e
Timber.w(e, "Could not filter mangas!")
mangas
}
Timber.d("===> Took %s milliseconds to filter manga!", System.currentTimeMillis() - startTime)
updateDataSet(out)
} catch(e: Exception) {
Timber.w(e, "Could not filter mangas!")
updateDataSet(mangas)
withContext(Dispatchers.Main) {
updateDataSet(newManga)
}
}
// EXH <--
lastFilterJob = job
job.join()
} else {
updateDataSet(mangas)
}
}
// EXH <--
}

View File

@ -18,7 +18,9 @@ import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.util.plusAssign
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import exh.ui.LoadingHandle
import kotlinx.android.synthetic.main.library_category.view.*
import kotlinx.coroutines.*
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy
@ -63,6 +65,15 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
*/
private var subscriptions = CompositeSubscription()
// EXH -->
private var initialLoadHandle: LoadingHandle? = null
lateinit var scope: CoroutineScope
private fun newScope() = object : CoroutineScope {
override val coroutineContext = SupervisorJob() + Dispatchers.Main
}
// EXH <--
fun onCreate(controller: LibraryController) {
this.controller = controller
@ -112,28 +123,62 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
SelectableAdapter.Mode.SINGLE
}
// EXH -->
scope = newScope()
initialLoadHandle = controller.loaderManager.openProgressBar()
// EXH <--
subscriptions += controller.searchRelay
.doOnNext { adapter.searchText = it }
.skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { adapter.performFilter() }
.subscribe {
// EXH -->
scope.launch {
val handle = controller.loaderManager.openProgressBar()
try {
// EXH <--
adapter.performFilter(this)
// EXH -->
} finally {
controller.loaderManager.closeProgressBar(handle)
}
}
// EXH <--
}
subscriptions += controller.libraryMangaRelay
.subscribe { onNextLibraryManga(it) }
.subscribe {
// EXH -->
scope.launch {
try {
// EXH <--
onNextLibraryManga(this, it)
// EXH -->
} finally {
controller.loaderManager.closeProgressBar(initialLoadHandle)
}
}
// EXH <--
}
subscriptions += controller.selectionRelay
.subscribe { onSelectionChanged(it) }
}
fun onRecycle() {
adapter.setItems(emptyList())
runBlocking { adapter.setItems(this, emptyList()) }
adapter.clearSelection()
unsubscribe()
}
fun unsubscribe() {
subscriptions.clear()
// EXH -->
scope.cancel()
controller.loaderManager.closeProgressBar(initialLoadHandle)
// EXH <--
}
/**
@ -142,12 +187,14 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
*
* @param event the event received.
*/
fun onNextLibraryManga(event: LibraryMangaEvent) {
suspend fun onNextLibraryManga(cScope: CoroutineScope, event: LibraryMangaEvent) {
// Get the manga list for this category.
val mangaForCategory = event.getMangaForCategory(category).orEmpty()
// Update the category with its manga.
adapter.setItems(mangaForCategory)
// EXH -->
adapter.setItems(cScope, mangaForCategory)
// EXH <--
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
controller.selectedMangas.forEach { manga ->

View File

@ -38,6 +38,7 @@ import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.util.toast
import exh.favorites.FavoritesIntroDialog
import exh.favorites.FavoritesSyncStatus
import exh.ui.LoaderManager
import kotlinx.android.synthetic.main.library_controller.*
import kotlinx.android.synthetic.main.main_activity.*
import rx.Subscription
@ -131,6 +132,7 @@ class LibraryController(
private var oldSyncStatus: FavoritesSyncStatus? = null
//Favorites
private var favoritesSyncSubscription: Subscription? = null
val loaderManager = LoaderManager()
// <-- EH
init {
@ -169,6 +171,12 @@ class LibraryController(
if (selectedMangas.isNotEmpty()) {
createActionModeIfNeeded()
}
// EXH -->
loaderManager.loadingChangeListener = {
library_progress.visibility = if(it) View.VISIBLE else View.GONE
}
// EXH <--
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
@ -185,6 +193,9 @@ class LibraryController(
actionMode = null
tabsVisibilitySubscription?.unsubscribe()
tabsVisibilitySubscription = null
// EXH -->
loaderManager.loadingChangeListener = null
// EXH <--
super.onDestroyView(view)
}

View File

@ -28,8 +28,11 @@ private inline fun <reified T> delegatedSourceId(): Long {
}!!.value.sourceId
}
fun isLewdSource(source: Long) = source in 6900..6999 || SourceManager.DELEGATED_SOURCES.filter {
// Used to speed up isLewdSource
private val lewdDelegatedSourceIds = SourceManager.DELEGATED_SOURCES.filter {
it.value.newSourceClass in DELEGATED_LEWD_SOURCES
}.any {
it.value.sourceId == source
}
}.map { it.value.sourceId }.sorted()
// This method MUST be fast!
fun isLewdSource(source: Long) = source in 6900..6999
|| lewdDelegatedSourceIds.binarySearch(source) >= 0

View File

@ -0,0 +1,55 @@
package exh.ui
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
typealias LoadingHandle = String
/**
* Class used to manage loader UIs
*/
class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext): CoroutineScope {
override val coroutineContext = Dispatchers.Main + parentContext
private val openLoadingHandles = mutableListOf<LoadingHandle>()
var loadingChangeListener: (suspend (newIsLoading: Boolean) -> Unit)? = null
fun openProgressBar(): LoadingHandle {
val (handle, shouldUpdateLoadingStatus) = synchronized(this) {
val handle = UUID.randomUUID().toString()
openLoadingHandles += handle
handle to (openLoadingHandles.size == 1)
}
if(shouldUpdateLoadingStatus) {
launch {
updateLoadingStatus(true)
}
}
return handle
}
@Synchronized
fun closeProgressBar(handle: LoadingHandle?) {
if(handle == null) return
val shouldUpdateLoadingStatus = synchronized(this) {
openLoadingHandles.remove(handle) && openLoadingHandles.isEmpty()
}
if(shouldUpdateLoadingStatus) {
launch {
updateLoadingStatus(false)
}
}
}
private suspend fun updateLoadingStatus(newStatus: Boolean) {
loadingChangeListener?.invoke(newStatus)
}
}

View File

@ -8,6 +8,13 @@
android:layout_height="match_parent"
android:id="@+id/library_pager"/>
<ProgressBar
android:id="@+id/library_progress"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view"
android:visibility="gone"