|  | @@ -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
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -}
 |