|
@@ -1,216 +0,0 @@
|
|
|
-package eu.kanade.tachiyomi.ui.base.controller
|
|
|
-
|
|
|
-import android.app.Activity
|
|
|
-import android.os.Bundle
|
|
|
-import android.text.style.CharacterStyle
|
|
|
-import android.view.Menu
|
|
|
-import android.view.MenuInflater
|
|
|
-import android.view.MenuItem
|
|
|
-import androidx.appcompat.widget.SearchView
|
|
|
-import androidx.core.text.getSpans
|
|
|
-import androidx.core.widget.doAfterTextChanged
|
|
|
-import androidx.viewbinding.ViewBinding
|
|
|
-import eu.kanade.tachiyomi.R
|
|
|
-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,
|
|
|
- ) {
|
|
|
- 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
|
|
|
-
|
|
|
- // Remove formatting from pasted text
|
|
|
- val searchAutoComplete: SearchView.SearchAutoComplete = searchView.findViewById(
|
|
|
- R.id.search_src_text,
|
|
|
- )
|
|
|
- searchAutoComplete.doAfterTextChanged { editable ->
|
|
|
- editable?.getSpans<CharacterStyle>()?.forEach { editable.removeSpan(it) }
|
|
|
- }
|
|
|
-
|
|
|
- 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)
|
|
|
- }
|
|
|
-
|
|
|
- // 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?) {
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Workaround for buggy menu item layout after expanding/collapsing an expandable item like a SearchView.
|
|
|
- * This method should be removed when fixed upstream.
|
|
|
- * Issue link: https://issuetracker.google.com/issues/37657375
|
|
|
- */
|
|
|
- private var expandActionViewFromInteraction = false
|
|
|
-
|
|
|
- private fun MenuItem.fixExpand(onExpand: ((MenuItem) -> Boolean)? = null, onCollapse: ((MenuItem) -> Boolean)? = null) {
|
|
|
- setOnActionExpandListener(
|
|
|
- object : MenuItem.OnActionExpandListener {
|
|
|
- override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
|
|
- return onExpand?.invoke(item) ?: true
|
|
|
- }
|
|
|
-
|
|
|
- override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
|
|
- activity?.invalidateOptionsMenu()
|
|
|
-
|
|
|
- return onCollapse?.invoke(item) ?: true
|
|
|
- }
|
|
|
- },
|
|
|
- )
|
|
|
-
|
|
|
- if (expandActionViewFromInteraction) {
|
|
|
- expandActionViewFromInteraction = false
|
|
|
- expandActionView()
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 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
|
|
|
- */
|
|
|
- private fun invalidateMenuOnExpand(): Boolean {
|
|
|
- return if (expandActionViewFromInteraction) {
|
|
|
- activity?.invalidateOptionsMenu()
|
|
|
- setCurrentSearchViewState(SearchViewState.FOCUSED) // we are technically focused here
|
|
|
- false
|
|
|
- } else {
|
|
|
- true
|
|
|
- }
|
|
|
- }
|
|
|
-}
|