* 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
|
* [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]
|
* This method should be called as part of [MenuItem.OnActionExpandListener.onMenuItemActionExpand]
|
||||||
*/
|
*/
|
||||||
fun invalidateMenuOnExpand(): Boolean {
|
open fun invalidateMenuOnExpand(): Boolean {
|
||||||
return if (expandActionViewFromInteraction) {
|
return if (expandActionViewFromInteraction) {
|
||||||
activity?.invalidateOptionsMenu()
|
activity?.invalidateOptionsMenu()
|
||||||
false
|
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
|
lateinit var presenterScope: CoroutineScope
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query from the view where applicable
|
||||||
|
*/
|
||||||
|
var query: String = ""
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
try {
|
try {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
|
@ -9,7 +9,6 @@ import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.list.listItems
|
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.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.*
|
||||||
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.browse.BrowseController
|
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
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.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
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.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
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
|
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
|
||||||
*/
|
*/
|
||||||
class SourceController :
|
class SourceController :
|
||||||
NucleusController<SourceMainControllerBinding, SourcePresenter>(),
|
SearchableNucleusController<SourceMainControllerBinding, SourcePresenter>(),
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
SourceAdapter.OnSourceClickListener {
|
SourceAdapter.OnSourceClickListener {
|
||||||
|
@ -200,37 +191,6 @@ class SourceController :
|
||||||
parentController!!.router.pushController(controller.withFadeTransaction())
|
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.
|
* 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.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
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.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
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.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||||
|
@ -51,12 +50,8 @@ import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
import eu.kanade.tachiyomi.widget.EmptyView
|
import eu.kanade.tachiyomi.widget.EmptyView
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
|
||||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
@ -64,7 +59,7 @@ import uy.kohesive.injekt.injectLazy
|
||||||
* Controller to manage the catalogues available in the app.
|
* Controller to manage the catalogues available in the app.
|
||||||
*/
|
*/
|
||||||
open class BrowseSourceController(bundle: Bundle) :
|
open class BrowseSourceController(bundle: Bundle) :
|
||||||
NucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
|
SearchableNucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
|
||||||
FabController,
|
FabController,
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
|
@ -259,25 +254,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.source_browse, menu)
|
createOptionsMenu(menu, inflater, R.menu.source_browse, R.id.action_search)
|
||||||
|
|
||||||
// Initialize search menu
|
|
||||||
val searchItem = menu.findItem(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(
|
searchItem.fixExpand(
|
||||||
onExpand = { invalidateMenuOnExpand() },
|
onExpand = { invalidateMenuOnExpand() },
|
||||||
|
@ -300,6 +278,10 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||||
menu.findItem(displayItem).isChecked = true
|
menu.findItem(displayItem).isChecked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSearchViewQueryTextSubmit(query: String?) {
|
||||||
|
searchWithQuery(query ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
super.onPrepareOptionsMenu(menu)
|
super.onPrepareOptionsMenu(menu)
|
||||||
|
|
||||||
|
|
|
@ -66,12 +66,6 @@ open class BrowseSourcePresenter(
|
||||||
*/
|
*/
|
||||||
lateinit var source: CatalogueSource
|
lateinit var source: CatalogueSource
|
||||||
|
|
||||||
/**
|
|
||||||
* Query from the view.
|
|
||||||
*/
|
|
||||||
var query = searchQuery ?: ""
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modifiable list of filters.
|
* Modifiable list of filters.
|
||||||
*/
|
*/
|
||||||
|
@ -108,6 +102,10 @@ open class BrowseSourcePresenter(
|
||||||
*/
|
*/
|
||||||
private var pageSubscription: Subscription? = null
|
private var pageSubscription: Subscription? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
query = searchQuery ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
|
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.*
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
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.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
|
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
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.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
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
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,7 +24,7 @@ import uy.kohesive.injekt.injectLazy
|
||||||
open class GlobalSearchController(
|
open class GlobalSearchController(
|
||||||
protected val initialQuery: String? = null,
|
protected val initialQuery: String? = null,
|
||||||
protected val extensionFilter: String? = null
|
protected val extensionFilter: String? = null
|
||||||
) : NucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(),
|
) : SearchableNucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(),
|
||||||
GlobalSearchCardAdapter.OnMangaClickListener,
|
GlobalSearchCardAdapter.OnMangaClickListener,
|
||||||
GlobalSearchAdapter.OnTitleClickListener {
|
GlobalSearchAdapter.OnTitleClickListener {
|
||||||
|
|
||||||
|
@ -45,6 +35,11 @@ open class GlobalSearchController(
|
||||||
*/
|
*/
|
||||||
protected var adapter: GlobalSearchAdapter? = null
|
protected var adapter: GlobalSearchAdapter? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ref to the OptionsMenu.SearchItem created in onCreateOptionsMenu
|
||||||
|
*/
|
||||||
|
private var optionsMenuSearchItem: MenuItem? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
}
|
}
|
||||||
|
@ -100,36 +95,32 @@ open class GlobalSearchController(
|
||||||
* @param inflater used to load the menu xml.
|
* @param inflater used to load the menu xml.
|
||||||
*/
|
*/
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
// Inflate menu.
|
createOptionsMenu(
|
||||||
inflater.inflate(R.menu.global_search, menu)
|
menu,
|
||||||
|
inflater,
|
||||||
// Initialize search menu
|
R.menu.global_search,
|
||||||
val searchItem = menu.findItem(R.id.action_search)
|
R.id.action_search,
|
||||||
val searchView = searchItem.actionView as SearchView
|
null,
|
||||||
searchView.maxWidth = Int.MAX_VALUE
|
false // the onMenuItemActionExpand will handle this
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
searchView.queryTextEvents()
|
optionsMenuSearchItem = menu.findItem(R.id.action_search)
|
||||||
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
|
|
||||||
.onEach {
|
|
||||||
presenter.search(it.queryText.toString())
|
|
||||||
searchItem.collapseActionView()
|
|
||||||
setTitle() // Update toolbar title
|
|
||||||
}
|
}
|
||||||
.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() }
|
val sources by lazy { getSourcesToQuery() }
|
||||||
|
|
||||||
/**
|
|
||||||
* Query from the view.
|
|
||||||
*/
|
|
||||||
var query = ""
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the different sources by user settings.
|
* Fetches the different sources by user settings.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -10,7 +10,6 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
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.data.preference.asImmediateFlow
|
||||||
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
|
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.*
|
||||||
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.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import reactivecircus.flowbinding.android.view.clicks
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
import reactivecircus.flowbinding.appcompat.queryTextChanges
|
|
||||||
import reactivecircus.flowbinding.viewpager.pageSelections
|
import reactivecircus.flowbinding.viewpager.pageSelections
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -50,7 +44,7 @@ import uy.kohesive.injekt.api.get
|
||||||
class LibraryController(
|
class LibraryController(
|
||||||
bundle: Bundle? = null,
|
bundle: Bundle? = null,
|
||||||
private val preferences: PreferencesHelper = Injekt.get()
|
private val preferences: PreferencesHelper = Injekt.get()
|
||||||
) : NucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
|
) : SearchableNucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
|
||||||
RootController,
|
RootController,
|
||||||
TabbedController,
|
TabbedController,
|
||||||
ActionMode.Callback,
|
ActionMode.Callback,
|
||||||
|
@ -67,11 +61,6 @@ class LibraryController(
|
||||||
*/
|
*/
|
||||||
private var actionMode: ActionMode? = null
|
private var actionMode: ActionMode? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Library search query.
|
|
||||||
*/
|
|
||||||
private var query: String = ""
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Currently selected mangas.
|
* Currently selected mangas.
|
||||||
*/
|
*/
|
||||||
|
@ -212,7 +201,7 @@ class LibraryController(
|
||||||
binding.btnGlobalSearch.clicks()
|
binding.btnGlobalSearch.clicks()
|
||||||
.onEach {
|
.onEach {
|
||||||
router.pushController(
|
router.pushController(
|
||||||
GlobalSearchController(query).withFadeTransaction()
|
GlobalSearchController(presenter.query).withFadeTransaction()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.launchIn(viewScope)
|
.launchIn(viewScope)
|
||||||
|
@ -384,52 +373,21 @@ class LibraryController(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.library, menu)
|
createOptionsMenu(menu, inflater, R.menu.library, R.id.action_search)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutate the filter icon because it needs to be tinted and the resource is shared.
|
// Mutate the filter icon because it needs to be tinted and the resource is shared.
|
||||||
menu.findItem(R.id.action_filter).icon.mutate()
|
menu.findItem(R.id.action_filter).icon.mutate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(query: String) {
|
fun search(query: String) {
|
||||||
this.query = query
|
presenter.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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performSearch() {
|
private fun performSearch() {
|
||||||
searchRelay.call(query)
|
searchRelay.call(presenter.query)
|
||||||
if (query.isNotEmpty()) {
|
if (presenter.query.isNotEmpty()) {
|
||||||
binding.btnGlobalSearch.isVisible = true
|
binding.btnGlobalSearch.isVisible = true
|
||||||
binding.btnGlobalSearch.text =
|
binding.btnGlobalSearch.text =
|
||||||
resources?.getString(R.string.action_global_search_query, query)
|
resources?.getString(R.string.action_global_search_query, presenter.query)
|
||||||
} else {
|
} else {
|
||||||
binding.btnGlobalSearch.isVisible = false
|
binding.btnGlobalSearch.isVisible = false
|
||||||
}
|
}
|
||||||
|
@ -611,4 +569,12 @@ class LibraryController(
|
||||||
selectInverseRelay.call(it)
|
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>() {
|
open class SettingsSearchPresenter : BasePresenter<SettingsSearchController>() {
|
||||||
|
|
||||||
/**
|
|
||||||
* Query from the view.
|
|
||||||
*/
|
|
||||||
var query = ""
|
|
||||||
private set
|
|
||||||
|
|
||||||
val preferences: PreferencesHelper = Injekt.get()
|
val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
|
Reference in a new issue