mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +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
						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