Search manga with filters
This commit is contained in:
@@ -83,7 +83,7 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:f2354957e6') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:f410df40f1') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.default_searchable"
|
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>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.details.ui.DetailsActivity"
|
android:name="org.koitharu.kotatsu.details.ui.DetailsActivity"
|
||||||
@@ -112,9 +112,6 @@
|
|||||||
android:name="com.samsung.android.support.REMOTE_ACTION"
|
android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||||
android:resource="@xml/remote_action" />
|
android:resource="@xml/remote_action" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
|
||||||
android:label="@string/search" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
|
android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
|
||||||
android:exported="true"
|
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.adapter.loadingStateAD
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -81,7 +82,14 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
|
|||||||
|
|
||||||
override fun onItemClick(item: MangaAlternativeModel, view: View) {
|
override fun onItemClick(item: MangaAlternativeModel, view: View) {
|
||||||
when (view.id) {
|
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)
|
R.id.button_migrate -> confirmMigration(item.manga)
|
||||||
else -> startActivity(DetailsActivity.newIntent(this, item.manga))
|
else -> startActivity(DetailsActivity.newIntent(this, item.manga))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
package org.koitharu.kotatsu.core.model
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.collection.MutableObjectIntMap
|
import androidx.collection.MutableObjectIntMap
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import androidx.core.text.buildSpannedString
|
||||||
|
import androidx.core.text.strikeThrough
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.util.ext.iterator
|
import org.koitharu.kotatsu.core.util.ext.iterator
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
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.Demographic
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
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.model.MangaState
|
||||||
import org.koitharu.kotatsu.parsers.util.formatSimple
|
import org.koitharu.kotatsu.parsers.util.formatSimple
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
@@ -152,3 +156,26 @@ fun Manga.chaptersCount(): Int {
|
|||||||
}
|
}
|
||||||
return max
|
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)
|
.setLongLabel(title)
|
||||||
.setIcon(icon)
|
.setIcon(icon)
|
||||||
.setLongLived(true)
|
.setLongLived(true)
|
||||||
.setIntent(MangaListActivity.newIntent(context, source))
|
.setIntent(MangaListActivity.newIntent(context, source, null))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,6 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
children.forEach { it.isClickable = isChipClickable }
|
children.forEach { it.isClickable = isChipClickable }
|
||||||
}
|
}
|
||||||
var onChipCloseClickListener: OnChipCloseClickListener? = null
|
var onChipCloseClickListener: OnChipCloseClickListener? = null
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
val isCloseIconVisible = value != null
|
|
||||||
children.forEach { (it as? Chip)?.isCloseIconVisible = isCloseIconVisible }
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val ta = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0)
|
val ta = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0)
|
||||||
@@ -98,6 +93,7 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
@ColorRes val tint: Int = 0,
|
@ColorRes val tint: Int = 0,
|
||||||
val isChecked: Boolean = false,
|
val isChecked: Boolean = false,
|
||||||
val isDropdown: Boolean = false,
|
val isDropdown: Boolean = false,
|
||||||
|
val isCloseable: Boolean = false,
|
||||||
val data: Any? = null,
|
val data: Any? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -139,7 +135,7 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
isChipIconVisible = true
|
isChipIconVisible = true
|
||||||
}
|
}
|
||||||
isCheckedIconVisible = model.isChecked
|
isCheckedIconVisible = model.isChecked
|
||||||
isCloseIconVisible = if (onChipCloseClickListener != null || model.isDropdown) {
|
isCloseIconVisible = if (model.isCloseable || model.isDropdown) {
|
||||||
setCloseIconResource(
|
setCloseIconResource(
|
||||||
if (model.isDropdown) R.drawable.ic_expand_more else materialR.drawable.ic_m3_chip_close,
|
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.core.os.ParcelCompat
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
// https://issuetracker.google.com/issues/240585930
|
// 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 {
|
fun <T> SavedStateHandle.require(key: String): T {
|
||||||
return checkNotNull(get(key)) {
|
return checkNotNull(get(key)) {
|
||||||
"Value $key not found in SavedStateHandle or has a wrong type"
|
"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)
|
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? {
|
fun <K, V> Map<K, V>.findKeyByValue(value: V): K? {
|
||||||
for ((k, v) in entries) {
|
for ((k, v) in entries) {
|
||||||
if (v == value) {
|
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.list.ui.size.StaticItemSizeResolver
|
||||||
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
|
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
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.model.MangaTag
|
||||||
import org.koitharu.kotatsu.parsers.util.ellipsize
|
import org.koitharu.kotatsu.parsers.util.ellipsize
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
|
||||||
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
|
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
@@ -213,10 +213,10 @@ class DetailsActivity :
|
|||||||
R.id.chip_author -> {
|
R.id.chip_author -> {
|
||||||
val manga = viewModel.manga.value ?: return
|
val manga = viewModel.manga.value ?: return
|
||||||
startActivity(
|
startActivity(
|
||||||
SearchActivity.newIntent(
|
MangaListActivity.newIntent(
|
||||||
context = v.context,
|
context = v.context,
|
||||||
source = manga.source,
|
source = manga.source,
|
||||||
query = manga.author ?: return,
|
filter = MangaListFilter(query = manga.author),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -227,6 +227,7 @@ class DetailsActivity :
|
|||||||
MangaListActivity.newIntent(
|
MangaListActivity.newIntent(
|
||||||
context = v.context,
|
context = v.context,
|
||||||
source = manga.source,
|
source = manga.source,
|
||||||
|
filter = null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -286,7 +287,8 @@ class DetailsActivity :
|
|||||||
|
|
||||||
override fun onChipClick(chip: Chip, data: Any?) {
|
override fun onChipClick(chip: Chip, data: Any?) {
|
||||||
val tag = data as? MangaTag ?: return
|
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) {
|
override fun onLongClick(v: View): Boolean = when (v.id) {
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ class DownloadNotificationFactory @AssistedInject constructor(
|
|||||||
if (manga != null) {
|
if (manga != null) {
|
||||||
DetailsActivity.newIntent(context, manga)
|
DetailsActivity.newIntent(context, manga)
|
||||||
} else {
|
} else {
|
||||||
MangaListActivity.newIntent(context, LocalMangaSource)
|
MangaListActivity.newIntent(context, LocalMangaSource, null)
|
||||||
},
|
},
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT,
|
PendingIntent.FLAG_CANCEL_CURRENT,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class ExploreFragment :
|
|||||||
|
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
val intent = when (v.id) {
|
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_bookmarks -> AllBookmarksActivity.newIntent(v.context)
|
||||||
R.id.button_more -> SuggestionsActivity.newIntent(v.context)
|
R.id.button_more -> SuggestionsActivity.newIntent(v.context)
|
||||||
R.id.button_downloads -> Intent(v.context, DownloadsActivity::class.java)
|
R.id.button_downloads -> Intent(v.context, DownloadsActivity::class.java)
|
||||||
@@ -144,7 +144,7 @@ class ExploreFragment :
|
|||||||
if (sourceSelectionController?.onItemClick(item.id) == true) {
|
if (sourceSelectionController?.onItemClick(item.id) == true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val intent = MangaListActivity.newIntent(view.context, item.source)
|
val intent = MangaListActivity.newIntent(view.context, item.source, null)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ class FilterCoordinator @Inject constructor(
|
|||||||
private val currentSortOrder = MutableStateFlow(repository.defaultSortOrder)
|
private val currentSortOrder = MutableStateFlow(repository.defaultSortOrder)
|
||||||
|
|
||||||
private val availableSortOrders = repository.sortOrders
|
private val availableSortOrders = repository.sortOrders
|
||||||
private val capabilities = repository.filterCapabilities
|
|
||||||
private val filterOptions = SuspendLazy { repository.getFilterOptions() }
|
private val filterOptions = SuspendLazy { repository.getFilterOptions() }
|
||||||
|
val capabilities = repository.filterCapabilities
|
||||||
|
|
||||||
val mangaSource: MangaSource
|
val mangaSource: MangaSource
|
||||||
get() = repository.source
|
get() = repository.source
|
||||||
@@ -69,7 +69,7 @@ class FilterCoordinator @Inject constructor(
|
|||||||
get() = !currentListFilter.value.isEmpty()
|
get() = !currentListFilter.value.isEmpty()
|
||||||
|
|
||||||
val query: StateFlow<String?> = currentListFilter.map { it.query }
|
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 ->
|
val sortOrder: StateFlow<FilterProperty<SortOrder>> = currentSortOrder.map { selected ->
|
||||||
FilterProperty(
|
FilterProperty(
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import javax.inject.Inject
|
|||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsView.OnChipClickListener {
|
class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsView.OnChipClickListener,
|
||||||
|
ChipsView.OnChipCloseClickListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var filterHeaderProducer: FilterHeaderProducer
|
lateinit var filterHeaderProducer: FilterHeaderProducer
|
||||||
@@ -37,6 +38,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
|
|||||||
override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) {
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
binding.chipsTags.onChipClickListener = this
|
binding.chipsTags.onChipClickListener = this
|
||||||
|
binding.chipsTags.onChipCloseClickListener = this
|
||||||
filterHeaderProducer.observeHeader(filter)
|
filterHeaderProducer.observeHeader(filter)
|
||||||
.flowOn(Dispatchers.Default)
|
.flowOn(Dispatchers.Default)
|
||||||
.observe(viewLifecycleOwner, ::onDataChanged)
|
.observe(viewLifecycleOwner, ::onDataChanged)
|
||||||
@@ -45,11 +47,16 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
|
|||||||
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
||||||
|
|
||||||
override fun onChipClick(chip: Chip, data: Any?) {
|
override fun onChipClick(chip: Chip, data: Any?) {
|
||||||
val tag = data as? MangaTag
|
when (data) {
|
||||||
if (tag == null) {
|
is MangaTag -> filter.toggleTag(data, !chip.isChecked)
|
||||||
TagsCatalogSheet.show(parentFragmentManager, isExcludeTag = false)
|
is String -> Unit
|
||||||
} else {
|
null -> TagsCatalogSheet.show(parentFragmentManager, isExcludeTag = false)
|
||||||
filter.toggleTag(tag, !chip.isChecked)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||||
import java.util.LinkedList
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
class FilterHeaderProducer @Inject constructor(
|
class FilterHeaderProducer @Inject constructor(
|
||||||
private val searchRepository: MangaSearchRepository,
|
private val searchRepository: MangaSearchRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun observeHeader(filterCoordinator: FilterCoordinator): Flow<FilterHeaderModel> {
|
fun observeHeader(filterCoordinator: FilterCoordinator): Flow<FilterHeaderModel> {
|
||||||
return filterCoordinator.tags.mapLatest {
|
return combine(filterCoordinator.tags, filterCoordinator.query) { tags, query ->
|
||||||
createChipsList(
|
createChipsList(
|
||||||
source = filterCoordinator.mangaSource,
|
source = filterCoordinator.mangaSource,
|
||||||
property = it,
|
property = tags,
|
||||||
|
query = query,
|
||||||
limit = 8,
|
limit = 8,
|
||||||
)
|
)
|
||||||
}.combine(filterCoordinator.observe()) { chipList, snapshot ->
|
}.combine(filterCoordinator.observe()) { chipList, snapshot ->
|
||||||
@@ -35,6 +35,7 @@ class FilterHeaderProducer @Inject constructor(
|
|||||||
private suspend fun createChipsList(
|
private suspend fun createChipsList(
|
||||||
source: MangaSource,
|
source: MangaSource,
|
||||||
property: FilterProperty<MangaTag>,
|
property: FilterProperty<MangaTag>,
|
||||||
|
query: String?,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
): List<ChipsView.ChipModel> {
|
): List<ChipsView.ChipModel> {
|
||||||
val selectedTags = property.selectedItems.toMutableSet()
|
val selectedTags = property.selectedItems.toMutableSet()
|
||||||
@@ -49,7 +50,7 @@ class FilterHeaderProducer @Inject constructor(
|
|||||||
if (tags.isEmpty() && selectedTags.isEmpty()) {
|
if (tags.isEmpty() && selectedTags.isEmpty()) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
val result = LinkedList<ChipsView.ChipModel>()
|
val result = ArrayDeque<ChipsView.ChipModel>(tags.size + selectedTags.size + 1)
|
||||||
for (tag in tags) {
|
for (tag in tags) {
|
||||||
val model = ChipsView.ChipModel(
|
val model = ChipsView.ChipModel(
|
||||||
title = tag.title,
|
title = tag.title,
|
||||||
@@ -70,6 +71,16 @@ class FilterHeaderProducer @Inject constructor(
|
|||||||
)
|
)
|
||||||
result.addFirst(model)
|
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
|
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.MainActivity
|
||||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
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.model.MangaTag
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||||
@@ -164,7 +165,8 @@ abstract class MangaListFragment :
|
|||||||
|
|
||||||
override fun onTagClick(manga: Manga, tag: MangaTag, view: View) {
|
override fun onTagClick(manga: Manga, tag: MangaTag, view: View) {
|
||||||
if (selectionController?.onItemClick(manga.id) != true) {
|
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)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity
|
|||||||
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
||||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
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.model.MangaTag
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -83,10 +83,10 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.textView_author -> startActivity(
|
R.id.textView_author -> startActivity(
|
||||||
SearchActivity.newIntent(
|
MangaListActivity.newIntent(
|
||||||
context = v.context,
|
context = v.context,
|
||||||
source = manga.source,
|
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 tag = data as? MangaTag ?: return
|
||||||
val filter = (activity as? FilterCoordinator.Owner)?.filterCoordinator
|
val filter = (activity as? FilterCoordinator.Owner)?.filterCoordinator
|
||||||
if (filter == null) {
|
if (filter == null) {
|
||||||
startActivity(MangaListActivity.newIntent(requireContext(), setOf(tag)))
|
startActivity(MangaListActivity.newIntent(chip.context, tag.source, MangaListFilter(tags = setOf(tag))))
|
||||||
} else {
|
} else {
|
||||||
filter.toggleTag(tag, true)
|
filter.toggleTag(tag, true)
|
||||||
closeSelf()
|
closeSelf()
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import org.koitharu.kotatsu.databinding.FragmentListBinding
|
|||||||
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
||||||
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
|
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
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.remotelist.ui.RemoteListFragment
|
||||||
import org.koitharu.kotatsu.settings.storage.RequestStorageManagerPermissionContract
|
import org.koitharu.kotatsu.settings.storage.RequestStorageManagerPermissionContract
|
||||||
import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
|
import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
|
||||||
@@ -61,6 +62,7 @@ class LocalListFragment : MangaListFragment(), FilterCoordinator.Owner {
|
|||||||
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
addMenuProvider(LocalListMenuProvider(binding.root.context, this::onEmptyActionClick))
|
addMenuProvider(LocalListMenuProvider(binding.root.context, this::onEmptyActionClick))
|
||||||
|
addMenuProvider(MangaSearchMenuProvider(filterCoordinator, viewModel))
|
||||||
viewModel.onMangaRemoved.observeEvent(viewLifecycleOwner) { onItemRemoved() }
|
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.data.MangaSourcesRepository
|
||||||
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
||||||
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
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.domain.MangaListMapper
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
@@ -41,7 +40,6 @@ class LocalListViewModel @Inject constructor(
|
|||||||
exploreRepository: ExploreRepository,
|
exploreRepository: ExploreRepository,
|
||||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||||
private val localStorageManager: LocalStorageManager,
|
private val localStorageManager: LocalStorageManager,
|
||||||
filterHeaderProducer: FilterHeaderProducer,
|
|
||||||
sourcesRepository: MangaSourcesRepository,
|
sourcesRepository: MangaSourcesRepository,
|
||||||
) : RemoteListViewModel(
|
) : RemoteListViewModel(
|
||||||
savedStateHandle,
|
savedStateHandle,
|
||||||
@@ -109,8 +107,10 @@ class LocalListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createEmptyState(canResetFilter: Boolean): EmptyState {
|
override fun createEmptyState(canResetFilter: Boolean): EmptyState = if (canResetFilter) {
|
||||||
return EmptyState(
|
super.createEmptyState(canResetFilter)
|
||||||
|
} else {
|
||||||
|
EmptyState(
|
||||||
icon = R.drawable.ic_empty_local,
|
icon = R.drawable.ic_empty_local,
|
||||||
textPrimary = R.string.text_local_holder_primary,
|
textPrimary = R.string.text_local_holder_primary,
|
||||||
textSecondary = R.string.text_local_holder_secondary,
|
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.owners.BottomNavOwner
|
||||||
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
|
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
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.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||||
@@ -265,7 +266,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onTagClick(tag: MangaTag) {
|
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) {
|
override fun onQueryChanged(query: String) {
|
||||||
@@ -277,7 +278,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSourceClick(source: MangaSource) {
|
override fun onSourceClick(source: MangaSource) {
|
||||||
val intent = MangaListActivity.newIntent(this, source)
|
val intent = MangaListActivity.newIntent(this, source, null)
|
||||||
startActivity(intent)
|
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.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.FilterCoordinator
|
||||||
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
|
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
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.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
|
||||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -44,6 +40,7 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner {
|
|||||||
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
addMenuProvider(RemoteListMenuProvider())
|
addMenuProvider(RemoteListMenuProvider())
|
||||||
|
addMenuProvider(MangaSearchMenuProvider(filterCoordinator, viewModel))
|
||||||
viewModel.isRandomLoading.observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
|
viewModel.isRandomLoading.observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
|
||||||
viewModel.onOpenManga.observeEvent(viewLifecycleOwner) {
|
viewModel.onOpenManga.observeEvent(viewLifecycleOwner) {
|
||||||
startActivity(DetailsActivity.newIntent(binding.root.context, it))
|
startActivity(DetailsActivity.newIntent(binding.root.context, it))
|
||||||
@@ -86,19 +83,10 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class RemoteListMenuProvider :
|
private inner class RemoteListMenuProvider : MenuProvider {
|
||||||
MenuProvider,
|
|
||||||
SearchView.OnQueryTextListener,
|
|
||||||
MenuItem.OnActionExpandListener {
|
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
menuInflater.inflate(R.menu.opt_list_remote, menu)
|
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) {
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||||
@@ -127,43 +115,9 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner {
|
|||||||
|
|
||||||
override fun onPrepareMenu(menu: Menu) {
|
override fun onPrepareMenu(menu: Menu) {
|
||||||
super.onPrepareMenu(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_random)?.isEnabled = !viewModel.isRandomLoading.value
|
||||||
menu.findItem(R.id.action_filter_reset)?.isVisible = filterCoordinator.isFilterApplied
|
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 {
|
companion object {
|
||||||
|
|||||||
@@ -70,9 +70,6 @@ open class RemoteListViewModel @Inject constructor(
|
|||||||
private var loadingJob: Job? = null
|
private var loadingJob: Job? = null
|
||||||
private var randomJob: Job? = null
|
private var randomJob: Job? = null
|
||||||
|
|
||||||
val isSearchAvailable: Boolean
|
|
||||||
get() = repository.filterCapabilities.isSearchSupported
|
|
||||||
|
|
||||||
val browserUrl: String?
|
val browserUrl: String?
|
||||||
get() = (repository as? ParserMangaRepository)?.domain?.let { "https://$it" }
|
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.R
|
||||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
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.getTitle
|
||||||
import org.koitharu.kotatsu.core.model.isNsfw
|
import org.koitharu.kotatsu.core.model.isNsfw
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
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.parser.MangaIntent
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.ui.model.titleRes
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
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 org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
@@ -63,28 +64,23 @@ class MangaListActivity :
|
|||||||
"Cannot find FilterCoordinator.Owner fragment in ${supportFragmentManager.fragments}"
|
"Cannot find FilterCoordinator.Owner fragment in ${supportFragmentManager.fragments}"
|
||||||
}.filterCoordinator
|
}.filterCoordinator
|
||||||
|
|
||||||
private var source: MangaSource? = null
|
private lateinit var source: MangaSource
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(ActivityMangaListBinding.inflate(layoutInflater))
|
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)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
if (viewBinding.containerFilterHeader != null) {
|
if (viewBinding.containerFilterHeader != null) {
|
||||||
viewBinding.appbar.addOnOffsetChangedListener(this)
|
viewBinding.appbar.addOnOffsetChangedListener(this)
|
||||||
}
|
}
|
||||||
source = intent.getStringExtra(EXTRA_SOURCE)?.let(::MangaSource) ?: tags?.firstOrNull()?.source
|
viewBinding.buttonOrder?.setOnClickListener(this)
|
||||||
val src = source
|
title = source.getTitle(this)
|
||||||
if (src == null) {
|
initList(source, filter)
|
||||||
finishAfterTransition()
|
|
||||||
} else {
|
|
||||||
viewBinding.buttonOrder?.setOnClickListener(this)
|
|
||||||
title = src.getTitle(this)
|
|
||||||
initList(src, tags)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isNsfwContent(): Flow<Boolean> = flowOf(source?.isNsfw() == true)
|
override fun isNsfwContent(): Flow<Boolean> = flowOf(source.isNsfw())
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
viewBinding.root.updatePadding(
|
viewBinding.root.updatePadding(
|
||||||
@@ -119,7 +115,7 @@ class MangaListActivity :
|
|||||||
|
|
||||||
fun hidePreview() = setSideFragment(FilterSheetFragment::class.java, null)
|
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 fm = supportFragmentManager
|
||||||
val existingFragment = fm.findFragmentById(R.id.container)
|
val existingFragment = fm.findFragmentById(R.id.container)
|
||||||
if (existingFragment is FilterCoordinator.Owner) {
|
if (existingFragment is FilterCoordinator.Owner) {
|
||||||
@@ -134,8 +130,8 @@ class MangaListActivity :
|
|||||||
}
|
}
|
||||||
replace(R.id.container, fragment)
|
replace(R.id.container, fragment)
|
||||||
runOnCommit { initFilter(fragment) }
|
runOnCommit { initFilter(fragment) }
|
||||||
if (!tags.isNullOrEmpty()) {
|
if (filter != null) {
|
||||||
runOnCommit(ApplyFilterRunnable(fragment, tags))
|
runOnCommit(ApplyFilterRunnable(fragment, filter))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,11 +157,12 @@ class MangaListActivity :
|
|||||||
filterBadge.setMaxCharacterCount(0)
|
filterBadge.setMaxCharacterCount(0)
|
||||||
filter.observe().observe(this) { snapshot ->
|
filter.observe().observe(this) { snapshot ->
|
||||||
chipSort.setTextAndVisible(snapshot.sortOrder.titleRes)
|
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 {
|
} else {
|
||||||
filter.observe().map {
|
filter.observe().map {
|
||||||
it.listFilter.tags.joinToString { tag -> tag.title }
|
it.listFilter.getSummary()
|
||||||
}.flowOn(Dispatchers.Default)
|
}.flowOn(Dispatchers.Default)
|
||||||
.observe(this) {
|
.observe(this) {
|
||||||
supportActionBar?.subtitle = it
|
supportActionBar?.subtitle = it
|
||||||
@@ -189,26 +186,28 @@ class MangaListActivity :
|
|||||||
|
|
||||||
private class ApplyFilterRunnable(
|
private class ApplyFilterRunnable(
|
||||||
private val filterOwner: FilterCoordinator.Owner,
|
private val filterOwner: FilterCoordinator.Owner,
|
||||||
private val tags: Set<MangaTag>,
|
private val filter: MangaListFilter,
|
||||||
) : Runnable {
|
) : Runnable {
|
||||||
|
|
||||||
override fun run() {
|
override fun run() {
|
||||||
filterOwner.filterCoordinator.set(MangaListFilter(tags = tags))
|
filterOwner.filterCoordinator.set(filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val EXTRA_TAGS = "tags"
|
private const val EXTRA_FILTER = "filter"
|
||||||
private const val EXTRA_SOURCE = "source"
|
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)
|
fun newIntent(context: Context, source: MangaSource, filter: MangaListFilter?): Intent =
|
||||||
.setAction(ACTION_MANGA_EXPLORE)
|
Intent(context, MangaListActivity::class.java)
|
||||||
.putExtra(EXTRA_TAGS, ParcelableMangaTags(tags))
|
.setAction(ACTION_MANGA_EXPLORE)
|
||||||
|
.putExtra(EXTRA_SOURCE, source.name)
|
||||||
fun newIntent(context: Context, source: MangaSource) = Intent(context, MangaListActivity::class.java)
|
.apply {
|
||||||
.setAction(ACTION_MANGA_EXPLORE)
|
if (!filter.isNullOrEmpty()) {
|
||||||
.putExtra(EXTRA_SOURCE, source.name)
|
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.model.ListHeader
|
||||||
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
|
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
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.model.MangaTag
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
|
||||||
import org.koitharu.kotatsu.search.ui.multi.adapter.MultiSearchAdapter
|
import org.koitharu.kotatsu.search.ui.multi.adapter.MultiSearchAdapter
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -63,7 +63,13 @@ class MultiSearchActivity :
|
|||||||
title = viewModel.query
|
title = viewModel.query
|
||||||
|
|
||||||
val itemCLickListener = OnListItemClickListener<MultiSearchListModel> { item, view ->
|
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 sizeResolver = DynamicItemSizeResolver(resources, settings, adjustWidth = true)
|
||||||
val selectionDecoration = MangaSelectionDecoration(this)
|
val selectionDecoration = MangaSelectionDecoration(this)
|
||||||
@@ -125,7 +131,7 @@ class MultiSearchActivity :
|
|||||||
|
|
||||||
override fun onTagClick(manga: Manga, tag: MangaTag, view: View) {
|
override fun onTagClick(manga: Manga, tag: MangaTag, view: View) {
|
||||||
if (!selectionController.onItemClick(manga.id)) {
|
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)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: SourceCatalogItem.Source, view: View) {
|
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 {
|
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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
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
|
<item
|
||||||
android:id="@+id/action_random"
|
android:id="@+id/action_random"
|
||||||
android:icon="@drawable/ic_dice"
|
android:icon="@drawable/ic_dice"
|
||||||
|
android:orderInCategory="10"
|
||||||
android:title="@string/random"
|
android:title="@string/random"
|
||||||
app:showAsAction="ifRoom" />
|
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="demographic_josei">Josei</string>
|
||||||
<string name="years">Years</string>
|
<string name="years">Years</string>
|
||||||
<string name="any">Any</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>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user