From 2911fe7a1aa8b94d8e95e99648e3bce0d23fa0a7 Mon Sep 17 00:00:00 2001 From: Antoine Gaudreau Simard Date: Sat, 27 Mar 2021 16:38:41 -0400 Subject: [PATCH] Add onPause\onResume persistence to searchView. Fixes issue #3627 (#4494) * 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 --- .../ui/base/controller/BaseController.kt | 2 +- .../controller/SearchableNucleusController.kt | 196 ++++++++++++++++++ .../ui/base/presenter/BasePresenter.kt | 5 + .../ui/browse/source/SourceController.kt | 61 ++---- .../source/browse/BrowseSourceController.kt | 32 +-- .../source/browse/BrowseSourcePresenter.kt | 10 +- .../globalsearch/GlobalSearchController.kt | 73 +++---- .../globalsearch/GlobalSearchPresenter.kt | 6 - .../tachiyomi/ui/library/LibraryController.kt | 66 ++---- .../setting/search/SettingsSearchPresenter.kt | 6 - 10 files changed, 280 insertions(+), 177 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SearchableNucleusController.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt index 3c520efe63..c868af2343 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt @@ -121,7 +121,7 @@ abstract class BaseController(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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SearchableNucleusController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SearchableNucleusController.kt new file mode 100644 index 0000000000..ddeadcb33f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SearchableNucleusController.kt @@ -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> +(bundle: Bundle? = null) : NucleusController(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 + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt index 185b01d73e..d80fccb1ea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt @@ -12,6 +12,11 @@ open class BasePresenter : RxPresenter() { lateinit var presenterScope: CoroutineScope + /** + * Query from the view where applicable + */ + var query: String = "" + override fun onCreate(savedState: Bundle?) { try { super.onCreate(savedState) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt index 228938eb60..b04522311a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt @@ -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(), + SearchableNucleusController(), 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() - .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() + ) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt index 7fcee032a3..59620e7894 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt @@ -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(bundle), + SearchableNucleusController(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() - .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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt index 440cbad8b8..4ae85e1182 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt index d277eff5e8..e36e4d75c9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt @@ -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(), +) : SearchableNucleusController(), 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() - .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 } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchPresenter.kt index 36933c50f6..3a6d1bf9ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchPresenter.kt @@ -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. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index e58827f1d8..dda4e664b3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -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(bundle), +) : SearchableNucleusController(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() + } + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchPresenter.kt index acb595359f..0d03d7561b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchPresenter.kt @@ -12,12 +12,6 @@ import uy.kohesive.injekt.api.get */ open class SettingsSearchPresenter : BasePresenter() { - /** - * Query from the view. - */ - var query = "" - private set - val preferences: PreferencesHelper = Injekt.get() override fun onCreate(savedState: Bundle?) {