Global search

This commit is contained in:
Koitharu
2020-05-30 09:48:04 +03:00
parent 0e3aa3f380
commit 9ce43a39c8
21 changed files with 254 additions and 21 deletions

View File

@@ -0,0 +1,19 @@
package org.koitharu.kotatsu.domain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.koin.core.KoinComponent
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.SortOrder
class MangaSearchRepository : KoinComponent {
fun globalSearch(query: String): Flow<List<Manga>> = flow {
val sources = MangaProviderFactory.getSources(false)
for (source in sources) {
val provider = MangaProviderFactory.create(source)
val list = provider.getList(0, query, SortOrder.POPULARITY)
emit(list.take(4))
}
}
}

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.ui.common.list
import android.view.ViewGroup
class ProgressBarAdapter : BaseRecyclerAdapter<Boolean, Unit>() {
var isVisible: Boolean
get() = dataSet.isNotEmpty()
set(value) {
if (value == dataSet.isEmpty()) {
if (value) {
appendItem(true)
} else {
removeItemAt(0)
}
}
}
override fun getExtra(item: Boolean, position: Int) = Unit
override fun onCreateViewHolder(parent: ViewGroup) = ProgressBarHolder(parent)
override fun onGetItemId(item: Boolean) = 1L
}

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.ui.common.list
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_progress.*
import org.koitharu.kotatsu.R
class ProgressBarHolder(parent: ViewGroup) :
BaseViewHolder<Boolean, Unit>(parent, R.layout.item_progress) {
override fun onBind(data: Boolean, extra: Unit) {
progressBar.visibility = if (data) {
View.VISIBLE
} else {
View.INVISIBLE
}
}
}

View File

@@ -32,6 +32,7 @@ import org.koitharu.kotatsu.ui.list.remote.RemoteListFragment
import org.koitharu.kotatsu.ui.list.tracklogs.FeedFragment
import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.search.SearchHelper
import org.koitharu.kotatsu.ui.settings.AppUpdateService
import org.koitharu.kotatsu.ui.settings.SettingsActivity
import org.koitharu.kotatsu.ui.tracker.TrackWorker
@@ -91,8 +92,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
drawerToggle.onConfigurationChanged(newConfig)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.opt_main, menu)
menu.findItem(R.id.action_search)?.let { menuItem ->
SearchHelper.setupSearchView(menuItem)
}
return super.onCreateOptionsMenu(menu)
}

View File

@@ -7,6 +7,7 @@ import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.search.global.GlobalSearchActivity
import org.koitharu.kotatsu.utils.ext.safe
object SearchHelper {
@@ -21,12 +22,26 @@ object SearchHelper {
view.setOnSuggestionListener(SuggestionListener(view))
}
private class QueryListener(private val context: Context, private val source: MangaSource) :
@JvmStatic
fun setupSearchView(menuItem: MenuItem) {
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))
view.setOnSuggestionListener(SuggestionListener(view))
}
private class QueryListener(private val context: Context, private val source: MangaSource? = null) :
SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return if (!query.isNullOrBlank()) {
context.startActivity(SearchActivity.newIntent(context, source, query.trim()))
if (source == null) {
context.startActivity(GlobalSearchActivity.newIntent(context, query.trim()))
} else {
context.startActivity(SearchActivity.newIntent(context, source, query.trim()))
}
MangaSuggestionsProvider.saveQuery(context, query)
true
} else false

View File

@@ -15,13 +15,6 @@ import org.koitharu.kotatsu.ui.list.MangaListView
@InjectViewState
class SearchPresenter : BasePresenter<MangaListView<Unit>>() {
private lateinit var sources: Array<MangaSource>
override fun onFirstViewAttach() {
sources = MangaSource.values()
super.onFirstViewAttach()
}
fun loadList(source: MangaSource, query: String, offset: Int) {
presenterScope.launch {
viewState.onLoadingStateChanged(true)

View File

@@ -0,0 +1,38 @@
package org.koitharu.kotatsu.ui.search.global
import android.content.Context
import android.content.Intent
import android.os.Bundle
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.BaseActivity
class GlobalSearchActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
val query = intent.getStringExtra(EXTRA_QUERY)
if (query == null) {
finish()
return
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
title = query
supportActionBar?.subtitle = getString(R.string.search_results)
supportFragmentManager
.beginTransaction()
.replace(R.id.container, GlobalSearchFragment.newInstance(query))
.commit()
}
companion object {
private const val EXTRA_QUERY = "query"
fun newIntent(context: Context, query: String) =
Intent(context, GlobalSearchActivity::class.java)
.putExtra(EXTRA_QUERY, query)
}
}

View File

@@ -0,0 +1,32 @@
package org.koitharu.kotatsu.ui.search.global
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.ui.list.MangaListFragment
import org.koitharu.kotatsu.utils.ext.withArgs
class GlobalSearchFragment: MangaListFragment<Unit>() {
private val presenter by moxyPresenter(factory = ::GlobalSearchPresenter)
private val query by stringArg(ARG_QUERY)
override fun onRequestMoreItems(offset: Int) {
if (offset == 0) {
presenter.startSearch(query.orEmpty())
}
}
override fun getTitle(): CharSequence? {
return query
}
companion object {
private const val ARG_QUERY = "query"
fun newInstance(query: String) = GlobalSearchFragment().withArgs(1) {
putString(ARG_QUERY, query)
}
}
}

View File

@@ -0,0 +1,49 @@
package org.koitharu.kotatsu.ui.search.global
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEmpty
import kotlinx.coroutines.launch
import moxy.presenterScope
import org.koitharu.kotatsu.domain.MangaSearchRepository
import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.list.MangaListView
import org.koitharu.kotatsu.utils.ext.onFirst
import java.io.IOException
class GlobalSearchPresenter : BasePresenter<MangaListView<Unit>>() {
private lateinit var repository: MangaSearchRepository
override fun onFirstViewAttach() {
repository = MangaSearchRepository()
super.onFirstViewAttach()
}
@Suppress("EXPERIMENTAL_API_USAGE")
fun startSearch(query: String) {
presenterScope.launch {
viewState.onLoadingStateChanged(isLoading = true)
repository.globalSearch(query)
.flowOn(Dispatchers.IO)
.catch { e ->
if (e is IOException) {
viewState.onError(e)
}
}
.onFirst {
viewState.onLoadingStateChanged(isLoading = false)
}
.onEmpty {
viewState.onListChanged(emptyList())
viewState.onLoadingStateChanged(isLoading = false)
}
.collect {
viewState.onListAppended(it)
}
}
}
}

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.utils.ext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
@@ -25,4 +27,14 @@ suspend fun Call.await() = suspendCancellableCoroutine<Response> { cont ->
this.cancel()
}
}
}
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
var isFirstCall = true
return onEach {
if (isFirstCall) {
action(it)
isFirstCall = false
}
}
}