* Add onPause\onResume persistence to searchView. Fixes issue #3627 * New controller subclass with built-in SearchView support * Implement new SearchableNucleusController in SourceController * Add query to BasePresenter (for one field it is not worth create a subclass in my opinion), convert BrowseSourceController to inherit from SearchableNucleusController * move to flows to fix an issue in GlobalSearch where it would trigger the search multiple times * Continue conversion to SearchableNucleusController * Convert LibraryController, convert to flows, Known ISSUE with empty string being posted after setting the query upon creation of UI * Fix issues with the post being tide to the SearchView queue which is not processed until shown. Add COLLAPSING state capture which should wrap this up. * refactoring & enforce @StringRes for queryHint
This commit is contained in:
parent
14c114756d
commit
2911fe7a1a
10 changed files with 280 additions and 177 deletions
|
@ -121,7 +121,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
|||
* [expandActionViewFromInteraction] should be set to true in [onOptionsItemSelected] when the expandable item is selected
|
||||
* This method should be called as part of [MenuItem.OnActionExpandListener.onMenuItemActionExpand]
|
||||
*/
|
||||
fun invalidateMenuOnExpand(): Boolean {
|
||||
open fun invalidateMenuOnExpand(): Boolean {
|
||||
return if (expandActionViewFromInteraction) {
|
||||
activity?.invalidateOptionsMenu()
|
||||
false
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
||||
|
||||
/**
|
||||
* Implementation of the NucleusController that has a built-in ViewSearch
|
||||
*/
|
||||
abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*>>
|
||||
(bundle: Bundle? = null) : NucleusController<VB, P>(bundle) {
|
||||
|
||||
enum class SearchViewState { LOADING, LOADED, COLLAPSING, FOCUSED }
|
||||
|
||||
/**
|
||||
* Used to bypass the initial searchView being set to empty string after an onResume
|
||||
*/
|
||||
private var currentSearchViewState: SearchViewState = SearchViewState.LOADING
|
||||
|
||||
/**
|
||||
* Store the query text that has not been submitted to reassign it after an onResume, UI-only
|
||||
*/
|
||||
protected var nonSubmittedQuery: String = ""
|
||||
|
||||
/**
|
||||
* To be called by classes that extend this subclass in onCreateOptionsMenu
|
||||
*/
|
||||
protected fun createOptionsMenu(
|
||||
menu: Menu,
|
||||
inflater: MenuInflater,
|
||||
menuId: Int,
|
||||
searchItemId: Int,
|
||||
@StringRes queryHint: Int? = null,
|
||||
restoreCurrentQuery: Boolean = true
|
||||
) {
|
||||
// Inflate menu
|
||||
inflater.inflate(menuId, menu)
|
||||
|
||||
// Initialize search option.
|
||||
val searchItem = menu.findItem(searchItemId)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
|
||||
searchView.queryTextEvents()
|
||||
.onEach {
|
||||
val newText = it.queryText.toString()
|
||||
|
||||
if (newText.isNotBlank() or acceptEmptyQuery()) {
|
||||
if (it is QueryTextEvent.QuerySubmitted) {
|
||||
// Abstract function for implementation
|
||||
// Run it first in case the old query data is needed (like BrowseSourceController)
|
||||
onSearchViewQueryTextSubmit(newText)
|
||||
presenter.query = newText
|
||||
nonSubmittedQuery = ""
|
||||
} else if ((it is QueryTextEvent.QueryChanged) && (presenter.query != newText)) {
|
||||
nonSubmittedQuery = newText
|
||||
|
||||
// Abstract function for implementation
|
||||
onSearchViewQueryTextChange(newText)
|
||||
}
|
||||
}
|
||||
// clear the collapsing flag
|
||||
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.COLLAPSING)
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
val query = presenter.query
|
||||
|
||||
// Restoring a query the user had not submitted
|
||||
if (nonSubmittedQuery.isNotBlank() and (nonSubmittedQuery != query)) {
|
||||
searchItem.expandActionView()
|
||||
searchView.setQuery(nonSubmittedQuery, false)
|
||||
onSearchViewQueryTextChange(nonSubmittedQuery)
|
||||
} else {
|
||||
if (queryHint != null) {
|
||||
searchView.queryHint = applicationContext?.getString(queryHint)
|
||||
}
|
||||
|
||||
if (restoreCurrentQuery) {
|
||||
// Restoring a query the user had submitted
|
||||
if (query.isNotBlank()) {
|
||||
searchItem.expandActionView()
|
||||
searchView.setQuery(query, true)
|
||||
searchView.clearFocus()
|
||||
onSearchViewQueryTextChange(query)
|
||||
onSearchViewQueryTextSubmit(query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for weird behavior where searchView gets empty text change despite
|
||||
// query being set already, prevents the query from being cleared
|
||||
binding.root.post {
|
||||
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.LOADING)
|
||||
}
|
||||
|
||||
searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
setCurrentSearchViewState(SearchViewState.FOCUSED)
|
||||
} else {
|
||||
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.FOCUSED)
|
||||
}
|
||||
}
|
||||
|
||||
searchItem.setOnActionExpandListener(
|
||||
object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||
onSearchMenuItemActionExpand(item)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
||||
val localSearchView = searchItem.actionView as SearchView
|
||||
|
||||
// if it is blank the flow event won't trigger so we would stay in a COLLAPSING state
|
||||
if (localSearchView.toString().isNotBlank()) {
|
||||
setCurrentSearchViewState(SearchViewState.COLLAPSING)
|
||||
}
|
||||
|
||||
onSearchMenuItemActionCollapse(item)
|
||||
return true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
super.onActivityResumed(activity)
|
||||
// Until everything is up and running don't accept empty queries
|
||||
setCurrentSearchViewState(SearchViewState.LOADING)
|
||||
}
|
||||
|
||||
private fun acceptEmptyQuery(): Boolean {
|
||||
return when (currentSearchViewState) {
|
||||
SearchViewState.COLLAPSING, SearchViewState.FOCUSED -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCurrentSearchViewState(to: SearchViewState, from: SearchViewState? = null) {
|
||||
// When loading ignore all requests other than loaded
|
||||
if ((currentSearchViewState == SearchViewState.LOADING) && (to != SearchViewState.LOADED)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent changing back to an unwanted state when using async flows (ie onFocus event doing
|
||||
// COLLAPSING -> LOADED)
|
||||
if ((from != null) && (currentSearchViewState != from)) {
|
||||
return
|
||||
}
|
||||
|
||||
currentSearchViewState = to
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the SearchView since since the implementation of these can vary in subclasses
|
||||
* Not abstract as they are optional
|
||||
*/
|
||||
protected open fun onSearchViewQueryTextChange(newText: String?) {
|
||||
}
|
||||
|
||||
protected open fun onSearchViewQueryTextSubmit(query: String?) {
|
||||
}
|
||||
|
||||
protected open fun onSearchMenuItemActionExpand(item: MenuItem?) {
|
||||
}
|
||||
|
||||
protected open fun onSearchMenuItemActionCollapse(item: MenuItem?) {
|
||||
}
|
||||
|
||||
/**
|
||||
* During the conversion to SearchableNucleusController (after which I plan to merge its code
|
||||
* into BaseController) this addresses an issue where the searchView.onTextFocus event is not
|
||||
* triggered
|
||||
*/
|
||||
override fun invalidateMenuOnExpand(): Boolean {
|
||||
return if (expandActionViewFromInteraction) {
|
||||
activity?.invalidateOptionsMenu()
|
||||
setCurrentSearchViewState(SearchViewState.FOCUSED) // we are technically focused here
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,6 +12,11 @@ open class BasePresenter<V> : RxPresenter<V>() {
|
|||
|
||||
lateinit var presenterScope: CoroutineScope
|
||||
|
||||
/**
|
||||
* Query from the view where applicable
|
||||
*/
|
||||
var query: String = ""
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
try {
|
||||
super.onCreate(savedState)
|
||||
|
|
|
@ -9,7 +9,6 @@ import android.view.MenuInflater
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItems
|
||||
|
@ -25,19 +24,11 @@ import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding
|
|||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.base.controller.*
|
||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
@ -48,7 +39,7 @@ import uy.kohesive.injekt.api.get
|
|||
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
|
||||
*/
|
||||
class SourceController :
|
||||
NucleusController<SourceMainControllerBinding, SourcePresenter>(),
|
||||
SearchableNucleusController<SourceMainControllerBinding, SourcePresenter>(),
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
SourceAdapter.OnSourceClickListener {
|
||||
|
@ -200,37 +191,6 @@ class SourceController :
|
|||
parentController!!.router.pushController(controller.withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds items to the options menu.
|
||||
*
|
||||
* @param menu menu containing options.
|
||||
* @param inflater used to load the menu xml.
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
// Inflate menu
|
||||
inflater.inflate(R.menu.source_main, menu)
|
||||
|
||||
// Initialize search option.
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
|
||||
// Change hint to show global search.
|
||||
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
|
||||
|
||||
// Create query listener which opens the global search view.
|
||||
searchView.queryTextEvents()
|
||||
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
|
||||
.onEach { performGlobalSearch(it.queryText.toString()) }
|
||||
.launchIn(viewScope)
|
||||
}
|
||||
|
||||
private fun performGlobalSearch(query: String) {
|
||||
parentController!!.router.pushController(
|
||||
GlobalSearchController(query).withFadeTransaction()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an option menu item has been selected by the user.
|
||||
*
|
||||
|
@ -290,4 +250,21 @@ class SourceController :
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
createOptionsMenu(
|
||||
menu,
|
||||
inflater,
|
||||
R.menu.source_main,
|
||||
R.id.action_search,
|
||||
R.string.action_global_search_hint,
|
||||
false // GlobalSearch handles the searching here
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSearchViewQueryTextSubmit(query: String?) {
|
||||
parentController!!.router.pushController(
|
||||
GlobalSearchController(query).withFadeTransaction()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import android.view.MenuInflater
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
|
@ -33,7 +32,7 @@ import eu.kanade.tachiyomi.source.LocalSource
|
|||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||
|
@ -51,12 +50,8 @@ import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
|||
import eu.kanade.tachiyomi.widget.EmptyView
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
|
@ -64,7 +59,7 @@ import uy.kohesive.injekt.injectLazy
|
|||
* Controller to manage the catalogues available in the app.
|
||||
*/
|
||||
open class BrowseSourceController(bundle: Bundle) :
|
||||
NucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
|
||||
SearchableNucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
|
||||
FabController,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
|
@ -259,25 +254,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.source_browse, menu)
|
||||
|
||||
// Initialize search menu
|
||||
createOptionsMenu(menu, inflater, R.menu.source_browse, R.id.action_search)
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
|
||||
val query = presenter.query
|
||||
if (query.isNotBlank()) {
|
||||
searchItem.expandActionView()
|
||||
searchView.setQuery(query, true)
|
||||
searchView.clearFocus()
|
||||
}
|
||||
|
||||
searchView.queryTextEvents()
|
||||
.filter { router.backstack.lastOrNull()?.controller() == this@BrowseSourceController }
|
||||
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
|
||||
.onEach { searchWithQuery(it.queryText.toString()) }
|
||||
.launchIn(viewScope)
|
||||
|
||||
searchItem.fixExpand(
|
||||
onExpand = { invalidateMenuOnExpand() },
|
||||
|
@ -300,6 +278,10 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||
menu.findItem(displayItem).isChecked = true
|
||||
}
|
||||
|
||||
override fun onSearchViewQueryTextSubmit(query: String?) {
|
||||
searchWithQuery(query ?: "")
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
|
||||
|
|
|
@ -66,12 +66,6 @@ open class BrowseSourcePresenter(
|
|||
*/
|
||||
lateinit var source: CatalogueSource
|
||||
|
||||
/**
|
||||
* Query from the view.
|
||||
*/
|
||||
var query = searchQuery ?: ""
|
||||
private set
|
||||
|
||||
/**
|
||||
* Modifiable list of filters.
|
||||
*/
|
||||
|
@ -108,6 +102,10 @@ open class BrowseSourcePresenter(
|
|||
*/
|
||||
private var pageSubscription: Subscription? = null
|
||||
|
||||
init {
|
||||
query = searchQuery ?: ""
|
||||
}
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.*
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -15,15 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
|
@ -34,7 +24,7 @@ import uy.kohesive.injekt.injectLazy
|
|||
open class GlobalSearchController(
|
||||
protected val initialQuery: String? = null,
|
||||
protected val extensionFilter: String? = null
|
||||
) : NucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(),
|
||||
) : SearchableNucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(),
|
||||
GlobalSearchCardAdapter.OnMangaClickListener,
|
||||
GlobalSearchAdapter.OnTitleClickListener {
|
||||
|
||||
|
@ -45,6 +35,11 @@ open class GlobalSearchController(
|
|||
*/
|
||||
protected var adapter: GlobalSearchAdapter? = null
|
||||
|
||||
/**
|
||||
* Ref to the OptionsMenu.SearchItem created in onCreateOptionsMenu
|
||||
*/
|
||||
private var optionsMenuSearchItem: MenuItem? = null
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
@ -100,36 +95,32 @@ open class GlobalSearchController(
|
|||
* @param inflater used to load the menu xml.
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
// Inflate menu.
|
||||
inflater.inflate(R.menu.global_search, menu)
|
||||
|
||||
// Initialize search menu
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
|
||||
searchItem.setOnActionExpandListener(
|
||||
object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||
searchView.onActionViewExpanded() // Required to show the query in the view
|
||||
searchView.setQuery(presenter.query, false)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
createOptionsMenu(
|
||||
menu,
|
||||
inflater,
|
||||
R.menu.global_search,
|
||||
R.id.action_search,
|
||||
null,
|
||||
false // the onMenuItemActionExpand will handle this
|
||||
)
|
||||
|
||||
searchView.queryTextEvents()
|
||||
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
|
||||
.onEach {
|
||||
presenter.search(it.queryText.toString())
|
||||
searchItem.collapseActionView()
|
||||
setTitle() // Update toolbar title
|
||||
optionsMenuSearchItem = menu.findItem(R.id.action_search)
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
override fun onSearchMenuItemActionExpand(item: MenuItem?) {
|
||||
super.onSearchMenuItemActionExpand(item)
|
||||
val searchView = optionsMenuSearchItem?.actionView as SearchView
|
||||
searchView.onActionViewExpanded() // Required to show the query in the view
|
||||
|
||||
if (nonSubmittedQuery.isBlank()) {
|
||||
searchView.setQuery(presenter.query, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSearchViewQueryTextSubmit(query: String?) {
|
||||
presenter.search(query ?: "")
|
||||
optionsMenuSearchItem?.collapseActionView()
|
||||
setTitle() // Update toolbar title
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -47,12 +47,6 @@ open class GlobalSearchPresenter(
|
|||
*/
|
||||
val sources by lazy { getSourcesToQuery() }
|
||||
|
||||
/**
|
||||
* Query from the view.
|
||||
*/
|
||||
var query = ""
|
||||
private set
|
||||
|
||||
/**
|
||||
* Fetches the different sources by user settings.
|
||||
*/
|
||||
|
|
|
@ -10,7 +10,6 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
|
@ -27,21 +26,16 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.base.controller.*
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
import reactivecircus.flowbinding.appcompat.queryTextChanges
|
||||
import reactivecircus.flowbinding.viewpager.pageSelections
|
||||
import rx.Subscription
|
||||
import uy.kohesive.injekt.Injekt
|
||||
|
@ -50,7 +44,7 @@ import uy.kohesive.injekt.api.get
|
|||
class LibraryController(
|
||||
bundle: Bundle? = null,
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
) : NucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
|
||||
) : SearchableNucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
|
||||
RootController,
|
||||
TabbedController,
|
||||
ActionMode.Callback,
|
||||
|
@ -67,11 +61,6 @@ class LibraryController(
|
|||
*/
|
||||
private var actionMode: ActionMode? = null
|
||||
|
||||
/**
|
||||
* Library search query.
|
||||
*/
|
||||
private var query: String = ""
|
||||
|
||||
/**
|
||||
* Currently selected mangas.
|
||||
*/
|
||||
|
@ -212,7 +201,7 @@ class LibraryController(
|
|||
binding.btnGlobalSearch.clicks()
|
||||
.onEach {
|
||||
router.pushController(
|
||||
GlobalSearchController(query).withFadeTransaction()
|
||||
GlobalSearchController(presenter.query).withFadeTransaction()
|
||||
)
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
@ -384,52 +373,21 @@ class LibraryController(
|
|||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.library, menu)
|
||||
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
|
||||
|
||||
if (query.isNotEmpty()) {
|
||||
searchItem.expandActionView()
|
||||
searchView.setQuery(query, true)
|
||||
searchView.clearFocus()
|
||||
|
||||
performSearch()
|
||||
|
||||
// Workaround for weird behavior where searchview gets empty text change despite
|
||||
// query being set already
|
||||
searchView.postDelayed({ initSearchHandler(searchView) }, 500)
|
||||
} else {
|
||||
initSearchHandler(searchView)
|
||||
}
|
||||
|
||||
createOptionsMenu(menu, inflater, R.menu.library, R.id.action_search)
|
||||
// Mutate the filter icon because it needs to be tinted and the resource is shared.
|
||||
menu.findItem(R.id.action_filter).icon.mutate()
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
this.query = query
|
||||
}
|
||||
|
||||
private fun initSearchHandler(searchView: SearchView) {
|
||||
searchView.queryTextChanges()
|
||||
// Ignore events if this controller isn't at the top to avoid query being reset
|
||||
.filter { router.backstack.lastOrNull()?.controller() == this }
|
||||
.onEach {
|
||||
query = it.toString()
|
||||
performSearch()
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
presenter.query = query
|
||||
}
|
||||
|
||||
private fun performSearch() {
|
||||
searchRelay.call(query)
|
||||
if (query.isNotEmpty()) {
|
||||
searchRelay.call(presenter.query)
|
||||
if (presenter.query.isNotEmpty()) {
|
||||
binding.btnGlobalSearch.isVisible = true
|
||||
binding.btnGlobalSearch.text =
|
||||
resources?.getString(R.string.action_global_search_query, query)
|
||||
resources?.getString(R.string.action_global_search_query, presenter.query)
|
||||
} else {
|
||||
binding.btnGlobalSearch.isVisible = false
|
||||
}
|
||||
|
@ -611,4 +569,12 @@ class LibraryController(
|
|||
selectInverseRelay.call(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSearchViewQueryTextChange(newText: String?) {
|
||||
// Ignore events if this controller isn't at the top to avoid query being reset
|
||||
if (router.backstack.lastOrNull()?.controller() == this) {
|
||||
presenter.query = newText ?: ""
|
||||
performSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,12 +12,6 @@ import uy.kohesive.injekt.api.get
|
|||
*/
|
||||
open class SettingsSearchPresenter : BasePresenter<SettingsSearchController>() {
|
||||
|
||||
/**
|
||||
* Query from the view.
|
||||
*/
|
||||
var query = ""
|
||||
private set
|
||||
|
||||
val preferences: PreferencesHelper = Injekt.get()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
|
|
Reference in a new issue