Open source from global search

Along with other changes from upstream

The difference as you'd expect:
Not allowing the opening of a source on migration and from extension intent
Also when moving up sources in global search after returning results. The incoming results for subsequent sources will go under the already returned results, instead of being alphabetical and shifting the top results (pinned sources still show on top though)

Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com>
This commit is contained in:
Jays2Kings 2021-04-24 16:31:31 -04:00
parent 48a4b3091f
commit c9388f6633
12 changed files with 155 additions and 60 deletions

View File

@ -284,7 +284,7 @@ class PreferencesHelper(val context: Context) {
fun hiddenSources() = flowPrefs.getStringSet("hidden_catalogues", mutableSetOf())
fun pinnedCatalogues() = rxPrefs.getStringSet("pinned_catalogues", emptySet())
fun pinnedCatalogues() = flowPrefs.getStringSet("pinned_catalogues", mutableSetOf())
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)

View File

@ -177,7 +177,7 @@ class PreMigrationController(bundle: Bundle? = null) :
val enabledSources = if (item.itemId == R.id.action_match_enabled) {
prefs.hiddenSources().get().mapNotNull { it.toLongOrNull() }
} else {
prefs.pinnedCatalogues().get()?.mapNotNull { it.toLongOrNull() } ?: emptyList()
prefs.pinnedCatalogues().get().mapNotNull { it.toLongOrNull() } ?: emptyList()
}
val items = adapter?.currentItems?.toList() ?: return true
items.forEach {

View File

@ -24,7 +24,6 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.databinding.BrowseControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
@ -390,7 +389,7 @@ class BrowseController :
}
private fun pinCatalogue(source: Source, isPinned: Boolean) {
val current = preferences.pinnedCatalogues().getOrDefault()
val current = preferences.pinnedCatalogues().get()
if (isPinned) {
preferences.pinnedCatalogues().set(current - source.id.toString())
} else {

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.source
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
@ -57,7 +56,7 @@ class SourcePresenter(
private fun loadSources() {
scope.launch {
val pinnedSources = mutableListOf<SourceItem>()
val pinnedCatalogues = preferences.pinnedCatalogues().getOrDefault()
val pinnedCatalogues = preferences.pinnedCatalogues().get()
val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
// Catalogues without a lang defined will be placed at the end
@ -105,7 +104,7 @@ class SourcePresenter(
private fun getLastUsedSource(value: Long): SourceItem? {
return (sourceManager.get(value) as? CatalogueSource)?.let { source ->
val pinnedCatalogues = preferences.pinnedCatalogues().getOrDefault()
val pinnedCatalogues = preferences.pinnedCatalogues().get()
val isPinned = source.id.toString() in pinnedCatalogues
if (isPinned) null
else SourceItem(source, null, isPinned)

View File

@ -12,7 +12,6 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
@ -29,6 +28,7 @@ import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.ui.source.BrowseController
import eu.kanade.tachiyomi.ui.source.global_search.GlobalSearchController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.addOrRemoveToFavorites
import eu.kanade.tachiyomi.util.system.connectivityManager
@ -37,6 +37,7 @@ import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.inflate
import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.util.view.updateLayoutParams
import eu.kanade.tachiyomi.util.view.visible
@ -44,12 +45,8 @@ import eu.kanade.tachiyomi.util.view.visibleIf
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.EmptyView
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
/**
* Controller to manage the catalogues available in the app.
@ -105,11 +102,6 @@ open class BrowseSourceController(bundle: Bundle) :
*/
private var recycler: RecyclerView? = null
/**
* Subscription for the search view.
*/
private var searchViewSubscription: Subscription? = null
/**
* Endless loading item.
*/
@ -124,7 +116,7 @@ open class BrowseSourceController(bundle: Bundle) :
}
override fun createPresenter(): BrowseSourcePresenter {
return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY))
return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY))
}
override fun createBinding(inflater: LayoutInflater) = BrowseSourceControllerBinding.inflate(inflater)
@ -142,8 +134,6 @@ open class BrowseSourceController(bundle: Bundle) :
}
override fun onDestroyView(view: View) {
searchViewSubscription?.unsubscribe()
searchViewSubscription = null
adapter = null
snack = null
recycler = null
@ -222,31 +212,40 @@ open class BrowseSourceController(bundle: Bundle) :
val searchView = searchItem.actionView as SearchView
val query = presenter.query
if (!query.isBlank()) {
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
val searchEventsObservable = searchView.queryTextChangeEvents()
.skip(1)
.filter { router.backstack.lastOrNull()?.controller() == this@BrowseSourceController }
.share()
val writingObservable = searchEventsObservable
.filter { !it.isSubmitted }
.debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
val submitObservable = searchEventsObservable
.filter { it.isSubmitted }
// val searchEventsObservable = searchView.queryTextChangeEvents()
// .skip(1)
// .filter { router.backstack.lastOrNull()?.controller() == this@BrowseSourceController }
// .share()
// val writingObservable = searchEventsObservable
// .filter { !it.isSubmitted }
// .debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
// val submitObservable = searchEventsObservable
// .filter { it.isSubmitted }
//
// searchViewSubscription?.unsubscribe()
// searchViewSubscription = Observable.merge(writingObservable, submitObservable)
// .map { it.queryText().toString() }
// .subscribeUntilDestroy { searchWithQuery(it) }
searchViewSubscription?.unsubscribe()
searchViewSubscription = Observable.merge(writingObservable, submitObservable)
.map { it.queryText().toString() }
.subscribeUntilDestroy { searchWithQuery(it) }
setOnQueryTextChangeListener(searchView, onlyOnSubmit = true, hideKbOnSubmit = false) {
searchWithQuery(it ?: "")
true
}
searchItem.fixExpand(
onExpand = { invalidateMenuOnExpand() },
onCollapse = {
searchWithQuery("")
if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller() is GlobalSearchController) {
router.popController(this)
} else {
searchWithQuery("")
}
true
}
)

View File

@ -48,6 +48,7 @@ import uy.kohesive.injekt.api.get
*/
open class BrowseSourcePresenter(
sourceId: Long,
searchQuery: String? = null,
sourceManager: SourceManager = Injekt.get(),
val db: DatabaseHelper = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(),
@ -117,6 +118,10 @@ open class BrowseSourcePresenter(
private var scope = CoroutineScope(Job() + Dispatchers.IO)
init {
query = searchQuery ?: ""
}
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import android.os.Parcelable
import android.util.SparseArray
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.source.CatalogueSource
/**
* Adapter that holds the search cards.
@ -13,6 +14,8 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
class GlobalSearchAdapter(val controller: GlobalSearchController) :
FlexibleAdapter<GlobalSearchItem>(null, controller, true) {
val titleClickListener: OnTitleClickListener = controller
/**
* Bundle where the view state of the holders is saved.
*/
@ -67,6 +70,10 @@ class GlobalSearchAdapter(val controller: GlobalSearchController) :
}
}
interface OnTitleClickListener {
fun onTitleClick(source: CatalogueSource)
}
private companion object {
const val HOLDER_BUNDLE_KEY = "holder_bundle"
}

View File

@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.util.addOrRemoveToFavorites
import eu.kanade.tachiyomi.util.view.activityBinding
import eu.kanade.tachiyomi.util.view.scrollViewWith
@ -33,9 +34,10 @@ import uy.kohesive.injekt.injectLazy
*/
open class GlobalSearchController(
protected val initialQuery: String? = null,
protected val extensionFilter: String? = null
val extensionFilter: String? = null
) : NucleusController<SourceGlobalSearchControllerBinding, GlobalSearchPresenter>(),
FloatingSearchInterface,
GlobalSearchAdapter.OnTitleClickListener,
GlobalSearchCardAdapter.OnMangaClickListener {
/**
@ -82,6 +84,11 @@ open class GlobalSearchController(
return GlobalSearchPresenter(initialQuery, extensionFilter)
}
override fun onTitleClick(source: CatalogueSource) {
preferences.lastUsedCatalogueSource().set(source.id)
router.pushController(BrowseSourceController(source, presenter.query).withFadeTransaction())
}
/**
* Called when manga in global search is clicked, opens manga.
*
@ -180,6 +187,9 @@ open class GlobalSearchController(
customTitle = view.context?.getString(R.string.loading)
setTitle()
}
binding.recycler.post {
binding.recycler.scrollToPosition(0)
}
}
override fun onDestroyView(view: View) {

View File

@ -1,9 +1,13 @@
package eu.kanade.tachiyomi.ui.source.global_search
import android.view.View
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.SourceGlobalSearchControllerCardBinding
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.migration.SearchController
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.visible
@ -30,6 +34,15 @@ class GlobalSearchHolder(view: View, val adapter: GlobalSearchAdapter) :
binding.recycler.layoutManager =
androidx.recyclerview.widget.LinearLayoutManager(view.context, androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL, false)
binding.recycler.adapter = mangaAdapter
binding.titleMoreIcon.isVisible = adapter.controller !is SearchController && adapter.controller.extensionFilter == null
if (binding.titleMoreIcon.isVisible) {
binding.titleWrapper.setOnClickListener {
adapter.getItem(bindingAdapterPosition)?.let {
adapter.titleClickListener.onTitleClick(it.source)
}
}
}
}
/**
@ -46,6 +59,8 @@ class GlobalSearchHolder(view: View, val adapter: GlobalSearchAdapter) :
// Set Title with country code if available.
binding.title.text = titlePrefix + source.name + langSuffix
binding.subtitle.isVisible = source !is LocalSource
binding.subtitle.text = LocaleHelper.getDisplayName(source.lang)
when {
results == null -> {

View File

@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
@ -25,6 +24,7 @@ import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.Date
/**
* Presenter of [GlobalSearchController]
@ -32,7 +32,7 @@ import uy.kohesive.injekt.injectLazy
*
* @param sourceManager manages the different sources.
* @param db manages the database calls.
* @param preferencesHelper manages the preference calls.
* @param preferences manages the preference calls.
*/
open class GlobalSearchPresenter(
private val initialQuery: String? = "",
@ -40,7 +40,7 @@ open class GlobalSearchPresenter(
private val sourcesToUse: List<CatalogueSource>? = null,
val sourceManager: SourceManager = Injekt.get(),
val db: DatabaseHelper = Injekt.get(),
private val preferencesHelper: PreferencesHelper = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
) : BasePresenter<GlobalSearchController>() {
@ -60,6 +60,8 @@ open class GlobalSearchPresenter(
*/
private var fetchSourcesSubscription: Subscription? = null
private var loadTime = hashMapOf<Long, Long>()
/**
* Subject which fetches image of given manga.
*/
@ -104,16 +106,16 @@ open class GlobalSearchPresenter(
* @return list containing enabled sources.
*/
protected open fun getEnabledSources(): List<CatalogueSource> {
val languages = preferencesHelper.enabledLanguages().get()
val hiddenCatalogues = preferencesHelper.hiddenSources().get()
val pinnedCatalogues = preferencesHelper.pinnedCatalogues().getOrDefault()
val languages = preferences.enabledLanguages().get()
val hiddenCatalogues = preferences.hiddenSources().get()
val pinnedCatalogues = preferences.pinnedCatalogues().get()
val list = sourceManager.getCatalogueSources()
.filter { it.lang in languages }
.filterNot { it.id.toString() in hiddenCatalogues }
.sortedBy { "(${it.lang}) ${it.name}" }
return if (preferencesHelper.onlySearchPinned().get()) {
return if (preferences.onlySearchPinned().get()) {
list.filter { it.id.toString() in pinnedCatalogues }
} else {
list.sortedBy { it.id.toString() !in pinnedCatalogues }
@ -176,6 +178,8 @@ open class GlobalSearchPresenter(
val initialItems = sources.map { createCatalogueSearchItem(it, null) }
var items = initialItems
val pinnedSourceIds = preferences.pinnedCatalogues().get()
fetchSourcesSubscription?.unsubscribe()
fetchSourcesSubscription = Observable.from(sources).flatMap(
{ source ->
@ -197,6 +201,9 @@ open class GlobalSearchPresenter(
} // Convert to local manga.
.doOnNext { fetchImage(it, source) } // Load manga covers.
.map {
if (it.isNotEmpty() && !loadTime.containsKey(source.id)) {
loadTime[source.id] = Date().time
}
createCatalogueSearchItem(
source,
it.map { GlobalSearchMangaItem(it) }
@ -204,10 +211,22 @@ open class GlobalSearchPresenter(
}
},
5
).observeOn(AndroidSchedulers.mainThread())
)
.observeOn(AndroidSchedulers.mainThread())
// Update matching source with the obtained results
.map { result ->
items.map { item -> if (item.source == result.source) result else item }
items
.map { item -> if (item.source == result.source) result else item }
.sortedWith(
compareBy(
// Bubble up sources that actually have results
{ it.results.isNullOrEmpty() },
// Same as initial sort, i.e. pinned first then alphabetically
{ it.source.id.toString() !in pinnedSourceIds },
{ loadTime[it.source.id] ?: 0L },
{ "${it.source.name.toLowerCase()} (${it.source.lang})" }
)
)
}
// Update current state
.doOnNext { items = it }

View File

@ -42,6 +42,7 @@ import kotlin.random.Random
fun Controller.setOnQueryTextChangeListener(
searchView: SearchView,
onlyOnSubmit: Boolean = false,
hideKbOnSubmit: Boolean = true,
f: (text: String?) -> Boolean
) {
searchView.setOnQueryTextListener(
@ -57,10 +58,12 @@ fun Controller.setOnQueryTextChangeListener(
override fun onQueryTextSubmit(query: String?): Boolean {
if (router.backstack.lastOrNull()?.controller() == this@setOnQueryTextChangeListener) {
val imm =
activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
?: return f(query)
imm.hideSoftInputFromWindow(searchView.windowToken, 0)
if (hideKbOnSubmit) {
val imm =
activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
?: return f(query)
imm.hideSoftInputFromWindow(searchView.windowToken, 0)
}
return f(query)
}
return true

View File

@ -6,18 +6,56 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/title"
style="@style/TextAppearance.Regular.SubHeading"
android:layout_width="wrap_content"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/title_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingStart="16dp"
android:paddingEnd="0dp"
android:paddingBottom="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Title" />
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/title"
style="@style/TextAppearance.Regular.SubHeading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/subtitle"
app:layout_constraintEnd_toStartOf="@id/title_more_icon"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Title" />
<TextView
android:id="@+id/subtitle"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:maxLines="1"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/title_more_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="English"
tools:visibility="visible" />
<ImageView
android:id="@+id/title_more_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/more"
android:padding="@dimen/material_component_text_fields_padding_above_and_below_label"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow_forward_24dp"
app:tint="?android:attr/textColorPrimary" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/no_results"
@ -34,7 +72,7 @@
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/title_wrapper"
android:text="@string/no_results_found"/>
<FrameLayout
@ -44,7 +82,8 @@
android:minHeight="208dp"
android:paddingTop="2dp"
android:paddingBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/title"
android:layout_marginTop="4dp"
app:layout_constraintTop_toBottomOf="@id/title_wrapper"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constrainedHeight="true"