mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	* 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:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							14c114756d
						
					
				
				
					commit
					2911fe7a1a
				
			@@ -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
 | 
			
		||||
            }
 | 
			
		||||
            .launchIn(viewScope)
 | 
			
		||||
        optionsMenuSearchItem = menu.findItem(R.id.action_search)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 New Issue
	
	Block a user