New global search activity
parent
e4b29b3ff9
commit
5d881ca154
@ -1,55 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.search.ui.global
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
|
||||||
import org.koitharu.kotatsu.databinding.ActivitySearchGlobalBinding
|
|
||||||
|
|
||||||
class GlobalSearchActivity : BaseActivity<ActivitySearchGlobalBinding>() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(ActivitySearchGlobalBinding.inflate(layoutInflater))
|
|
||||||
val query = intent.getStringExtra(EXTRA_QUERY)
|
|
||||||
|
|
||||||
if (query == null) {
|
|
||||||
finishAfterTransition()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
title = query
|
|
||||||
supportActionBar?.subtitle = getString(R.string.search_results)
|
|
||||||
supportFragmentManager
|
|
||||||
.beginTransaction()
|
|
||||||
.replace(R.id.container, GlobalSearchFragment.newInstance(query))
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
|
||||||
with(binding.toolbar) {
|
|
||||||
updatePadding(
|
|
||||||
left = insets.left,
|
|
||||||
right = insets.right
|
|
||||||
)
|
|
||||||
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
topMargin = insets.top
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val EXTRA_QUERY = "query"
|
|
||||||
|
|
||||||
fun newIntent(context: Context, query: String) =
|
|
||||||
Intent(context, GlobalSearchActivity::class.java)
|
|
||||||
.putExtra(EXTRA_QUERY, query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.search.ui.global
|
|
||||||
|
|
||||||
import android.view.Menu
|
|
||||||
import androidx.appcompat.view.ActionMode
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
import org.koin.core.parameter.parametersOf
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
|
||||||
import org.koitharu.kotatsu.utils.ext.stringArgument
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
|
|
||||||
class GlobalSearchFragment : MangaListFragment() {
|
|
||||||
|
|
||||||
override val viewModel by viewModel<GlobalSearchViewModel> {
|
|
||||||
parametersOf(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val query by stringArgument(ARG_QUERY)
|
|
||||||
|
|
||||||
override fun onScrolledToEnd() = Unit
|
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
||||||
mode.menuInflater.inflate(R.menu.mode_remote, menu)
|
|
||||||
return super.onCreateActionMode(mode, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val ARG_QUERY = "query"
|
|
||||||
|
|
||||||
fun newInstance(query: String) = GlobalSearchFragment().withArgs(1) {
|
|
||||||
putString(ARG_QUERY, query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.search.ui.global
|
|
||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.*
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
|
||||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
|
||||||
|
|
||||||
class GlobalSearchViewModel(
|
|
||||||
private val query: String,
|
|
||||||
private val repository: MangaSearchRepository,
|
|
||||||
settings: AppSettings
|
|
||||||
) : MangaListViewModel(settings) {
|
|
||||||
|
|
||||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
|
||||||
private val hasNextPage = MutableStateFlow(false)
|
|
||||||
private val listError = MutableStateFlow<Throwable?>(null)
|
|
||||||
private var searchJob: Job? = null
|
|
||||||
|
|
||||||
override val content = combine(
|
|
||||||
mangaList,
|
|
||||||
createListModeFlow(),
|
|
||||||
listError,
|
|
||||||
hasNextPage
|
|
||||||
) { list, mode, error, hasNext ->
|
|
||||||
when {
|
|
||||||
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
|
|
||||||
list == null -> listOf(LoadingState)
|
|
||||||
list.isEmpty() -> listOf(
|
|
||||||
EmptyState(
|
|
||||||
icon = R.drawable.ic_book_search,
|
|
||||||
textPrimary = R.string.nothing_found,
|
|
||||||
textSecondary = R.string.text_search_holder_secondary,
|
|
||||||
actionStringRes = 0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else -> {
|
|
||||||
val result = ArrayList<ListModel>(list.size + 1)
|
|
||||||
list.toUi(result, mode)
|
|
||||||
when {
|
|
||||||
error != null -> result += error.toErrorFooter()
|
|
||||||
hasNext -> result += LoadingFooter
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
|
||||||
|
|
||||||
init {
|
|
||||||
onRefresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRetry() {
|
|
||||||
onRefresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRefresh() {
|
|
||||||
searchJob?.cancel()
|
|
||||||
searchJob = repository.globalSearch(query)
|
|
||||||
.catch { e ->
|
|
||||||
listError.value = e
|
|
||||||
loadingCounter.reset()
|
|
||||||
}.onStart {
|
|
||||||
mangaList.value = null
|
|
||||||
listError.value = null
|
|
||||||
loadingCounter.increment()
|
|
||||||
hasNextPage.value = true
|
|
||||||
}.onEmpty {
|
|
||||||
mangaList.value = emptyList()
|
|
||||||
}.onCompletion {
|
|
||||||
loadingCounter.reset()
|
|
||||||
hasNextPage.value = false
|
|
||||||
}.onFirst {
|
|
||||||
loadingCounter.reset()
|
|
||||||
}.onEach {
|
|
||||||
mangaList.value = mangaList.value?.plus(it) ?: listOf(it)
|
|
||||||
}.launchIn(viewModelScope + Dispatchers.Default)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,183 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.multi
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.databinding.ActivitySearchMultiBinding
|
||||||
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
|
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||||
|
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
|
||||||
|
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||||
|
import org.koitharu.kotatsu.search.ui.multi.adapter.ItemSizeResolver
|
||||||
|
import org.koitharu.kotatsu.search.ui.multi.adapter.MultiSearchAdapter
|
||||||
|
import org.koitharu.kotatsu.utils.ShareHelper
|
||||||
|
import org.koitharu.kotatsu.utils.ext.findViewsByType
|
||||||
|
|
||||||
|
class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaListListener, ActionMode.Callback {
|
||||||
|
|
||||||
|
private val viewModel by viewModel<MultiSearchViewModel> {
|
||||||
|
parametersOf(intent.getStringExtra(EXTRA_QUERY).orEmpty())
|
||||||
|
}
|
||||||
|
private lateinit var adapter: MultiSearchAdapter
|
||||||
|
private lateinit var selectionDecoration: MangaSelectionDecoration
|
||||||
|
private var actionMode: ActionMode? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(ActivitySearchMultiBinding.inflate(layoutInflater))
|
||||||
|
|
||||||
|
val itemCLickListener = object : OnListItemClickListener<MultiSearchListModel> {
|
||||||
|
override fun onItemClick(item: MultiSearchListModel, view: View) {
|
||||||
|
startActivity(SearchActivity.newIntent(view.context, item.source, viewModel.query.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val sizeResolver = ItemSizeResolver(resources, get())
|
||||||
|
selectionDecoration = MangaSelectionDecoration(this)
|
||||||
|
adapter = MultiSearchAdapter(
|
||||||
|
lifecycleOwner = this,
|
||||||
|
coil = get(),
|
||||||
|
listener = this,
|
||||||
|
itemClickListener = itemCLickListener,
|
||||||
|
sizeResolver = sizeResolver,
|
||||||
|
selectionDecoration = selectionDecoration,
|
||||||
|
)
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
|
|
||||||
|
supportActionBar?.run {
|
||||||
|
setDisplayHomeAsUpEnabled(true)
|
||||||
|
setSubtitle(R.string.search_results)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.query.observe(this) { title = it }
|
||||||
|
viewModel.list.observe(this) { adapter.items = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
|
with(binding.toolbar) {
|
||||||
|
updatePadding(
|
||||||
|
left = insets.left,
|
||||||
|
right = insets.right,
|
||||||
|
)
|
||||||
|
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
topMargin = insets.top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.recyclerView.updatePadding(
|
||||||
|
bottom = insets.bottom,
|
||||||
|
left = insets.left,
|
||||||
|
right = insets.right,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(item: Manga, view: View) {
|
||||||
|
if (selectionDecoration.checkedItemsCount != 0) {
|
||||||
|
selectionDecoration.toggleItemChecked(item.id)
|
||||||
|
if (selectionDecoration.checkedItemsCount == 0) {
|
||||||
|
actionMode?.finish()
|
||||||
|
} else {
|
||||||
|
actionMode?.invalidate()
|
||||||
|
invalidateItemDecorations()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val intent = DetailsActivity.newIntent(this, item)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemLongClick(item: Manga, view: View): Boolean {
|
||||||
|
if (actionMode == null) {
|
||||||
|
actionMode = startSupportActionMode(this)
|
||||||
|
}
|
||||||
|
return actionMode?.also {
|
||||||
|
selectionDecoration.setItemIsChecked(item.id, true)
|
||||||
|
invalidateItemDecorations()
|
||||||
|
it.invalidate()
|
||||||
|
} != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRetryClick(error: Throwable) {
|
||||||
|
viewModel.doSearch(viewModel.query.value.orEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTagRemoveClick(tag: MangaTag) = Unit
|
||||||
|
|
||||||
|
override fun onFilterClick() = Unit
|
||||||
|
|
||||||
|
override fun onEmptyActionClick() = Unit
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
mode.menuInflater.inflate(R.menu.mode_remote, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
mode.title = selectionDecoration.checkedItemsCount.toString()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
R.id.action_share -> {
|
||||||
|
ShareHelper(this).shareMangaLinks(collectSelectedItems())
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.action_favourite -> {
|
||||||
|
FavouriteCategoriesBottomSheet.show(supportFragmentManager, collectSelectedItems())
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.action_save -> {
|
||||||
|
DownloadService.confirmAndStart(this, collectSelectedItems())
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
|
selectionDecoration.clearSelection()
|
||||||
|
invalidateItemDecorations()
|
||||||
|
actionMode = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun collectSelectedItems(): Set<Manga> {
|
||||||
|
return viewModel.getItems(selectionDecoration.checkedItemsIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidateItemDecorations() {
|
||||||
|
binding.recyclerView.findViewsByType(RecyclerView::class.java).forEach {
|
||||||
|
it.invalidateItemDecorations()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val EXTRA_QUERY = "query"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, query: String) =
|
||||||
|
Intent(context, MultiSearchActivity::class.java)
|
||||||
|
.putExtra(EXTRA_QUERY, query)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.multi
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
class MultiSearchListModel(
|
||||||
|
val source: MangaSource,
|
||||||
|
val list: List<MangaItemModel>,
|
||||||
|
) : ListModel {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as MultiSearchListModel
|
||||||
|
|
||||||
|
if (source != other.source) return false
|
||||||
|
if (list != other.list) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = source.hashCode()
|
||||||
|
result = 31 * result + list.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.multi
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
|
||||||
|
private const val MAX_PARALLELISM = 4
|
||||||
|
|
||||||
|
class MultiSearchViewModel(
|
||||||
|
initialQuery: String,
|
||||||
|
private val settings: AppSettings,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private var searchJob: Job? = null
|
||||||
|
private val listData = MutableStateFlow<List<MultiSearchListModel>>(emptyList())
|
||||||
|
private val loadingData = MutableStateFlow(false)
|
||||||
|
private var listError = MutableStateFlow<Throwable?>(null)
|
||||||
|
|
||||||
|
val query = MutableLiveData(initialQuery)
|
||||||
|
val list: LiveData<List<ListModel>> = combine(
|
||||||
|
listData,
|
||||||
|
loadingData,
|
||||||
|
listError,
|
||||||
|
) { list, loading, error ->
|
||||||
|
when {
|
||||||
|
list.isEmpty() -> listOf(
|
||||||
|
when {
|
||||||
|
loading -> LoadingState
|
||||||
|
error != null -> error.toErrorState(canRetry = true)
|
||||||
|
else -> EmptyState(
|
||||||
|
icon = R.drawable.ic_book_search,
|
||||||
|
textPrimary = R.string.nothing_found,
|
||||||
|
textSecondary = R.string.text_search_holder_secondary,
|
||||||
|
actionStringRes = 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
loading -> list + LoadingFooter
|
||||||
|
else -> list
|
||||||
|
}
|
||||||
|
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||||
|
|
||||||
|
init {
|
||||||
|
doSearch(initialQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getItems(ids: Set<Long>): Set<Manga> {
|
||||||
|
val result = HashSet<Manga>(ids.size)
|
||||||
|
listData.value.forEach { x ->
|
||||||
|
for (item in x.list) {
|
||||||
|
if (item.id in ids) {
|
||||||
|
result.add(item.manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doSearch(q: String) {
|
||||||
|
val prevJob = searchJob
|
||||||
|
searchJob = launchJob(Dispatchers.Default) {
|
||||||
|
prevJob?.cancelAndJoin()
|
||||||
|
try {
|
||||||
|
listError.value = null
|
||||||
|
listData.value = emptyList()
|
||||||
|
loadingData.value = true
|
||||||
|
query.postValue(q)
|
||||||
|
val errors = searchImpl(q)
|
||||||
|
listError.value = errors.firstOrNull()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
listError.value = e
|
||||||
|
} finally {
|
||||||
|
loadingData.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun searchImpl(q: String): List<Throwable> {
|
||||||
|
val sources = settings.getMangaSources(includeHidden = false)
|
||||||
|
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
|
||||||
|
return coroutineScope {
|
||||||
|
sources.map { source ->
|
||||||
|
async(dispatcher) {
|
||||||
|
runCatching {
|
||||||
|
val list = MangaRepository(source).getList(offset = 0, query = q)
|
||||||
|
// .sortedBy { x -> x.title.levenshteinDistance(q) }
|
||||||
|
.toUi(ListMode.GRID)
|
||||||
|
if (list.isNotEmpty()) {
|
||||||
|
val item = MultiSearchListModel(source, list)
|
||||||
|
listData.update { x -> x + item }
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTraceDebug()
|
||||||
|
}.exceptionOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll().filterNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.multi.adapter
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
|
||||||
|
class ItemSizeResolver(resources: Resources, settings: AppSettings) {
|
||||||
|
|
||||||
|
private val scaleFactor = settings.gridSize / 100f
|
||||||
|
private val gridWidth = resources.getDimension(R.dimen.preferred_grid_width)
|
||||||
|
|
||||||
|
val cellWidth: Int
|
||||||
|
get() = (gridWidth * scaleFactor).roundToInt()
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.multi.adapter
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.*
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.search.ui.multi.MultiSearchListModel
|
||||||
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
|
class MultiSearchAdapter(
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
coil: ImageLoader,
|
||||||
|
listener: MangaListListener,
|
||||||
|
itemClickListener: OnListItemClickListener<MultiSearchListModel>,
|
||||||
|
sizeResolver: ItemSizeResolver,
|
||||||
|
selectionDecoration: MangaSelectionDecoration,
|
||||||
|
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
val pool = RecycledViewPool()
|
||||||
|
delegatesManager
|
||||||
|
.addDelegate(
|
||||||
|
searchResultsAD(
|
||||||
|
sharedPool = pool,
|
||||||
|
lifecycleOwner = lifecycleOwner,
|
||||||
|
coil = coil,
|
||||||
|
sizeResolver = sizeResolver,
|
||||||
|
selectionDecoration = selectionDecoration,
|
||||||
|
listener = listener,
|
||||||
|
itemClickListener = itemClickListener,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addDelegate(loadingStateAD())
|
||||||
|
.addDelegate(loadingFooterAD())
|
||||||
|
.addDelegate(emptyStateListAD(listener))
|
||||||
|
.addDelegate(errorStateListAD(listener))
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||||
|
return when {
|
||||||
|
oldItem is MultiSearchListModel && newItem is MultiSearchListModel -> {
|
||||||
|
oldItem.source == newItem.source
|
||||||
|
}
|
||||||
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||||
|
return Intrinsics.areEqual(oldItem, newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.multi.adapter
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemListGroupBinding
|
||||||
|
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.search.ui.multi.MultiSearchListModel
|
||||||
|
|
||||||
|
fun searchResultsAD(
|
||||||
|
sharedPool: RecycledViewPool,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
coil: ImageLoader,
|
||||||
|
sizeResolver: ItemSizeResolver,
|
||||||
|
selectionDecoration: MangaSelectionDecoration,
|
||||||
|
listener: OnListItemClickListener<Manga>,
|
||||||
|
itemClickListener: OnListItemClickListener<MultiSearchListModel>,
|
||||||
|
) = adapterDelegateViewBinding<MultiSearchListModel, ListModel, ItemListGroupBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) }
|
||||||
|
) {
|
||||||
|
|
||||||
|
binding.recyclerView.setRecycledViewPool(sharedPool)
|
||||||
|
val adapter = ListDelegationAdapter(
|
||||||
|
mangaGridItemAD(coil, lifecycleOwner, listener, sizeResolver)
|
||||||
|
)
|
||||||
|
binding.recyclerView.addItemDecoration(selectionDecoration)
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing)
|
||||||
|
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
|
||||||
|
val eventListener = AdapterDelegateClickListenerAdapter(this, itemClickListener)
|
||||||
|
itemView.setOnClickListener(eventListener)
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.textViewTitle.text = item.source.title
|
||||||
|
adapter.items = item.list
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:layout_scrollFlags="scroll|enterAlways" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingVertical="@dimen/grid_spacing_outer">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="@dimen/grid_spacing"
|
||||||
|
android:gravity="center_vertical|start"
|
||||||
|
android:padding="@dimen/grid_spacing"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
|
||||||
|
tools:text="@tools:sample/lorem[2]" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="@dimen/grid_spacing"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
Loading…
Reference in New Issue