Global search
This commit is contained in:
@@ -4,16 +4,31 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
import org.koitharu.kotatsu.core.model.SortOrder
|
import org.koitharu.kotatsu.core.model.SortOrder
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class MangaSearchRepository : KoinComponent {
|
class MangaSearchRepository : KoinComponent {
|
||||||
|
|
||||||
fun globalSearch(query: String): Flow<List<Manga>> = flow {
|
fun globalSearch(query: String, batchSize: Int = 4): Flow<List<Manga>> = flow {
|
||||||
val sources = MangaProviderFactory.getSources(false)
|
val sources = MangaProviderFactory.getSources(false)
|
||||||
for (source in sources) {
|
val lists = EnumMap<MangaSource, List<Manga>>(MangaSource::class.java)
|
||||||
val provider = MangaProviderFactory.create(source)
|
var i = 0
|
||||||
val list = provider.getList(0, query, SortOrder.POPULARITY)
|
while (true) {
|
||||||
emit(list.take(4))
|
var isEmitted = false
|
||||||
|
for (source in sources) {
|
||||||
|
val list = lists.getOrPut(source) {
|
||||||
|
MangaProviderFactory.create(source).getList(0, query, SortOrder.POPULARITY)
|
||||||
|
}
|
||||||
|
if (i < list.size) {
|
||||||
|
emit(list.subList(i, (i + batchSize).coerceAtMost(list.lastIndex)))
|
||||||
|
isEmitted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += batchSize
|
||||||
|
if (!isEmitted) {
|
||||||
|
return@flow
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,13 @@ package org.koitharu.kotatsu.ui.list.remote
|
|||||||
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
import moxy.ktx.moxyPresenter
|
import moxy.ktx.moxyPresenter
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.model.MangaFilter
|
import org.koitharu.kotatsu.core.model.MangaFilter
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
import org.koitharu.kotatsu.ui.list.MangaListFragment
|
import org.koitharu.kotatsu.ui.list.MangaListFragment
|
||||||
import org.koitharu.kotatsu.ui.search.SearchHelper
|
import org.koitharu.kotatsu.ui.search.SearchActivity
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
|
||||||
class RemoteListFragment : MangaListFragment<Unit>() {
|
class RemoteListFragment : MangaListFragment<Unit>() {
|
||||||
@@ -31,12 +32,17 @@ class RemoteListFragment : MangaListFragment<Unit>() {
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.opt_remote, menu)
|
inflater.inflate(R.menu.opt_remote, menu)
|
||||||
menu.findItem(R.id.action_search)?.let { menuItem ->
|
|
||||||
SearchHelper.setupSearchView(menuItem, source)
|
|
||||||
}
|
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
|
R.id.action_search_internal -> {
|
||||||
|
context?.startActivity(SearchActivity.newIntent(requireContext(), source, null))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val ARG_SOURCE = "provider"
|
private const val ARG_SOURCE = "provider"
|
||||||
|
|||||||
@@ -4,38 +4,60 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import kotlinx.android.synthetic.main.activity_search.*
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
import org.koitharu.kotatsu.ui.common.BaseActivity
|
import org.koitharu.kotatsu.ui.common.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.utils.ext.showKeyboard
|
||||||
|
|
||||||
class SearchActivity : BaseActivity() {
|
class SearchActivity : BaseActivity(), SearchView.OnQueryTextListener {
|
||||||
|
|
||||||
|
private lateinit var source: MangaSource
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_search)
|
setContentView(R.layout.activity_search)
|
||||||
val source = intent.getParcelableExtra<MangaSource>(EXTRA_SOURCE)
|
source = intent.getParcelableExtra(EXTRA_SOURCE) ?: run {
|
||||||
val query = intent.getStringExtra(EXTRA_QUERY)
|
|
||||||
|
|
||||||
if (source == null || query == null) {
|
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val query = intent.getStringExtra(EXTRA_QUERY)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
title = query
|
searchView.queryHint = getString(R.string.search_on_s, source.title)
|
||||||
supportActionBar?.subtitle = getString(R.string.search_results_on_s, source.title)
|
searchView.suggestionsAdapter = MangaSuggestionsProvider.getSuggestionAdapter(this)
|
||||||
supportFragmentManager
|
searchView.setOnSuggestionListener(SearchHelper.SuggestionListener(searchView))
|
||||||
.beginTransaction()
|
searchView.setOnQueryTextListener(this)
|
||||||
.replace(R.id.container, SearchFragment.newInstance(source, query))
|
|
||||||
.commit()
|
if (query.isNullOrBlank()) {
|
||||||
|
searchView.requestFocus()
|
||||||
|
searchView.showKeyboard()
|
||||||
|
} else {
|
||||||
|
searchView.setQuery(query, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
return if (!query.isNullOrBlank()) {
|
||||||
|
title = query
|
||||||
|
supportFragmentManager
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(R.id.container, SearchFragment.newInstance(source, query))
|
||||||
|
.commit()
|
||||||
|
searchView.clearFocus()
|
||||||
|
MangaSuggestionsProvider.saveQuery(this, query)
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?) = false
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val EXTRA_SOURCE = "source"
|
private const val EXTRA_SOURCE = "source"
|
||||||
private const val EXTRA_QUERY = "query"
|
private const val EXTRA_QUERY = "query"
|
||||||
|
|
||||||
fun newIntent(context: Context, source: MangaSource, query: String) =
|
fun newIntent(context: Context, source: MangaSource, query: String?) =
|
||||||
Intent(context, SearchActivity::class.java)
|
Intent(context, SearchActivity::class.java)
|
||||||
.putExtra(EXTRA_SOURCE, source as Parcelable)
|
.putExtra(EXTRA_SOURCE, source as Parcelable)
|
||||||
.putExtra(EXTRA_QUERY, query)
|
.putExtra(EXTRA_QUERY, query)
|
||||||
|
|||||||
@@ -6,22 +6,11 @@ import android.database.Cursor
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.ui.search.global.GlobalSearchActivity
|
import org.koitharu.kotatsu.ui.search.global.GlobalSearchActivity
|
||||||
import org.koitharu.kotatsu.utils.ext.safe
|
import org.koitharu.kotatsu.utils.ext.safe
|
||||||
|
|
||||||
object SearchHelper {
|
object SearchHelper {
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun setupSearchView(menuItem: MenuItem, source: MangaSource) {
|
|
||||||
val view = menuItem.actionView as? SearchView ?: return
|
|
||||||
val context = view.context
|
|
||||||
view.queryHint = context.getString(R.string.search_manga)
|
|
||||||
view.suggestionsAdapter = MangaSuggestionsProvider.getSuggestionAdapter(context)
|
|
||||||
view.setOnQueryTextListener(QueryListener(context, source))
|
|
||||||
view.setOnSuggestionListener(SuggestionListener(view))
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun setupSearchView(menuItem: MenuItem) {
|
fun setupSearchView(menuItem: MenuItem) {
|
||||||
val view = menuItem.actionView as? SearchView ?: return
|
val view = menuItem.actionView as? SearchView ?: return
|
||||||
@@ -32,16 +21,12 @@ object SearchHelper {
|
|||||||
view.setOnSuggestionListener(SuggestionListener(view))
|
view.setOnSuggestionListener(SuggestionListener(view))
|
||||||
}
|
}
|
||||||
|
|
||||||
private class QueryListener(private val context: Context, private val source: MangaSource? = null) :
|
private class QueryListener(private val context: Context) :
|
||||||
SearchView.OnQueryTextListener {
|
SearchView.OnQueryTextListener {
|
||||||
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
return if (!query.isNullOrBlank()) {
|
return if (!query.isNullOrBlank()) {
|
||||||
if (source == null) {
|
context.startActivity(GlobalSearchActivity.newIntent(context, query.trim()))
|
||||||
context.startActivity(GlobalSearchActivity.newIntent(context, query.trim()))
|
|
||||||
} else {
|
|
||||||
context.startActivity(SearchActivity.newIntent(context, source, query.trim()))
|
|
||||||
}
|
|
||||||
MangaSuggestionsProvider.saveQuery(context, query)
|
MangaSuggestionsProvider.saveQuery(context, query)
|
||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
@@ -50,7 +35,7 @@ object SearchHelper {
|
|||||||
override fun onQueryTextChange(newText: String?) = false
|
override fun onQueryTextChange(newText: String?) = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SuggestionListener(private val view: SearchView) :
|
class SuggestionListener(private val view: SearchView) :
|
||||||
SearchView.OnSuggestionListener {
|
SearchView.OnSuggestionListener {
|
||||||
|
|
||||||
override fun onSuggestionSelect(position: Int) = false
|
override fun onSuggestionSelect(position: Int) = false
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class GlobalSearchActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_search)
|
setContentView(R.layout.activity_search_global)
|
||||||
val query = intent.getStringExtra(EXTRA_QUERY)
|
val query = intent.getStringExtra(EXTRA_QUERY)
|
||||||
|
|
||||||
if (query == null) {
|
if (query == null) {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class GlobalSearchPresenter : BasePresenter<MangaListView<Unit>>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFirst {
|
.onFirst {
|
||||||
|
viewState.onListChanged(emptyList())
|
||||||
viewState.onLoadingStateChanged(isLoading = false)
|
viewState.onLoadingStateChanged(isLoading = false)
|
||||||
}
|
}
|
||||||
.onEmpty {
|
.onEmpty {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
android:id="@+id/appbar"
|
android:id="@+id/appbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:fitsSystemWindows="true"
|
|
||||||
android:background="?colorPrimary"
|
android:background="?colorPrimary"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
android:theme="@style/AppToolbarTheme">
|
android:theme="@style/AppToolbarTheme">
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
<androidx.appcompat.widget.Toolbar
|
||||||
@@ -18,7 +18,18 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?android:actionBarSize"
|
android:layout_height="?android:actionBarSize"
|
||||||
app:layout_scrollFlags="scroll|enterAlways"
|
app:layout_scrollFlags="scroll|enterAlways"
|
||||||
app:popupTheme="@style/AppPopupTheme" />
|
app:popupTheme="@style/AppPopupTheme">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SearchView
|
||||||
|
android:id="@+id/searchView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
app:iconifiedByDefault="false"
|
||||||
|
app:searchHintIcon="@null"
|
||||||
|
app:searchIcon="@null" />
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
|||||||
31
app/src/main/res/layout/activity_search_global.xml
Normal file
31
app/src/main/res/layout/activity_search_global.xml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
android:background="?colorPrimary"
|
||||||
|
android:theme="@style/AppToolbarTheme">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?android:actionBarSize"
|
||||||
|
app:layout_scrollFlags="scroll|enterAlways"
|
||||||
|
app:popupTheme="@style/AppPopupTheme" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:id="@id/container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -5,10 +5,8 @@
|
|||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_search_internal"
|
android:id="@+id/action_search_internal"
|
||||||
android:icon="@drawable/ic_search"
|
android:orderInCategory="26"
|
||||||
android:orderInCategory="1"
|
|
||||||
android:title="@string/search"
|
android:title="@string/search"
|
||||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
app:showAsAction="never" />
|
||||||
app:showAsAction="always|collapseActionView" />
|
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
@@ -78,6 +78,7 @@
|
|||||||
<string name="read_mode">Режим чтения</string>
|
<string name="read_mode">Режим чтения</string>
|
||||||
<string name="grid_size">Размер таблицы</string>
|
<string name="grid_size">Размер таблицы</string>
|
||||||
<string name="search_results_on_s">Результаты поиска по %s</string>
|
<string name="search_results_on_s">Результаты поиска по %s</string>
|
||||||
|
<string name="search_on_s">Поиск по %s</string>
|
||||||
<string name="delete_manga">Удалить мангу</string>
|
<string name="delete_manga">Удалить мангу</string>
|
||||||
<string name="text_delete_local_manga">Вы уверены, что хотите удалить \"%s\" с устройства? \nЭто действие нельзя будет отменить.</string>
|
<string name="text_delete_local_manga">Вы уверены, что хотите удалить \"%s\" с устройства? \nЭто действие нельзя будет отменить.</string>
|
||||||
<string name="reader_settings">Настройки чтения</string>
|
<string name="reader_settings">Настройки чтения</string>
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
<string name="read_mode">Read mode</string>
|
<string name="read_mode">Read mode</string>
|
||||||
<string name="grid_size">Grid size</string>
|
<string name="grid_size">Grid size</string>
|
||||||
<string name="search_results_on_s">Search results on %s</string>
|
<string name="search_results_on_s">Search results on %s</string>
|
||||||
|
<string name="search_on_s">Search on %s</string>
|
||||||
<string name="delete_manga">Delete manga</string>
|
<string name="delete_manga">Delete manga</string>
|
||||||
<string name="text_delete_local_manga">Do you really want to delete \"%s\" from your phone\'s local storage? \nThis operation cannot be undone.</string>
|
<string name="text_delete_local_manga">Do you really want to delete \"%s\" from your phone\'s local storage? \nThis operation cannot be undone.</string>
|
||||||
<string name="reader_settings">Reader settings</string>
|
<string name="reader_settings">Reader settings</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user