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.isLewdSource
|
||||||
import exh.metadata.sql.tables.SearchMetadataTable
|
import exh.metadata.sql.tables.SearchMetadataTable
|
||||||
import exh.search.SearchEngine
|
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 timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
@ -17,15 +21,16 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*/
|
*/
|
||||||
class LibraryCategoryAdapter(val view: LibraryCategoryView) :
|
class LibraryCategoryAdapter(val view: LibraryCategoryView) :
|
||||||
FlexibleAdapter<LibraryItem>(null, view, true) {
|
FlexibleAdapter<LibraryItem>(null, view, true) {
|
||||||
// --> EH
|
// EXH -->
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
private val searchEngine = SearchEngine()
|
private val searchEngine = SearchEngine()
|
||||||
|
private var lastFilterJob: Job? = null
|
||||||
|
|
||||||
// Keep compatibility as searchText field was replaced when we upgraded FlexibleAdapter
|
// Keep compatibility as searchText field was replaced when we upgraded FlexibleAdapter
|
||||||
var searchText
|
var searchText
|
||||||
get() = getFilter(String::class.java) ?: ""
|
get() = getFilter(String::class.java) ?: ""
|
||||||
set(value) { setFilter(value) }
|
set(value) { setFilter(value) }
|
||||||
// <-- EH
|
// EXH <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of manga in this category.
|
* The list of manga in this category.
|
||||||
@ -37,11 +42,11 @@ class LibraryCategoryAdapter(val view: LibraryCategoryView) :
|
|||||||
*
|
*
|
||||||
* @param list the list to set.
|
* @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.
|
// A copy of manga always unfiltered.
|
||||||
mangas = list.toList()
|
mangas = list.toList()
|
||||||
|
|
||||||
performFilter()
|
performFilter(cScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,45 +58,66 @@ class LibraryCategoryAdapter(val view: LibraryCategoryView) :
|
|||||||
return currentItems.indexOfFirst { it.manga.id == manga.id }
|
return currentItems.indexOfFirst { it.manga.id == manga.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun performFilter() {
|
// EXH -->
|
||||||
if(searchText.isNotBlank()) {
|
// Note that we cannot use FlexibleAdapter's built in filtering system as we cannot cancel it
|
||||||
// EXH -->
|
// (well technically we can cancel it by invoking filterItems again but that doesn't work when
|
||||||
try {
|
// we want to perform a no-op filter)
|
||||||
val startTime = System.currentTimeMillis()
|
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 sqlQuery = searchEngine.queryToSql(parsedQuery)
|
val newManga = try {
|
||||||
val queryResult = db.lowLevel().rawQuery(RawQuery.builder()
|
// Prepare filter object
|
||||||
.query(sqlQuery.first)
|
val parsedQuery = searchEngine.parseQuery(savedSearchText)
|
||||||
.args(*sqlQuery.second.toTypedArray())
|
val sqlQuery = searchEngine.queryToSql(parsedQuery)
|
||||||
.build())
|
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 mangaIdCol = queryResult.getColumnIndex(SearchMetadataTable.COL_MANGA_ID)
|
|
||||||
queryResult.moveToFirst()
|
|
||||||
while(queryResult.count > 0 && !queryResult.isAfterLast) {
|
|
||||||
convertedResult += queryResult.getLong(mangaIdCol)
|
|
||||||
queryResult.moveToNext()
|
|
||||||
}
|
|
||||||
|
|
||||||
val out = mangas.filter {
|
val convertedResult = LongArray(queryResult.count)
|
||||||
if(isLewdSource(it.manga.source)) {
|
if(convertedResult.isNotEmpty()) {
|
||||||
convertedResult.binarySearch(it.manga.id) >= 0
|
val mangaIdCol = queryResult.getColumnIndex(SearchMetadataTable.COL_MANGA_ID)
|
||||||
} else {
|
queryResult.moveToFirst()
|
||||||
it.filter(searchText)
|
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)
|
withContext(Dispatchers.Main) {
|
||||||
|
updateDataSet(newManga)
|
||||||
updateDataSet(out)
|
}
|
||||||
} catch(e: Exception) {
|
|
||||||
Timber.w(e, "Could not filter mangas!")
|
|
||||||
updateDataSet(mangas)
|
|
||||||
}
|
}
|
||||||
// EXH <--
|
lastFilterJob = job
|
||||||
|
job.join()
|
||||||
} else {
|
} else {
|
||||||
updateDataSet(mangas)
|
updateDataSet(mangas)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// EXH <--
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,9 @@ import eu.kanade.tachiyomi.util.inflate
|
|||||||
import eu.kanade.tachiyomi.util.plusAssign
|
import eu.kanade.tachiyomi.util.plusAssign
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
|
import exh.ui.LoadingHandle
|
||||||
import kotlinx.android.synthetic.main.library_category.view.*
|
import kotlinx.android.synthetic.main.library_category.view.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -63,6 +65,15 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
*/
|
*/
|
||||||
private var subscriptions = CompositeSubscription()
|
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) {
|
fun onCreate(controller: LibraryController) {
|
||||||
this.controller = controller
|
this.controller = controller
|
||||||
|
|
||||||
@ -112,28 +123,62 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
SelectableAdapter.Mode.SINGLE
|
SelectableAdapter.Mode.SINGLE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EXH -->
|
||||||
|
scope = newScope()
|
||||||
|
initialLoadHandle = controller.loaderManager.openProgressBar()
|
||||||
|
// EXH <--
|
||||||
|
|
||||||
subscriptions += controller.searchRelay
|
subscriptions += controller.searchRelay
|
||||||
.doOnNext { adapter.searchText = it }
|
.doOnNext { adapter.searchText = it }
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.debounce(500, TimeUnit.MILLISECONDS)
|
.debounce(500, TimeUnit.MILLISECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.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
|
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
|
subscriptions += controller.selectionRelay
|
||||||
.subscribe { onSelectionChanged(it) }
|
.subscribe { onSelectionChanged(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRecycle() {
|
fun onRecycle() {
|
||||||
adapter.setItems(emptyList())
|
runBlocking { adapter.setItems(this, emptyList()) }
|
||||||
adapter.clearSelection()
|
adapter.clearSelection()
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unsubscribe() {
|
fun unsubscribe() {
|
||||||
subscriptions.clear()
|
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.
|
* @param event the event received.
|
||||||
*/
|
*/
|
||||||
fun onNextLibraryManga(event: LibraryMangaEvent) {
|
suspend fun onNextLibraryManga(cScope: CoroutineScope, event: LibraryMangaEvent) {
|
||||||
// Get the manga list for this category.
|
// Get the manga list for this category.
|
||||||
val mangaForCategory = event.getMangaForCategory(category).orEmpty()
|
val mangaForCategory = event.getMangaForCategory(category).orEmpty()
|
||||||
|
|
||||||
// Update the category with its manga.
|
// Update the category with its manga.
|
||||||
adapter.setItems(mangaForCategory)
|
// EXH -->
|
||||||
|
adapter.setItems(cScope, mangaForCategory)
|
||||||
|
// EXH <--
|
||||||
|
|
||||||
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||||
controller.selectedMangas.forEach { manga ->
|
controller.selectedMangas.forEach { manga ->
|
||||||
|
@ -38,6 +38,7 @@ import eu.kanade.tachiyomi.util.inflate
|
|||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import exh.favorites.FavoritesIntroDialog
|
import exh.favorites.FavoritesIntroDialog
|
||||||
import exh.favorites.FavoritesSyncStatus
|
import exh.favorites.FavoritesSyncStatus
|
||||||
|
import exh.ui.LoaderManager
|
||||||
import kotlinx.android.synthetic.main.library_controller.*
|
import kotlinx.android.synthetic.main.library_controller.*
|
||||||
import kotlinx.android.synthetic.main.main_activity.*
|
import kotlinx.android.synthetic.main.main_activity.*
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
@ -131,6 +132,7 @@ class LibraryController(
|
|||||||
private var oldSyncStatus: FavoritesSyncStatus? = null
|
private var oldSyncStatus: FavoritesSyncStatus? = null
|
||||||
//Favorites
|
//Favorites
|
||||||
private var favoritesSyncSubscription: Subscription? = null
|
private var favoritesSyncSubscription: Subscription? = null
|
||||||
|
val loaderManager = LoaderManager()
|
||||||
// <-- EH
|
// <-- EH
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -169,6 +171,12 @@ class LibraryController(
|
|||||||
if (selectedMangas.isNotEmpty()) {
|
if (selectedMangas.isNotEmpty()) {
|
||||||
createActionModeIfNeeded()
|
createActionModeIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EXH -->
|
||||||
|
loaderManager.loadingChangeListener = {
|
||||||
|
library_progress.visibility = if(it) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
// EXH <--
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
@ -185,6 +193,9 @@ class LibraryController(
|
|||||||
actionMode = null
|
actionMode = null
|
||||||
tabsVisibilitySubscription?.unsubscribe()
|
tabsVisibilitySubscription?.unsubscribe()
|
||||||
tabsVisibilitySubscription = null
|
tabsVisibilitySubscription = null
|
||||||
|
// EXH -->
|
||||||
|
loaderManager.loadingChangeListener = null
|
||||||
|
// EXH <--
|
||||||
super.onDestroyView(view)
|
super.onDestroyView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,8 +28,11 @@ private inline fun <reified T> delegatedSourceId(): Long {
|
|||||||
}!!.value.sourceId
|
}!!.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
|
it.value.newSourceClass in DELEGATED_LEWD_SOURCES
|
||||||
}.any {
|
}.map { it.value.sourceId }.sorted()
|
||||||
it.value.sourceId == source
|
|
||||||
}
|
// 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:layout_height="match_parent"
|
||||||
android:id="@+id/library_pager"/>
|
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
|
<eu.kanade.tachiyomi.widget.EmptyView
|
||||||
android:id="@+id/empty_view"
|
android:id="@+id/empty_view"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user