diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 13f5cecbf..9cb967580 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -88,6 +88,9 @@
+
{
viewModel.manga.value?.let {
- startActivity(GlobalSearchActivity.newIntent(this, it.title))
+ startActivity(MultiSearchActivity.newIntent(this, it.title))
}
true
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt
index f1d6d3af4..e4ad38d3e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt
@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.list.ui.adapter
+import androidx.core.view.updateLayoutParams
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
@@ -13,6 +14,7 @@ import org.koitharu.kotatsu.databinding.ItemMangaGridBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.search.ui.multi.adapter.ItemSizeResolver
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.referer
@@ -21,6 +23,7 @@ fun mangaGridItemAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener,
+ sizeResolver: ItemSizeResolver?,
) = adapterDelegateViewBinding(
{ inflater, parent -> ItemMangaGridBinding.inflate(inflater, parent, false) }
) {
@@ -34,6 +37,11 @@ fun mangaGridItemAD(
itemView.setOnLongClickListener {
clickListener.onItemLongClick(item.manga, it)
}
+ if (sizeResolver != null) {
+ itemView.updateLayoutParams {
+ width = sizeResolver.cellWidth
+ }
+ }
bind {
binding.textViewTitle.text = item.title
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt
index 2b359a8a9..93f271c0c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt
@@ -18,7 +18,7 @@ class MangaListAdapter(
delegatesManager
.addDelegate(ITEM_TYPE_MANGA_LIST, mangaListItemAD(coil, lifecycleOwner, listener))
.addDelegate(ITEM_TYPE_MANGA_LIST_DETAILED, mangaListDetailedItemAD(coil, lifecycleOwner, listener))
- .addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, listener))
+ .addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, listener, null))
.addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD())
.addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD())
.addDelegate(ITEM_TYPE_DATE, relatedDateItemAD())
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt
index 0d9de41f3..cda127e73 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt
@@ -40,7 +40,7 @@ fun Manga.toGridModel(counter: Int) = MangaGridModel(
suspend fun List.toUi(
mode: ListMode,
countersProvider: CountersProvider,
-): List = when (mode) {
+): List = when (mode) {
ListMode.LIST -> map { it.toListModel(countersProvider.getCounter(it.id)) }
ListMode.DETAILED_LIST -> map { it.toListDetailedModel(countersProvider.getCounter(it.id)) }
ListMode.GRID -> map { it.toGridModel(countersProvider.getCounter(it.id)) }
@@ -58,7 +58,7 @@ suspend fun > List.toUi(
fun List.toUi(
mode: ListMode,
-): List = when (mode) {
+): List = when (mode) {
ListMode.LIST -> map { it.toListModel(0) }
ListMode.DETAILED_LIST -> map { it.toListDetailedModel(0) }
ListMode.GRID -> map { it.toGridModel(0) }
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
index b57158069..e11527071 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
@@ -45,7 +45,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity
-import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity
+import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
@@ -268,7 +268,7 @@ class MainActivity :
if (source != null) {
startActivity(SearchActivity.newIntent(this, source, query))
} else {
- startActivity(GlobalSearchActivity.newIntent(this, query))
+ startActivity(MultiSearchActivity.newIntent(this, query))
}
searchSuggestionViewModel.saveQuery(query)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt
index 1d1fb43fc..b06e06bfd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt
@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.search.ui.SearchViewModel
-import org.koitharu.kotatsu.search.ui.global.GlobalSearchViewModel
+import org.koitharu.kotatsu.search.ui.multi.MultiSearchViewModel
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
val searchModule
@@ -16,11 +16,7 @@ val searchModule
factory { MangaSearchRepository(get(), get(), androidContext(), get()) }
factory { MangaSuggestionsProvider.createSuggestions(androidContext()) }
- viewModel { params ->
- SearchViewModel(MangaRepository(params[0]), params[1], get())
- }
- viewModel { query ->
- GlobalSearchViewModel(query.get(), get(), get())
- }
+ viewModel { params -> SearchViewModel(MangaRepository(params[0]), params[1], get()) }
viewModel { SearchSuggestionViewModel(get(), get()) }
+ viewModel { params -> MultiSearchViewModel(params[0], get()) }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt
index 151bb2b33..4904ab34a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt
@@ -14,16 +14,16 @@ import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
-import org.koitharu.kotatsu.databinding.ActivitySearchGlobalBinding
+import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
-class MangaListActivity : BaseActivity() {
+class MangaListActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(ActivitySearchGlobalBinding.inflate(layoutInflater))
+ setContentView(ActivityContainerBinding.inflate(layoutInflater))
val tags = intent.getParcelableExtra(EXTRA_TAGS)?.tags ?: run {
finishAfterTransition()
return
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchActivity.kt
deleted file mode 100644
index ad23f0b98..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchActivity.kt
+++ /dev/null
@@ -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() {
-
- 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 {
- 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)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchFragment.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchFragment.kt
deleted file mode 100644
index 185de3d25..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchFragment.kt
+++ /dev/null
@@ -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 {
- 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)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt
deleted file mode 100644
index 3511f3b3d..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt
+++ /dev/null
@@ -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?>(null)
- private val hasNextPage = MutableStateFlow(false)
- private val listError = MutableStateFlow(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(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)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt
new file mode 100644
index 000000000..5bd119a83
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt
@@ -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(), MangaListListener, ActionMode.Callback {
+
+ private val viewModel by viewModel {
+ 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 {
+ 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 {
+ 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 {
+ 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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt
new file mode 100644
index 000000000..eb8d71a7e
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt
@@ -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,
+) : 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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt
new file mode 100644
index 000000000..40b6a8619
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt
@@ -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>(emptyList())
+ private val loadingData = MutableStateFlow(false)
+ private var listError = MutableStateFlow(null)
+
+ val query = MutableLiveData(initialQuery)
+ val list: LiveData> = 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): Set {
+ val result = HashSet(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 {
+ 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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/ItemSizeResolver.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/ItemSizeResolver.kt
new file mode 100644
index 000000000..a5f5d3f72
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/ItemSizeResolver.kt
@@ -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()
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt
new file mode 100644
index 000000000..35afb49d4
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt
@@ -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,
+ sizeResolver: ItemSizeResolver,
+ selectionDecoration: MangaSelectionDecoration,
+) : AsyncListDifferDelegationAdapter(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() {
+
+ 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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt
new file mode 100644
index 000000000..ee58933e8
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt
@@ -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,
+ itemClickListener: OnListItemClickListener,
+) = adapterDelegateViewBinding(
+ { 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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt
index 021c77859..586e40eef 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils.ext
import android.app.Activity
import android.graphics.Rect
import android.view.View
+import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.core.view.children
import androidx.recyclerview.widget.LinearLayoutManager
@@ -138,4 +139,19 @@ val RecyclerView.isScrolledToTop: Boolean
}
val holder = findViewHolderForAdapterPosition(0)
return holder != null && holder.itemView.top >= 0
- }
\ No newline at end of file
+ }
+
+fun ViewGroup.findViewsByType(clazz: Class): Sequence {
+ if (childCount == 0) {
+ return emptySequence()
+ }
+ return sequence {
+ for (view in children) {
+ if (clazz.isInstance(view)) {
+ yield(clazz.cast(view)!!)
+ } else if (view is ViewGroup && view.childCount != 0) {
+ yieldAll(view.findViewsByType(clazz))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_search_global.xml b/app/src/main/res/layout/activity_container.xml
similarity index 100%
rename from app/src/main/res/layout/activity_search_global.xml
rename to app/src/main/res/layout/activity_container.xml
diff --git a/app/src/main/res/layout/activity_search_multi.xml b/app/src/main/res/layout/activity_search_multi.xml
new file mode 100644
index 000000000..23b613556
--- /dev/null
+++ b/app/src/main/res/layout/activity_search_multi.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_list_group.xml b/app/src/main/res/layout/item_list_group.xml
new file mode 100644
index 000000000..3296973f5
--- /dev/null
+++ b/app/src/main/res/layout/item_list_group.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_manga_grid.xml b/app/src/main/res/layout/item_manga_grid.xml
index c0cbb05b9..5174b647a 100644
--- a/app/src/main/res/layout/item_manga_grid.xml
+++ b/app/src/main/res/layout/item_manga_grid.xml
@@ -27,8 +27,9 @@
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:elegantTextHeight="false"
android:ellipsize="end"
- android:maxLines="2"
+ android:lines="2"
android:padding="8dp"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="?android:attr/textColorPrimary"