diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 138c8f127c..bbafae4217 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -284,7 +284,7 @@ class PreferencesHelper(val context: Context) { fun hiddenSources() = flowPrefs.getStringSet("hidden_catalogues", mutableSetOf()) - fun pinnedCatalogues() = rxPrefs.getStringSet("pinned_catalogues", emptySet()) + fun pinnedCatalogues() = flowPrefs.getStringSet("pinned_catalogues", mutableSetOf()) fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/PreMigrationController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/PreMigrationController.kt index 2a685e0391..fe926bf68d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/PreMigrationController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/PreMigrationController.kt @@ -177,7 +177,7 @@ class PreMigrationController(bundle: Bundle? = null) : val enabledSources = if (item.itemId == R.id.action_match_enabled) { prefs.hiddenSources().get().mapNotNull { it.toLongOrNull() } } else { - prefs.pinnedCatalogues().get()?.mapNotNull { it.toLongOrNull() } ?: emptyList() + prefs.pinnedCatalogues().get().mapNotNull { it.toLongOrNull() } ?: emptyList() } val items = adapter?.currentItems?.toList() ?: return true items.forEach { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/BrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/BrowseController.kt index 0e60f31db3..5b796740c7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/BrowseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/BrowseController.kt @@ -24,7 +24,6 @@ import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.databinding.BrowseControllerBinding import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.LocalSource @@ -390,7 +389,7 @@ class BrowseController : } private fun pinCatalogue(source: Source, isPinned: Boolean) { - val current = preferences.pinnedCatalogues().getOrDefault() + val current = preferences.pinnedCatalogues().get() if (isPinned) { preferences.pinnedCatalogues().set(current - source.id.toString()) } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourcePresenter.kt index 1bf94d74d2..342a55a294 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourcePresenter.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.source import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.SourceManager @@ -57,7 +56,7 @@ class SourcePresenter( private fun loadSources() { scope.launch { val pinnedSources = mutableListOf() - val pinnedCatalogues = preferences.pinnedCatalogues().getOrDefault() + val pinnedCatalogues = preferences.pinnedCatalogues().get() val map = TreeMap> { d1, d2 -> // Catalogues without a lang defined will be placed at the end @@ -105,7 +104,7 @@ class SourcePresenter( private fun getLastUsedSource(value: Long): SourceItem? { return (sourceManager.get(value) as? CatalogueSource)?.let { source -> - val pinnedCatalogues = preferences.pinnedCatalogues().getOrDefault() + val pinnedCatalogues = preferences.pinnedCatalogues().get() val isPinned = source.id.toString() in pinnedCatalogues if (isPinned) null else SourceItem(source, null, isPinned) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index ca17c12137..a5e865f0dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -12,7 +12,6 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar -import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R @@ -29,6 +28,7 @@ import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.source.BrowseController +import eu.kanade.tachiyomi.ui.source.global_search.GlobalSearchController import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.util.addOrRemoveToFavorites import eu.kanade.tachiyomi.util.system.connectivityManager @@ -37,6 +37,7 @@ import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.inflate import eu.kanade.tachiyomi.util.view.scrollViewWith +import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.util.view.visible @@ -44,12 +45,8 @@ import eu.kanade.tachiyomi.util.view.visibleIf import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.EmptyView -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers import timber.log.Timber import uy.kohesive.injekt.injectLazy -import java.util.concurrent.TimeUnit /** * Controller to manage the catalogues available in the app. @@ -105,11 +102,6 @@ open class BrowseSourceController(bundle: Bundle) : */ private var recycler: RecyclerView? = null - /** - * Subscription for the search view. - */ - private var searchViewSubscription: Subscription? = null - /** * Endless loading item. */ @@ -124,7 +116,7 @@ open class BrowseSourceController(bundle: Bundle) : } override fun createPresenter(): BrowseSourcePresenter { - return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY)) + return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY)) } override fun createBinding(inflater: LayoutInflater) = BrowseSourceControllerBinding.inflate(inflater) @@ -142,8 +134,6 @@ open class BrowseSourceController(bundle: Bundle) : } override fun onDestroyView(view: View) { - searchViewSubscription?.unsubscribe() - searchViewSubscription = null adapter = null snack = null recycler = null @@ -222,31 +212,40 @@ open class BrowseSourceController(bundle: Bundle) : val searchView = searchItem.actionView as SearchView val query = presenter.query - if (!query.isBlank()) { + if (query.isNotBlank()) { searchItem.expandActionView() searchView.setQuery(query, true) searchView.clearFocus() } - val searchEventsObservable = searchView.queryTextChangeEvents() - .skip(1) - .filter { router.backstack.lastOrNull()?.controller() == this@BrowseSourceController } - .share() - val writingObservable = searchEventsObservable - .filter { !it.isSubmitted } - .debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) - val submitObservable = searchEventsObservable - .filter { it.isSubmitted } +// val searchEventsObservable = searchView.queryTextChangeEvents() +// .skip(1) +// .filter { router.backstack.lastOrNull()?.controller() == this@BrowseSourceController } +// .share() +// val writingObservable = searchEventsObservable +// .filter { !it.isSubmitted } +// .debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) +// val submitObservable = searchEventsObservable +// .filter { it.isSubmitted } +// +// searchViewSubscription?.unsubscribe() +// searchViewSubscription = Observable.merge(writingObservable, submitObservable) +// .map { it.queryText().toString() } +// .subscribeUntilDestroy { searchWithQuery(it) } - searchViewSubscription?.unsubscribe() - searchViewSubscription = Observable.merge(writingObservable, submitObservable) - .map { it.queryText().toString() } - .subscribeUntilDestroy { searchWithQuery(it) } + setOnQueryTextChangeListener(searchView, onlyOnSubmit = true, hideKbOnSubmit = false) { + searchWithQuery(it ?: "") + true + } searchItem.fixExpand( onExpand = { invalidateMenuOnExpand() }, onCollapse = { - searchWithQuery("") + if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller() is GlobalSearchController) { + router.popController(this) + } else { + searchWithQuery("") + } true } ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt index e5d4ab3546..1dd9d55dac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt @@ -48,6 +48,7 @@ import uy.kohesive.injekt.api.get */ open class BrowseSourcePresenter( sourceId: Long, + searchQuery: String? = null, sourceManager: SourceManager = Injekt.get(), val db: DatabaseHelper = Injekt.get(), private val prefs: PreferencesHelper = Injekt.get(), @@ -117,6 +118,10 @@ open class BrowseSourcePresenter( private var scope = CoroutineScope(Job() + Dispatchers.IO) + init { + query = searchQuery ?: "" + } + override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchAdapter.kt index c5e4c3c986..de739c8ffe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchAdapter.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.os.Parcelable import android.util.SparseArray import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.source.CatalogueSource /** * Adapter that holds the search cards. @@ -13,6 +14,8 @@ import eu.davidea.flexibleadapter.FlexibleAdapter class GlobalSearchAdapter(val controller: GlobalSearchController) : FlexibleAdapter(null, controller, true) { + val titleClickListener: OnTitleClickListener = controller + /** * Bundle where the view state of the holders is saved. */ @@ -67,6 +70,10 @@ class GlobalSearchAdapter(val controller: GlobalSearchController) : } } + interface OnTitleClickListener { + fun onTitleClick(source: CatalogueSource) + } + private companion object { const val HOLDER_BUNDLE_KEY = "holder_bundle" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchController.kt index 4eae8b19b3..3eb667b639 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchController.kt @@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaDetailsController +import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController import eu.kanade.tachiyomi.util.addOrRemoveToFavorites import eu.kanade.tachiyomi.util.view.activityBinding import eu.kanade.tachiyomi.util.view.scrollViewWith @@ -33,9 +34,10 @@ import uy.kohesive.injekt.injectLazy */ open class GlobalSearchController( protected val initialQuery: String? = null, - protected val extensionFilter: String? = null + val extensionFilter: String? = null ) : NucleusController(), FloatingSearchInterface, + GlobalSearchAdapter.OnTitleClickListener, GlobalSearchCardAdapter.OnMangaClickListener { /** @@ -82,6 +84,11 @@ open class GlobalSearchController( return GlobalSearchPresenter(initialQuery, extensionFilter) } + override fun onTitleClick(source: CatalogueSource) { + preferences.lastUsedCatalogueSource().set(source.id) + router.pushController(BrowseSourceController(source, presenter.query).withFadeTransaction()) + } + /** * Called when manga in global search is clicked, opens manga. * @@ -180,6 +187,9 @@ open class GlobalSearchController( customTitle = view.context?.getString(R.string.loading) setTitle() } + binding.recycler.post { + binding.recycler.scrollToPosition(0) + } } override fun onDestroyView(view: View) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchHolder.kt index 13070ecc0e..112820dd4c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchHolder.kt @@ -1,9 +1,13 @@ package eu.kanade.tachiyomi.ui.source.global_search import android.view.View +import androidx.core.view.isVisible import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.databinding.SourceGlobalSearchControllerCardBinding +import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import eu.kanade.tachiyomi.ui.migration.SearchController +import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.visible @@ -30,6 +34,15 @@ class GlobalSearchHolder(view: View, val adapter: GlobalSearchAdapter) : binding.recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context, androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL, false) binding.recycler.adapter = mangaAdapter + + binding.titleMoreIcon.isVisible = adapter.controller !is SearchController && adapter.controller.extensionFilter == null + if (binding.titleMoreIcon.isVisible) { + binding.titleWrapper.setOnClickListener { + adapter.getItem(bindingAdapterPosition)?.let { + adapter.titleClickListener.onTitleClick(it.source) + } + } + } } /** @@ -46,6 +59,8 @@ class GlobalSearchHolder(view: View, val adapter: GlobalSearchAdapter) : // Set Title with country code if available. binding.title.text = titlePrefix + source.name + langSuffix + binding.subtitle.isVisible = source !is LocalSource + binding.subtitle.text = LocaleHelper.getDisplayName(source.lang) when { results == null -> { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchPresenter.kt index 8bfcdae4ef..fb71a2a0aa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchPresenter.kt @@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source @@ -25,6 +24,7 @@ import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy +import java.util.Date /** * Presenter of [GlobalSearchController] @@ -32,7 +32,7 @@ import uy.kohesive.injekt.injectLazy * * @param sourceManager manages the different sources. * @param db manages the database calls. - * @param preferencesHelper manages the preference calls. + * @param preferences manages the preference calls. */ open class GlobalSearchPresenter( private val initialQuery: String? = "", @@ -40,7 +40,7 @@ open class GlobalSearchPresenter( private val sourcesToUse: List? = null, val sourceManager: SourceManager = Injekt.get(), val db: DatabaseHelper = Injekt.get(), - private val preferencesHelper: PreferencesHelper = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get(), private val coverCache: CoverCache = Injekt.get() ) : BasePresenter() { @@ -60,6 +60,8 @@ open class GlobalSearchPresenter( */ private var fetchSourcesSubscription: Subscription? = null + private var loadTime = hashMapOf() + /** * Subject which fetches image of given manga. */ @@ -104,16 +106,16 @@ open class GlobalSearchPresenter( * @return list containing enabled sources. */ protected open fun getEnabledSources(): List { - val languages = preferencesHelper.enabledLanguages().get() - val hiddenCatalogues = preferencesHelper.hiddenSources().get() - val pinnedCatalogues = preferencesHelper.pinnedCatalogues().getOrDefault() + val languages = preferences.enabledLanguages().get() + val hiddenCatalogues = preferences.hiddenSources().get() + val pinnedCatalogues = preferences.pinnedCatalogues().get() val list = sourceManager.getCatalogueSources() .filter { it.lang in languages } .filterNot { it.id.toString() in hiddenCatalogues } .sortedBy { "(${it.lang}) ${it.name}" } - return if (preferencesHelper.onlySearchPinned().get()) { + return if (preferences.onlySearchPinned().get()) { list.filter { it.id.toString() in pinnedCatalogues } } else { list.sortedBy { it.id.toString() !in pinnedCatalogues } @@ -176,6 +178,8 @@ open class GlobalSearchPresenter( val initialItems = sources.map { createCatalogueSearchItem(it, null) } var items = initialItems + val pinnedSourceIds = preferences.pinnedCatalogues().get() + fetchSourcesSubscription?.unsubscribe() fetchSourcesSubscription = Observable.from(sources).flatMap( { source -> @@ -197,6 +201,9 @@ open class GlobalSearchPresenter( } // Convert to local manga. .doOnNext { fetchImage(it, source) } // Load manga covers. .map { + if (it.isNotEmpty() && !loadTime.containsKey(source.id)) { + loadTime[source.id] = Date().time + } createCatalogueSearchItem( source, it.map { GlobalSearchMangaItem(it) } @@ -204,10 +211,22 @@ open class GlobalSearchPresenter( } }, 5 - ).observeOn(AndroidSchedulers.mainThread()) + ) + .observeOn(AndroidSchedulers.mainThread()) // Update matching source with the obtained results .map { result -> - items.map { item -> if (item.source == result.source) result else item } + items + .map { item -> if (item.source == result.source) result else item } + .sortedWith( + compareBy( + // Bubble up sources that actually have results + { it.results.isNullOrEmpty() }, + // Same as initial sort, i.e. pinned first then alphabetically + { it.source.id.toString() !in pinnedSourceIds }, + { loadTime[it.source.id] ?: 0L }, + { "${it.source.name.toLowerCase()} (${it.source.lang})" } + ) + ) } // Update current state .doOnNext { items = it } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt index 0e7c81a9a8..cbfebae19a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt @@ -42,6 +42,7 @@ import kotlin.random.Random fun Controller.setOnQueryTextChangeListener( searchView: SearchView, onlyOnSubmit: Boolean = false, + hideKbOnSubmit: Boolean = true, f: (text: String?) -> Boolean ) { searchView.setOnQueryTextListener( @@ -57,10 +58,12 @@ fun Controller.setOnQueryTextChangeListener( override fun onQueryTextSubmit(query: String?): Boolean { if (router.backstack.lastOrNull()?.controller() == this@setOnQueryTextChangeListener) { - val imm = - activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - ?: return f(query) - imm.hideSoftInputFromWindow(searchView.windowToken, 0) + if (hideKbOnSubmit) { + val imm = + activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + ?: return f(query) + imm.hideSoftInputFromWindow(searchView.windowToken, 0) + } return f(query) } return true diff --git a/app/src/main/res/layout/source_global_search_controller_card.xml b/app/src/main/res/layout/source_global_search_controller_card.xml index 322723251e..bc8bc6760c 100644 --- a/app/src/main/res/layout/source_global_search_controller_card.xml +++ b/app/src/main/res/layout/source_global_search_controller_card.xml @@ -6,18 +6,56 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - + android:background="?attr/selectableItemBackground"> + + + + + + +