Library search is now asynchronous
Library will now display progress bar while loading More optimizations to library search
This commit is contained in:
parent
cadd389658
commit
f772501159
@ -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()
|
||||
// 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 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)
|
||||
if(!isActive) return@launch // Fail early when cancelled
|
||||
|
||||
val convertedResult = LongArray(queryResult.count)
|
||||
if(convertedResult.isNotEmpty()) {
|
||||
val mangaIdCol = queryResult.getColumnIndex(SearchMetadataTable.COL_MANGA_ID)
|
||||
queryResult.moveToFirst()
|
||||
while(queryResult.count > 0 && !queryResult.isAfterLast) {
|
||||
convertedResult += queryResult.getLong(mangaIdCol)
|
||||
while (!queryResult.isAfterLast) {
|
||||
if(!isActive) return@launch // Fail early when cancelled
|
||||
|
||||
convertedResult[queryResult.position] = queryResult.getLong(mangaIdCol)
|
||||
queryResult.moveToNext()
|
||||
}
|
||||
}
|
||||
|
||||
val out = mangas.filter {
|
||||
if(isLewdSource(it.manga.source)) {
|
||||
convertedResult.binarySearch(it.manga.id) >= 0
|
||||
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 {
|
||||
it.filter(searchText)
|
||||
}
|
||||
item.filter(savedSearchText)
|
||||
}
|
||||
}.toList()
|
||||
} catch (e: Exception) {
|
||||
// Do not catch cancellations
|
||||
if(e is CancellationException) throw e
|
||||
|
||||
Timber.d("===> Took %s milliseconds to filter manga!", System.currentTimeMillis() - startTime)
|
||||
|
||||
updateDataSet(out)
|
||||
} catch(e: Exception) {
|
||||
Timber.w(e, "Could not filter mangas!")
|
||||
mangas
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
updateDataSet(newManga)
|
||||
}
|
||||
}
|
||||
lastFilterJob = job
|
||||
job.join()
|
||||
} else {
|
||||
updateDataSet(mangas)
|
||||
}
|
||||
}
|
||||
// EXH <--
|
||||
} else {
|
||||
updateDataSet(mangas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 ->
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
55
app/src/main/java/exh/ui/LoadingTools.kt
Normal file
55
app/src/main/java/exh/ui/LoadingTools.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user