Search manga with filters
This commit is contained in:
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.default_searchable"
|
||||
android:value="org.koitharu.kotatsu.ui.search.SearchActivity" />
|
||||
android:value="org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.details.ui.DetailsActivity"
|
||||
@@ -112,9 +112,6 @@
|
||||
android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||
android:resource="@xml/remote_action" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
||||
android:label="@string/search" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -30,7 +30,8 @@ import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -81,7 +82,14 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MangaListFilter> {
|
||||
|
||||
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<ParcelableMangaTags>()?.tags.orEmpty(),
|
||||
tagsExclude = parcel.readParcelableCompat<ParcelableMangaTags>()?.tags.orEmpty(),
|
||||
locale = parcel.readSerializableCompat(),
|
||||
originalLocale = parcel.readSerializableCompat(),
|
||||
states = parcel.readEnumSet<MangaState>().orEmpty(),
|
||||
contentRating = parcel.readEnumSet<ContentRating>().orEmpty(),
|
||||
types = parcel.readEnumSet<ContentType>().orEmpty(),
|
||||
demographics = parcel.readEnumSet<Demographic>().orEmpty(),
|
||||
year = parcel.readInt(),
|
||||
yearFrom = parcel.readInt(),
|
||||
yearTo = parcel.readInt(),
|
||||
)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
@TypeParceler<MangaListFilter, MangaListFilterParceler>
|
||||
data class ParcelableMangaListFilter(val filter: MangaListFilter) : Parcelable
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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 <reified T : Serializable> Bundle.requireSerializable(key: String): T
|
||||
}
|
||||
}
|
||||
|
||||
fun <E : Enum<E>> Parcel.writeEnumSet(set: Set<E>?) {
|
||||
if (set == null) {
|
||||
writeValue(null)
|
||||
} else {
|
||||
val array = IntArray(set.size)
|
||||
set.forEachIndexed { i, e -> array[i] = e.ordinal }
|
||||
writeIntArray(array)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified E : Enum<E>> Parcel.readEnumSet(): Set<E>? = readEnumSet(E::class.java)
|
||||
|
||||
fun <E : Enum<E>> Parcel.readEnumSet(cls: Class<E>): Set<E>? {
|
||||
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 <T> SavedStateHandle.require(key: String): T {
|
||||
return checkNotNull(get(key)) {
|
||||
"Value $key not found in SavedStateHandle or has a wrong type"
|
||||
|
||||
@@ -25,6 +25,12 @@ fun <T> Collection<T>.asArrayList(): ArrayList<T> = if (this is ArrayList<*>) {
|
||||
ArrayList(this)
|
||||
}
|
||||
|
||||
fun <E : Enum<E>> Set<E>.asEnumSet(cls: Class<E>): EnumSet<E> = if (this is EnumSet<*>) {
|
||||
this as EnumSet<E>
|
||||
} else {
|
||||
EnumSet.noneOf(cls).apply { addAll(this@asEnumSet) }
|
||||
}
|
||||
|
||||
fun <K, V> Map<K, V>.findKeyByValue(value: V): K? {
|
||||
for ((k, v) in entries) {
|
||||
if (v == value) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String?> = currentListFilter.map { it.query }
|
||||
.stateIn(coroutineScope, SharingStarted.Lazily, null)
|
||||
.stateIn(coroutineScope, SharingStarted.Eagerly, null)
|
||||
|
||||
val sortOrder: StateFlow<FilterProperty<SortOrder>> = currentSortOrder.map { selected ->
|
||||
FilterProperty(
|
||||
|
||||
@@ -22,7 +22,8 @@ import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsView.OnChipClickListener {
|
||||
class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsView.OnChipClickListener,
|
||||
ChipsView.OnChipCloseClickListener {
|
||||
|
||||
@Inject
|
||||
lateinit var filterHeaderProducer: FilterHeaderProducer
|
||||
@@ -37,6 +38,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), 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<FragmentFilterHeaderBinding>(), 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<FilterHeaderModel> {
|
||||
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<MangaTag>,
|
||||
query: String?,
|
||||
limit: Int,
|
||||
): List<ChipsView.ChipModel> {
|
||||
val selectedTags = property.selectedItems.toMutableSet()
|
||||
@@ -49,7 +50,7 @@ class FilterHeaderProducer @Inject constructor(
|
||||
if (tags.isEmpty() && selectedTags.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val result = LinkedList<ChipsView.ChipModel>()
|
||||
val result = ArrayDeque<ChipsView.ChipModel>(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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FragmentPreviewBinding>(), 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<FragmentPreviewBinding>(), 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()
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
|
||||
@@ -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<LocalManga?>,
|
||||
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,
|
||||
|
||||
@@ -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<ActivityMainBinding>(), 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<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
}
|
||||
|
||||
override fun onSourceClick(source: MangaSource) {
|
||||
val intent = MangaListActivity.newIntent(this, source)
|
||||
val intent = MangaListActivity.newIntent(this, source, null)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" }
|
||||
|
||||
|
||||
@@ -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<ParcelableMangaTags>(EXTRA_TAGS)?.tags
|
||||
val filter = intent.getParcelableExtraCompat<ParcelableMangaListFilter>(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<Boolean> = flowOf(source?.isNsfw() == true)
|
||||
override fun isNsfwContent(): Flow<Boolean> = 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<MangaTag>?) {
|
||||
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<MangaTag>,
|
||||
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<MangaTag>) = 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ActivitySearchBinding>(), SearchView.OnQueryTextListener {
|
||||
|
||||
private val searchSuggestionViewModel by viewModels<SearchSuggestionViewModel>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<SearchViewModel>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>(SearchFragment.ARG_QUERY)
|
||||
private val repository = repositoryFactory.create(MangaSource(savedStateHandle[SearchFragment.ARG_SOURCE]))
|
||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||
private val hasNextPage = MutableStateFlow(false)
|
||||
private val listError = MutableStateFlow<Throwable?>(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<ListModel>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<MultiSearchListModel> { 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<?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="wrap_content"
|
||||
app:layout_scrollFlags="scroll|enterAlways|snap">
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/searchView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_vertical"
|
||||
app:iconifiedByDefault="false"
|
||||
app:queryBackground="@null"
|
||||
app:searchHintIcon="@null"
|
||||
app:searchIcon="@null" />
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -4,16 +4,10 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:icon="?actionModeWebSearchDrawable"
|
||||
android:title="@string/search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="ifRoom|collapseActionView" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_random"
|
||||
android:icon="@drawable/ic_dice"
|
||||
android:orderInCategory="10"
|
||||
android:title="@string/random"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
|
||||
14
app/src/main/res/menu/opt_search.xml
Normal file
14
app/src/main/res/menu/opt_search.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:icon="?actionModeWebSearchDrawable"
|
||||
android:orderInCategory="0"
|
||||
android:title="@string/search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="ifRoom|collapseActionView" />
|
||||
|
||||
</menu>
|
||||
@@ -726,4 +726,5 @@
|
||||
<string name="demographic_josei">Josei</string>
|
||||
<string name="years">Years</string>
|
||||
<string name="any">Any</string>
|
||||
<string name="filter_search_warning">This source does not support search with filters. Your filters have been cleared</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user