From 66644d55a46c801df8038b086c3f68a19ac268e6 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 22 Sep 2024 17:42:28 +0300 Subject: [PATCH] Search manga with filters --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 5 +- .../alternatives/ui/AlternativesActivity.kt | 12 +- .../org/koitharu/kotatsu/core/model/Manga.kt | 27 ++++ .../parcelable/ParcelableMangaListFilter.kt | 53 +++++++ .../kotatsu/core/os/AppShortcutManager.kt | 2 +- .../kotatsu/core/ui/widgets/ChipsView.kt | 8 +- .../koitharu/kotatsu/core/util/ext/Bundle.kt | 26 ++++ .../kotatsu/core/util/ext/Collections.kt | 6 + .../kotatsu/details/ui/DetailsActivity.kt | 10 +- .../ui/worker/DownloadNotificationFactory.kt | 2 +- .../kotatsu/explore/ui/ExploreFragment.kt | 4 +- .../kotatsu/filter/ui/FilterCoordinator.kt | 4 +- .../kotatsu/filter/ui/FilterHeaderFragment.kt | 19 ++- .../kotatsu/filter/ui/FilterHeaderProducer.kt | 21 ++- .../kotatsu/list/ui/MangaListFragment.kt | 4 +- .../list/ui/preview/PreviewFragment.kt | 8 +- .../kotatsu/local/ui/LocalListFragment.kt | 2 + .../kotatsu/local/ui/LocalListViewModel.kt | 8 +- .../koitharu/kotatsu/main/ui/MainActivity.kt | 5 +- .../remotelist/ui/MangaSearchMenuProvider.kt | 69 ++++++++++ .../remotelist/ui/RemoteListFragment.kt | 50 +------ .../remotelist/ui/RemoteListViewModel.kt | 3 - .../kotatsu/search/ui/MangaListActivity.kt | 59 ++++---- .../kotatsu/search/ui/SearchActivity.kt | 97 ------------- .../kotatsu/search/ui/SearchFragment.kt | 37 ----- .../kotatsu/search/ui/SearchViewModel.kt | 129 ------------------ .../search/ui/multi/MultiSearchActivity.kt | 12 +- .../sources/catalog/SourcesCatalogActivity.kt | 2 +- app/src/main/res/layout/activity_search.xml | 41 ------ app/src/main/res/menu/opt_list_remote.xml | 8 +- app/src/main/res/menu/opt_search.xml | 14 ++ app/src/main/res/values/strings.xml | 1 + 33 files changed, 309 insertions(+), 441 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaListFilter.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/MangaSearchMenuProvider.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchActivity.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchFragment.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt delete mode 100644 app/src/main/res/layout/activity_search.xml create mode 100644 app/src/main/res/menu/opt_search.xml diff --git a/app/build.gradle b/app/build.gradle index e8093e261..0daab315f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -83,7 +83,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:f2354957e6') { + implementation('com.github.KotatsuApp:kotatsu-parsers:f410df40f1') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c0f146385..31d3ffc16 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -68,7 +68,7 @@ + android:value="org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity" /> - (), override fun onItemClick(item: MangaAlternativeModel, view: View) { when (view.id) { - R.id.chip_source -> startActivity(SearchActivity.newIntent(this, item.manga.source, viewModel.manga.title)) + R.id.chip_source -> startActivity( + MangaListActivity.newIntent( + this, + item.manga.source, + MangaListFilter(query = viewModel.manga.title), + ), + ) + R.id.button_migrate -> confirmMigration(item.manga) else -> startActivity(DetailsActivity.newIntent(this, item.manga)) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt index aa5bd3330..78c4bd3da 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt @@ -1,10 +1,13 @@ package org.koitharu.kotatsu.core.model import android.net.Uri +import android.text.SpannableStringBuilder import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.collection.MutableObjectIntMap import androidx.core.os.LocaleListCompat +import androidx.core.text.buildSpannedString +import androidx.core.text.strikeThrough import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.util.ext.iterator import org.koitharu.kotatsu.details.ui.model.ChapterListItem @@ -12,6 +15,7 @@ import org.koitharu.kotatsu.parsers.model.ContentRating import org.koitharu.kotatsu.parsers.model.Demographic import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.util.formatSimple import org.koitharu.kotatsu.parsers.util.mapToSet @@ -152,3 +156,26 @@ fun Manga.chaptersCount(): Int { } return max } + +fun MangaListFilter.getSummary() = buildSpannedString { + if (!query.isNullOrEmpty()) { + append(query) + if (tags.isNotEmpty() || tagsExclude.isNotEmpty()) { + append(' ') + append('(') + appendTagsSummary(this@getSummary) + append(')') + } + } else { + appendTagsSummary(this@getSummary) + } +} + +private fun SpannableStringBuilder.appendTagsSummary(filter: MangaListFilter) { + filter.tags.joinTo(this) { it.title } + if (filter.tagsExclude.isNotEmpty()) { + strikeThrough { + filter.tagsExclude.joinTo(this) { it.title } + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaListFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaListFilter.kt new file mode 100644 index 000000000..6b0eb85ed --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaListFilter.kt @@ -0,0 +1,53 @@ +package org.koitharu.kotatsu.core.model.parcelable + +import android.os.Parcel +import android.os.Parcelable +import kotlinx.parcelize.Parceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.TypeParceler +import org.koitharu.kotatsu.core.util.ext.readEnumSet +import org.koitharu.kotatsu.core.util.ext.readParcelableCompat +import org.koitharu.kotatsu.core.util.ext.readSerializableCompat +import org.koitharu.kotatsu.core.util.ext.writeEnumSet +import org.koitharu.kotatsu.parsers.model.ContentRating +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.Demographic +import org.koitharu.kotatsu.parsers.model.MangaListFilter +import org.koitharu.kotatsu.parsers.model.MangaState + +object MangaListFilterParceler : Parceler { + + override fun MangaListFilter.write(parcel: Parcel, flags: Int) { + parcel.writeString(query) + parcel.writeParcelable(ParcelableMangaTags(tags), 0) + parcel.writeParcelable(ParcelableMangaTags(tagsExclude), 0) + parcel.writeSerializable(locale) + parcel.writeSerializable(originalLocale) + parcel.writeEnumSet(states) + parcel.writeEnumSet(contentRating) + parcel.writeEnumSet(types) + parcel.writeEnumSet(demographics) + parcel.writeInt(year) + parcel.writeInt(yearFrom) + parcel.writeInt(yearTo) + } + + override fun create(parcel: Parcel) = MangaListFilter( + query = parcel.readString(), + tags = parcel.readParcelableCompat()?.tags.orEmpty(), + tagsExclude = parcel.readParcelableCompat()?.tags.orEmpty(), + locale = parcel.readSerializableCompat(), + originalLocale = parcel.readSerializableCompat(), + states = parcel.readEnumSet().orEmpty(), + contentRating = parcel.readEnumSet().orEmpty(), + types = parcel.readEnumSet().orEmpty(), + demographics = parcel.readEnumSet().orEmpty(), + year = parcel.readInt(), + yearFrom = parcel.readInt(), + yearTo = parcel.readInt(), + ) +} + +@Parcelize +@TypeParceler +data class ParcelableMangaListFilter(val filter: MangaListFilter) : Parcelable diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/os/AppShortcutManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/AppShortcutManager.kt index 71aad8f3f..414a8a24f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/os/AppShortcutManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/AppShortcutManager.kt @@ -180,7 +180,7 @@ class AppShortcutManager @Inject constructor( .setLongLabel(title) .setIcon(icon) .setLongLived(true) - .setIntent(MangaListActivity.newIntent(context, source)) + .setIntent(MangaListActivity.newIntent(context, source, null)) .build() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt index 873746bce..bb0f7972d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt @@ -36,11 +36,6 @@ class ChipsView @JvmOverloads constructor( children.forEach { it.isClickable = isChipClickable } } var onChipCloseClickListener: OnChipCloseClickListener? = null - set(value) { - field = value - val isCloseIconVisible = value != null - children.forEach { (it as? Chip)?.isCloseIconVisible = isCloseIconVisible } - } init { val ta = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0) @@ -98,6 +93,7 @@ class ChipsView @JvmOverloads constructor( @ColorRes val tint: Int = 0, val isChecked: Boolean = false, val isDropdown: Boolean = false, + val isCloseable: Boolean = false, val data: Any? = null, ) @@ -139,7 +135,7 @@ class ChipsView @JvmOverloads constructor( isChipIconVisible = true } isCheckedIconVisible = model.isChecked - isCloseIconVisible = if (onChipCloseClickListener != null || model.isDropdown) { + isCloseIconVisible = if (model.isCloseable || model.isDropdown) { setCloseIconResource( if (model.isDropdown) R.drawable.ic_expand_more else materialR.drawable.ic_m3_chip_close, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Bundle.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Bundle.kt index 8933ae4db..34f3f440e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Bundle.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Bundle.kt @@ -12,6 +12,7 @@ import androidx.core.os.BundleCompat import androidx.core.os.ParcelCompat import androidx.lifecycle.SavedStateHandle import java.io.Serializable +import java.util.EnumSet // https://issuetracker.google.com/issues/240585930 @@ -53,6 +54,31 @@ inline fun Bundle.requireSerializable(key: String): T } } +fun > Parcel.writeEnumSet(set: Set?) { + if (set == null) { + writeValue(null) + } else { + val array = IntArray(set.size) + set.forEachIndexed { i, e -> array[i] = e.ordinal } + writeIntArray(array) + } +} + +inline fun > Parcel.readEnumSet(): Set? = readEnumSet(E::class.java) + +fun > Parcel.readEnumSet(cls: Class): Set? { + val array = createIntArray() ?: return null + if (array.isEmpty()) { + return emptySet() + } + val enumValues = cls.enumConstants ?: return null + val set = EnumSet.noneOf(cls) + array.forEach { e -> + set.add(enumValues[e]) + } + return set +} + fun SavedStateHandle.require(key: String): T { return checkNotNull(get(key)) { "Value $key not found in SavedStateHandle or has a wrong type" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt index edcb7845a..109d605a4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt @@ -25,6 +25,12 @@ fun Collection.asArrayList(): ArrayList = if (this is ArrayList<*>) { ArrayList(this) } +fun > Set.asEnumSet(cls: Class): EnumSet = if (this is EnumSet<*>) { + this as EnumSet +} else { + EnumSet.noneOf(cls).apply { addAll(this@asEnumSet) } +} + fun Map.findKeyByValue(value: V): K? { for ((k, v) in entries) { if (v == value) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 0d689804f..6a3dfab94 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -98,13 +98,13 @@ import org.koitharu.kotatsu.list.ui.model.MangaListModel import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.util.ellipsize import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet import org.koitharu.kotatsu.search.ui.MangaListActivity -import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet import javax.inject.Inject import com.google.android.material.R as materialR @@ -213,10 +213,10 @@ class DetailsActivity : R.id.chip_author -> { val manga = viewModel.manga.value ?: return startActivity( - SearchActivity.newIntent( + MangaListActivity.newIntent( context = v.context, source = manga.source, - query = manga.author ?: return, + filter = MangaListFilter(query = manga.author), ), ) } @@ -227,6 +227,7 @@ class DetailsActivity : MangaListActivity.newIntent( context = v.context, source = manga.source, + filter = null, ), ) } @@ -286,7 +287,8 @@ class DetailsActivity : override fun onChipClick(chip: Chip, data: Any?) { val tag = data as? MangaTag ?: return - startActivity(MangaListActivity.newIntent(this, setOf(tag))) + // TODO dialog + startActivity(MangaListActivity.newIntent(this, tag.source, MangaListFilter(tags = setOf(tag)))) } override fun onLongClick(v: View): Boolean = when (v.id) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt index 05cdbd931..03f23e5cf 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt @@ -265,7 +265,7 @@ class DownloadNotificationFactory @AssistedInject constructor( if (manga != null) { DetailsActivity.newIntent(context, manga) } else { - MangaListActivity.newIntent(context, LocalMangaSource) + MangaListActivity.newIntent(context, LocalMangaSource, null) }, PendingIntent.FLAG_CANCEL_CURRENT, false, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt index 743253b59..2f4ea0c24 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt @@ -126,7 +126,7 @@ class ExploreFragment : override fun onClick(v: View) { val intent = when (v.id) { - R.id.button_local -> MangaListActivity.newIntent(v.context, LocalMangaSource) + R.id.button_local -> MangaListActivity.newIntent(v.context, LocalMangaSource, null) R.id.button_bookmarks -> AllBookmarksActivity.newIntent(v.context) R.id.button_more -> SuggestionsActivity.newIntent(v.context) R.id.button_downloads -> Intent(v.context, DownloadsActivity::class.java) @@ -144,7 +144,7 @@ class ExploreFragment : if (sourceSelectionController?.onItemClick(item.id) == true) { return } - val intent = MangaListActivity.newIntent(view.context, item.source) + val intent = MangaListActivity.newIntent(view.context, item.source, null) startActivity(intent) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt index 059546880..0bdbae1f4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt @@ -59,8 +59,8 @@ class FilterCoordinator @Inject constructor( private val currentSortOrder = MutableStateFlow(repository.defaultSortOrder) private val availableSortOrders = repository.sortOrders - private val capabilities = repository.filterCapabilities private val filterOptions = SuspendLazy { repository.getFilterOptions() } + val capabilities = repository.filterCapabilities val mangaSource: MangaSource get() = repository.source @@ -69,7 +69,7 @@ class FilterCoordinator @Inject constructor( get() = !currentListFilter.value.isEmpty() val query: StateFlow = currentListFilter.map { it.query } - .stateIn(coroutineScope, SharingStarted.Lazily, null) + .stateIn(coroutineScope, SharingStarted.Eagerly, null) val sortOrder: StateFlow> = currentSortOrder.map { selected -> FilterProperty( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt index e34b872ad..57662e12c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt @@ -22,7 +22,8 @@ import javax.inject.Inject import com.google.android.material.R as materialR @AndroidEntryPoint -class FilterHeaderFragment : BaseFragment(), ChipsView.OnChipClickListener { +class FilterHeaderFragment : BaseFragment(), ChipsView.OnChipClickListener, + ChipsView.OnChipCloseClickListener { @Inject lateinit var filterHeaderProducer: FilterHeaderProducer @@ -37,6 +38,7 @@ class FilterHeaderFragment : BaseFragment(), ChipsV override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) { super.onViewBindingCreated(binding, savedInstanceState) binding.chipsTags.onChipClickListener = this + binding.chipsTags.onChipCloseClickListener = this filterHeaderProducer.observeHeader(filter) .flowOn(Dispatchers.Default) .observe(viewLifecycleOwner, ::onDataChanged) @@ -45,11 +47,16 @@ class FilterHeaderFragment : BaseFragment(), ChipsV override fun onWindowInsetsChanged(insets: Insets) = Unit override fun onChipClick(chip: Chip, data: Any?) { - val tag = data as? MangaTag - if (tag == null) { - TagsCatalogSheet.show(parentFragmentManager, isExcludeTag = false) - } else { - filter.toggleTag(tag, !chip.isChecked) + when (data) { + is MangaTag -> filter.toggleTag(data, !chip.isChecked) + is String -> Unit + null -> TagsCatalogSheet.show(parentFragmentManager, isExcludeTag = false) + } + } + + override fun onChipCloseClick(chip: Chip, data: Any?) { + when (data) { + is String -> filter.setQuery(null) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt index 02ea5169f..f43811745 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt @@ -2,25 +2,25 @@ package org.koitharu.kotatsu.filter.ui import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.mapLatest import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel import org.koitharu.kotatsu.filter.ui.model.FilterProperty import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.search.domain.MangaSearchRepository -import java.util.LinkedList import javax.inject.Inject +import com.google.android.material.R as materialR class FilterHeaderProducer @Inject constructor( private val searchRepository: MangaSearchRepository, ) { fun observeHeader(filterCoordinator: FilterCoordinator): Flow { - return filterCoordinator.tags.mapLatest { + return combine(filterCoordinator.tags, filterCoordinator.query) { tags, query -> createChipsList( source = filterCoordinator.mangaSource, - property = it, + property = tags, + query = query, limit = 8, ) }.combine(filterCoordinator.observe()) { chipList, snapshot -> @@ -35,6 +35,7 @@ class FilterHeaderProducer @Inject constructor( private suspend fun createChipsList( source: MangaSource, property: FilterProperty, + query: String?, limit: Int, ): List { val selectedTags = property.selectedItems.toMutableSet() @@ -49,7 +50,7 @@ class FilterHeaderProducer @Inject constructor( if (tags.isEmpty() && selectedTags.isEmpty()) { return emptyList() } - val result = LinkedList() + val result = ArrayDeque(tags.size + selectedTags.size + 1) for (tag in tags) { val model = ChipsView.ChipModel( title = tag.title, @@ -70,6 +71,16 @@ class FilterHeaderProducer @Inject constructor( ) result.addFirst(model) } + if (!query.isNullOrEmpty()) { + result.addFirst( + ChipsView.ChipModel( + title = query, + icon = materialR.drawable.abc_ic_search_api_material, + isCloseable = true, + data = query, + ), + ) + } return result } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 3a9f02614..9184b0f8a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -60,6 +60,7 @@ import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver import org.koitharu.kotatsu.main.ui.MainActivity import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.search.ui.MangaListActivity @@ -164,7 +165,8 @@ abstract class MangaListFragment : override fun onTagClick(manga: Manga, tag: MangaTag, view: View) { if (selectionController?.onItemClick(manga.id) != true) { - val intent = MangaListActivity.newIntent(context ?: return, setOf(tag)) + // TODO dialog + val intent = MangaListActivity.newIntent(view.context, tag.source, MangaListFilter(tags = setOf(tag))) startActivity(intent) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt index 4512e2488..132f69382 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt @@ -31,10 +31,10 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.image.ui.ImageActivity import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.search.ui.MangaListActivity -import org.koitharu.kotatsu.search.ui.SearchActivity import javax.inject.Inject @AndroidEntryPoint @@ -83,10 +83,10 @@ class PreviewFragment : BaseFragment(), View.OnClickList } R.id.textView_author -> startActivity( - SearchActivity.newIntent( + MangaListActivity.newIntent( context = v.context, source = manga.source, - query = manga.author ?: return, + filter = MangaListFilter(query = manga.author), ), ) @@ -107,7 +107,7 @@ class PreviewFragment : BaseFragment(), View.OnClickList val tag = data as? MangaTag ?: return val filter = (activity as? FilterCoordinator.Owner)?.filterCoordinator if (filter == null) { - startActivity(MangaListActivity.newIntent(requireContext(), setOf(tag))) + startActivity(MangaListActivity.newIntent(chip.context, tag.source, MangaListFilter(tags = setOf(tag)))) } else { filter.toggleTag(tag, true) closeSelf() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt index 44794afa4..41b7307b0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt @@ -26,6 +26,7 @@ import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment import org.koitharu.kotatsu.list.ui.MangaListFragment +import org.koitharu.kotatsu.remotelist.ui.MangaSearchMenuProvider import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.settings.storage.RequestStorageManagerPermissionContract import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity @@ -61,6 +62,7 @@ class LocalListFragment : MangaListFragment(), FilterCoordinator.Owner { override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { super.onViewBindingCreated(binding, savedInstanceState) addMenuProvider(LocalListMenuProvider(binding.root.context, this::onEmptyActionClick)) + addMenuProvider(MangaSearchMenuProvider(filterCoordinator, viewModel)) viewModel.onMangaRemoved.observeEvent(viewLifecycleOwner) { onItemRemoved() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index ac8993e9e..08217f7fa 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -16,7 +16,6 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.domain.ExploreRepository import org.koitharu.kotatsu.filter.ui.FilterCoordinator -import org.koitharu.kotatsu.filter.ui.FilterHeaderProducer import org.koitharu.kotatsu.list.domain.MangaListMapper import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel @@ -41,7 +40,6 @@ class LocalListViewModel @Inject constructor( exploreRepository: ExploreRepository, @LocalStorageChanges private val localStorageChanges: SharedFlow, private val localStorageManager: LocalStorageManager, - filterHeaderProducer: FilterHeaderProducer, sourcesRepository: MangaSourcesRepository, ) : RemoteListViewModel( savedStateHandle, @@ -109,8 +107,10 @@ class LocalListViewModel @Inject constructor( } } - override fun createEmptyState(canResetFilter: Boolean): EmptyState { - return EmptyState( + override fun createEmptyState(canResetFilter: Boolean): EmptyState = if (canResetFilter) { + super.createEmptyState(canResetFilter) + } else { + EmptyState( icon = R.drawable.ic_empty_local, textPrimary = R.string.text_local_holder_primary, textSecondary = R.string.text_local_holder_secondary, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt index 0db277b0b..3360ab460 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -60,6 +60,7 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder @@ -265,7 +266,7 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav } override fun onTagClick(tag: MangaTag) { - startActivity(MangaListActivity.newIntent(this, setOf(tag))) + startActivity(MangaListActivity.newIntent(this, tag.source, MangaListFilter(tags = setOf(tag)))) } override fun onQueryChanged(query: String) { @@ -277,7 +278,7 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav } override fun onSourceClick(source: MangaSource) { - val intent = MangaListActivity.newIntent(this, source) + val intent = MangaListActivity.newIntent(this, source, null) startActivity(intent) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/MangaSearchMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/MangaSearchMenuProvider.kt new file mode 100644 index 000000000..08dc09970 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/MangaSearchMenuProvider.kt @@ -0,0 +1,69 @@ +package org.koitharu.kotatsu.remotelist.ui + +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.appcompat.widget.SearchView +import androidx.core.view.MenuProvider +import androidx.core.view.inputmethod.EditorInfoCompat +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.util.ReversibleAction +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.filter.ui.FilterCoordinator +import org.koitharu.kotatsu.list.ui.MangaListViewModel +import org.koitharu.kotatsu.parsers.model.MangaListFilter + +class MangaSearchMenuProvider( + private val filter: FilterCoordinator, + private val viewModel: MangaListViewModel, +) : MenuProvider, MenuItem.OnActionExpandListener, SearchView.OnQueryTextListener { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_search, menu) + val menuItem = menu.findItem(R.id.action_search) + menuItem.setOnActionExpandListener(this) + val searchView = menuItem.actionView as SearchView + searchView.setOnQueryTextListener(this) + searchView.queryHint = menuItem.title + } + + override fun onPrepareMenu(menu: Menu) { + super.onPrepareMenu(menu) + menu.findItem(R.id.action_search)?.isVisible = filter.capabilities.isSearchSupported + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false + + override fun onQueryTextSubmit(query: String?): Boolean { + val snapshot = filter.snapshot() + if (!query.isNullOrEmpty() && !filter.capabilities.isSearchWithFiltersSupported && snapshot.listFilter.hasNonSearchOptions()) { + filter.set(MangaListFilter(query = query)) + viewModel.onActionDone.call( + ReversibleAction(R.string.filter_search_warning) { filter.set(snapshot.listFilter) }, + ) + } else { + filter.setQuery(query) + } + return true + } + + override fun onQueryTextChange(newText: String?): Boolean = false + + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + (item.actionView as? SearchView)?.run { + post { adjustSearchView() } + } + return true + } + + override fun onMenuItemActionCollapse(item: MenuItem): Boolean = true + + private fun SearchView.adjustSearchView() { + imeOptions = if (viewModel.isIncognitoModeEnabled) { + imeOptions or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING + } else { + imeOptions and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv() + } + setQuery(filter.query.value, false) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index b8d7ff663..5776e1444 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -6,9 +6,7 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.appcompat.view.ActionMode -import androidx.appcompat.widget.SearchView import androidx.core.view.MenuProvider -import androidx.core.view.inputmethod.EditorInfoCompat import androidx.fragment.app.viewModels import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint @@ -28,9 +26,7 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment import org.koitharu.kotatsu.list.ui.MangaListFragment -import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.settings.SettingsActivity @AndroidEntryPoint @@ -44,6 +40,7 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner { override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { super.onViewBindingCreated(binding, savedInstanceState) addMenuProvider(RemoteListMenuProvider()) + addMenuProvider(MangaSearchMenuProvider(filterCoordinator, viewModel)) viewModel.isRandomLoading.observe(viewLifecycleOwner, MenuInvalidator(requireActivity())) viewModel.onOpenManga.observeEvent(viewLifecycleOwner) { startActivity(DetailsActivity.newIntent(binding.root.context, it)) @@ -86,19 +83,10 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner { .show() } - private inner class RemoteListMenuProvider : - MenuProvider, - SearchView.OnQueryTextListener, - MenuItem.OnActionExpandListener { + private inner class RemoteListMenuProvider : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.opt_list_remote, menu) - val searchMenuItem = menu.findItem(R.id.action_search) - searchMenuItem.setOnActionExpandListener(this) - val searchView = searchMenuItem.actionView as SearchView - searchView.setOnQueryTextListener(this) - searchView.setIconifiedByDefault(false) - searchView.queryHint = searchMenuItem.title } override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { @@ -127,43 +115,9 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner { override fun onPrepareMenu(menu: Menu) { super.onPrepareMenu(menu) - menu.findItem(R.id.action_search)?.isVisible = viewModel.isSearchAvailable menu.findItem(R.id.action_random)?.isEnabled = !viewModel.isRandomLoading.value menu.findItem(R.id.action_filter_reset)?.isVisible = filterCoordinator.isFilterApplied } - - override fun onQueryTextSubmit(query: String?): Boolean { - if (query.isNullOrEmpty()) { - return false - } - val intent = SearchActivity.newIntent( - context = this@RemoteListFragment.context ?: return false, - source = viewModel.source, - query = query, - ) - startActivity(intent) - return true - } - - override fun onQueryTextChange(newText: String?): Boolean = false - - override fun onMenuItemActionExpand(item: MenuItem): Boolean { - (activity as? AppBarOwner)?.appBar?.setExpanded(false, true) - (item.actionView as? SearchView)?.run { - imeOptions = if (viewModel.isIncognitoModeEnabled) { - imeOptions or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING - } else { - imeOptions and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv() - } - } - return true - } - - override fun onMenuItemActionCollapse(item: MenuItem): Boolean { - val searchView = (item.actionView as? SearchView) ?: return false - searchView.setQuery("", false) - return true - } } companion object { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 30906a4c5..3727776b1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -70,9 +70,6 @@ open class RemoteListViewModel @Inject constructor( private var loadingJob: Job? = null private var randomJob: Job? = null - val isSearchAvailable: Boolean - get() = repository.filterCapabilities.isSearchSupported - val browserUrl: String? get() = (repository as? ParserMangaRepository)?.domain?.let { "https://$it" } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt index d7bd95c92..989828470 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt @@ -23,10 +23,11 @@ import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.LocalMangaSource import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.model.getSummary import org.koitharu.kotatsu.core.model.getTitle import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags +import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaListFilter import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.model.titleRes @@ -45,7 +46,7 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.util.isNullOrEmpty import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import kotlin.math.absoluteValue import com.google.android.material.R as materialR @@ -63,28 +64,23 @@ class MangaListActivity : "Cannot find FilterCoordinator.Owner fragment in ${supportFragmentManager.fragments}" }.filterCoordinator - private var source: MangaSource? = null + private lateinit var source: MangaSource override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityMangaListBinding.inflate(layoutInflater)) - val tags = intent.getParcelableExtraCompat(EXTRA_TAGS)?.tags + val filter = intent.getParcelableExtraCompat(EXTRA_FILTER)?.filter + source = MangaSource(intent.getStringExtra(EXTRA_SOURCE)) supportActionBar?.setDisplayHomeAsUpEnabled(true) if (viewBinding.containerFilterHeader != null) { viewBinding.appbar.addOnOffsetChangedListener(this) } - source = intent.getStringExtra(EXTRA_SOURCE)?.let(::MangaSource) ?: tags?.firstOrNull()?.source - val src = source - if (src == null) { - finishAfterTransition() - } else { - viewBinding.buttonOrder?.setOnClickListener(this) - title = src.getTitle(this) - initList(src, tags) - } + viewBinding.buttonOrder?.setOnClickListener(this) + title = source.getTitle(this) + initList(source, filter) } - override fun isNsfwContent(): Flow = flowOf(source?.isNsfw() == true) + override fun isNsfwContent(): Flow = flowOf(source.isNsfw()) override fun onWindowInsetsChanged(insets: Insets) { viewBinding.root.updatePadding( @@ -119,7 +115,7 @@ class MangaListActivity : fun hidePreview() = setSideFragment(FilterSheetFragment::class.java, null) - private fun initList(source: MangaSource, tags: Set?) { + private fun initList(source: MangaSource, filter: MangaListFilter?) { val fm = supportFragmentManager val existingFragment = fm.findFragmentById(R.id.container) if (existingFragment is FilterCoordinator.Owner) { @@ -134,8 +130,8 @@ class MangaListActivity : } replace(R.id.container, fragment) runOnCommit { initFilter(fragment) } - if (!tags.isNullOrEmpty()) { - runOnCommit(ApplyFilterRunnable(fragment, tags)) + if (filter != null) { + runOnCommit(ApplyFilterRunnable(fragment, filter)) } } } @@ -161,11 +157,12 @@ class MangaListActivity : filterBadge.setMaxCharacterCount(0) filter.observe().observe(this) { snapshot -> chipSort.setTextAndVisible(snapshot.sortOrder.titleRes) - filterBadge.counter = if (snapshot.listFilter.isEmpty()) 0 else 1 + filterBadge.counter = if (snapshot.listFilter.hasNonSearchOptions()) 1 else 0 + supportActionBar?.subtitle = snapshot.listFilter.query } } else { filter.observe().map { - it.listFilter.tags.joinToString { tag -> tag.title } + it.listFilter.getSummary() }.flowOn(Dispatchers.Default) .observe(this) { supportActionBar?.subtitle = it @@ -189,26 +186,28 @@ class MangaListActivity : private class ApplyFilterRunnable( private val filterOwner: FilterCoordinator.Owner, - private val tags: Set, + private val filter: MangaListFilter, ) : Runnable { override fun run() { - filterOwner.filterCoordinator.set(MangaListFilter(tags = tags)) + filterOwner.filterCoordinator.set(filter) } } companion object { - private const val EXTRA_TAGS = "tags" + private const val EXTRA_FILTER = "filter" private const val EXTRA_SOURCE = "source" - const val ACTION_MANGA_EXPLORE = "${BuildConfig.APPLICATION_ID}.action.EXPLORE_MANGA" + private const val ACTION_MANGA_EXPLORE = "${BuildConfig.APPLICATION_ID}.action.EXPLORE_MANGA" - fun newIntent(context: Context, tags: Set) = Intent(context, MangaListActivity::class.java) - .setAction(ACTION_MANGA_EXPLORE) - .putExtra(EXTRA_TAGS, ParcelableMangaTags(tags)) - - fun newIntent(context: Context, source: MangaSource) = Intent(context, MangaListActivity::class.java) - .setAction(ACTION_MANGA_EXPLORE) - .putExtra(EXTRA_SOURCE, source.name) + fun newIntent(context: Context, source: MangaSource, filter: MangaListFilter?): Intent = + Intent(context, MangaListActivity::class.java) + .setAction(ACTION_MANGA_EXPLORE) + .putExtra(EXTRA_SOURCE, source.name) + .apply { + if (!filter.isNullOrEmpty()) { + putExtra(EXTRA_FILTER, ParcelableMangaListFilter(filter)) + } + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchActivity.kt deleted file mode 100644 index ec63cf80e..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchActivity.kt +++ /dev/null @@ -1,97 +0,0 @@ -package org.koitharu.kotatsu.search.ui - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.activity.viewModels -import androidx.appcompat.widget.SearchView -import androidx.core.graphics.Insets -import androidx.core.view.SoftwareKeyboardControllerCompat -import androidx.core.view.inputmethod.EditorInfoCompat -import androidx.core.view.updatePadding -import androidx.fragment.app.commit -import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.model.MangaSource -import org.koitharu.kotatsu.core.model.getTitle -import org.koitharu.kotatsu.core.ui.BaseActivity -import org.koitharu.kotatsu.core.util.ext.observe -import org.koitharu.kotatsu.databinding.ActivitySearchBinding -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel - -@AndroidEntryPoint -class SearchActivity : BaseActivity(), SearchView.OnQueryTextListener { - - private val searchSuggestionViewModel by viewModels() - private lateinit var source: MangaSource - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(ActivitySearchBinding.inflate(layoutInflater)) - source = MangaSource(intent.getStringExtra(EXTRA_SOURCE)) - val query = intent.getStringExtra(EXTRA_QUERY) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged) - with(viewBinding.searchView) { - queryHint = getString(R.string.search_on_s, source.getTitle(context)) - setOnQueryTextListener(this@SearchActivity) - - if (query.isNullOrBlank()) { - requestFocus() - SoftwareKeyboardControllerCompat(this).show() - } else { - setQuery(query, true) - } - } - } - - override fun onWindowInsetsChanged(insets: Insets) { - viewBinding.toolbar.updatePadding( - left = insets.left, - right = insets.right, - top = insets.top, - ) - viewBinding.container.updatePadding( - bottom = insets.bottom, - ) - } - - override fun onQueryTextSubmit(query: String?): Boolean { - val q = query?.trim() - if (q.isNullOrEmpty()) { - return false - } - title = query - supportFragmentManager.commit { - setReorderingAllowed(true) - replace(R.id.container, SearchFragment.newInstance(source, q)) - } - viewBinding.searchView.clearFocus() - searchSuggestionViewModel.saveQuery(q) - return true - } - - override fun onQueryTextChange(newText: String?): Boolean = false - - private fun onIncognitoModeChanged(isIncognito: Boolean) { - var options = viewBinding.searchView.imeOptions - options = if (isIncognito) { - options or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING - } else { - options and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv() - } - viewBinding.searchView.imeOptions = options - } - - companion object { - - private const val EXTRA_SOURCE = "source" - private const val EXTRA_QUERY = "query" - - fun newIntent(context: Context, source: MangaSource, query: String?) = - Intent(context, SearchActivity::class.java) - .putExtra(EXTRA_SOURCE, source.name) - .putExtra(EXTRA_QUERY, query) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchFragment.kt deleted file mode 100644 index 7662f9eb9..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchFragment.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.koitharu.kotatsu.search.ui - -import android.view.Menu -import androidx.appcompat.view.ActionMode -import androidx.fragment.app.viewModels -import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.list.ListSelectionController -import org.koitharu.kotatsu.core.util.ext.withArgs -import org.koitharu.kotatsu.list.ui.MangaListFragment -import org.koitharu.kotatsu.parsers.model.MangaSource - -@AndroidEntryPoint -class SearchFragment : MangaListFragment() { - - override val viewModel by viewModels() - - override fun onScrolledToEnd() { - viewModel.loadNextPage() - } - - override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.mode_remote, menu) - return super.onCreateActionMode(controller, mode, menu) - } - - companion object { - - const val ARG_QUERY = "query" - const val ARG_SOURCE = "source" - - fun newInstance(source: MangaSource, query: String) = SearchFragment().withArgs(2) { - putString(ARG_SOURCE, source.name) - putString(ARG_QUERY, query) - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt deleted file mode 100644 index e1ceb0db2..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt +++ /dev/null @@ -1,129 +0,0 @@ -package org.koitharu.kotatsu.search.ui - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.plus -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.model.MangaSource -import org.koitharu.kotatsu.core.model.distinctById -import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.util.ext.require -import org.koitharu.kotatsu.core.util.ext.sizeOrZero -import org.koitharu.kotatsu.download.ui.worker.DownloadWorker -import org.koitharu.kotatsu.list.domain.MangaListMapper -import org.koitharu.kotatsu.list.ui.MangaListViewModel -import org.koitharu.kotatsu.list.ui.model.EmptyState -import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.list.ui.model.LoadingFooter -import org.koitharu.kotatsu.list.ui.model.LoadingState -import org.koitharu.kotatsu.list.ui.model.toErrorFooter -import org.koitharu.kotatsu.list.ui.model.toErrorState -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaListFilter -import javax.inject.Inject - -@HiltViewModel -class SearchViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - repositoryFactory: MangaRepository.Factory, - settings: AppSettings, - private val mangaListMapper: MangaListMapper, - downloadScheduler: DownloadWorker.Scheduler, -) : MangaListViewModel(settings, downloadScheduler) { - - private val query = savedStateHandle.require(SearchFragment.ARG_QUERY) - private val repository = repositoryFactory.create(MangaSource(savedStateHandle[SearchFragment.ARG_SOURCE])) - private val mangaList = MutableStateFlow?>(null) - private val hasNextPage = MutableStateFlow(false) - private val listError = MutableStateFlow(null) - private var loadingJob: Job? = null - - override val content = combine( - mangaList.map { it?.skipNsfwIfNeeded() }, - observeListModeWithTriggers(), - 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_empty_common, - textPrimary = R.string.nothing_found, - textSecondary = R.string.text_search_holder_secondary, - actionStringRes = 0, - ), - ) - - else -> { - val result = ArrayList(list.size + 1) - mangaListMapper.toListModelList(result, list, mode) - when { - error != null -> result += error.toErrorFooter() - hasNext -> result += LoadingFooter() - } - result - } - } - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) - - init { - loadList(append = false) - } - - override fun onRefresh() { - loadList(append = false) - } - - override fun onRetry() { - loadList(append = !mangaList.value.isNullOrEmpty()) - } - - fun loadNextPage() { - if (hasNextPage.value && listError.value == null) { - loadList(append = true) - } - } - - private fun loadList(append: Boolean) { - if (loadingJob?.isActive == true) { - return - } - loadingJob = launchLoadingJob(Dispatchers.Default) { - try { - listError.value = null - val list = repository.getList( - offset = if (append) mangaList.value.sizeOrZero() else 0, - order = null, - filter = MangaListFilter(query = query), - ) - val prevList = mangaList.value.orEmpty() - if (!append) { - mangaList.value = list.distinctById() - } else if (list.isNotEmpty()) { - mangaList.value = (prevList + list).distinctById() - } - hasNextPage.value = if (append) { - prevList != mangaList.value - } else { - list.isNotEmpty() - } - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - listError.value = e - } - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt index eed362e67..a719c4f20 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt @@ -34,10 +34,10 @@ import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.search.ui.MangaListActivity -import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.search.ui.multi.adapter.MultiSearchAdapter import javax.inject.Inject @@ -63,7 +63,13 @@ class MultiSearchActivity : title = viewModel.query val itemCLickListener = OnListItemClickListener { item, view -> - startActivity(SearchActivity.newIntent(view.context, item.source, viewModel.query)) + startActivity( + MangaListActivity.newIntent( + view.context, + item.source, + MangaListFilter(query = viewModel.query), + ), + ) } val sizeResolver = DynamicItemSizeResolver(resources, settings, adjustWidth = true) val selectionDecoration = MangaSelectionDecoration(this) @@ -125,7 +131,7 @@ class MultiSearchActivity : override fun onTagClick(manga: Manga, tag: MangaTag, view: View) { if (!selectionController.onItemClick(manga.id)) { - val intent = MangaListActivity.newIntent(this, setOf(tag)) + val intent = MangaListActivity.newIntent(this, manga.source, MangaListFilter(tags = setOf(tag))) startActivity(intent) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt index 30005249b..a655cb410 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt @@ -89,7 +89,7 @@ class SourcesCatalogActivity : BaseActivity(), } override fun onItemClick(item: SourceCatalogItem.Source, view: View) { - startActivity(MangaListActivity.newIntent(this, item.source)) + startActivity(MangaListActivity.newIntent(this, item.source, null)) } override fun onItemLongClick(item: SourceCatalogItem.Source, view: View): Boolean { diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml deleted file mode 100644 index 494f0935a..000000000 --- a/app/src/main/res/layout/activity_search.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/opt_list_remote.xml b/app/src/main/res/menu/opt_list_remote.xml index 768334fea..c1f6cdde1 100644 --- a/app/src/main/res/menu/opt_list_remote.xml +++ b/app/src/main/res/menu/opt_list_remote.xml @@ -4,16 +4,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> - - diff --git a/app/src/main/res/menu/opt_search.xml b/app/src/main/res/menu/opt_search.xml new file mode 100644 index 000000000..49d226ff9 --- /dev/null +++ b/app/src/main/res/menu/opt_search.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9fef1ac61..6f78da58e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -726,4 +726,5 @@ Josei Years Any + This source does not support search with filters. Your filters have been cleared