Improve quick filter behavior

This commit is contained in:
Koitharu
2024-08-20 12:03:05 +03:00
parent d99bc08e49
commit c00614f17d
14 changed files with 138 additions and 89 deletions

View File

@@ -57,6 +57,8 @@ class MangaQueryBuilder(
if (filterOptions.isNotEmpty()) {
if (whereConditions.isEmpty()) {
append(" WHERE")
} else {
append(" AND")
}
var isFirst = true
val groupedOptions = filterOptions.groupBy { it.groupKey }
@@ -97,10 +99,12 @@ class MangaQueryBuilder(
}
}.let { SimpleSQLiteQuery(it) }
private fun getConditionOrThrow(option: ListFilterOption): String =
requireNotNull(conditionCallback.getCondition(option)) {
private fun getConditionOrThrow(option: ListFilterOption): String = when (option) {
is ListFilterOption.Inverted -> "NOT(${getConditionOrThrow(option.option)})"
else -> requireNotNull(conditionCallback.getCondition(option)) {
"Unsupported filter option $option"
}
}
fun interface ConditionCallback {

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.View.OnClickListener
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
@@ -11,8 +12,6 @@ import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.google.android.material.chip.ChipGroup
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.castOrNull
import com.google.android.material.R as materialR
class ChipsView @JvmOverloads constructor(
@@ -23,9 +22,7 @@ class ChipsView @JvmOverloads constructor(
private var isLayoutSuppressedCompat = false
private var isLayoutCalledOnSuppressed = false
private val chipOnClickListener = OnClickListener {
onChipClickListener?.onChipClick(it as Chip, it.tag)
}
private val chipOnClickListener = InternalChipClickListener()
private val chipOnCloseListener = OnClickListener {
val chip = it as Chip
val data = it.tag
@@ -71,8 +68,8 @@ class ChipsView @JvmOverloads constructor(
suppressLayoutCompat(true)
try {
for ((i, model) in items.withIndex()) {
val chip = getChildAt(i) as Chip? ?: addChip()
bindChip(chip, model)
val chip = getChildAt(i) as DataChip? ?: addChip()
chip.bind(model)
}
if (childCount > items.size) {
removeViews(items.size, childCount - items.size)
@@ -82,56 +79,7 @@ class ChipsView @JvmOverloads constructor(
}
}
fun <T> getCheckedData(cls: Class<T>): Set<T> {
val result = LinkedHashSet<T>(childCount)
for (child in children) {
if (child is Chip && child.isChecked) {
result += cls.castOrNull(child.tag) ?: continue
}
}
return result
}
private fun bindChip(chip: Chip, model: ChipModel) {
if (model.titleResId == 0) {
chip.text = model.title
} else {
chip.setText(model.titleResId)
}
chip.isClickable = onChipClickListener != null || model.isCheckable
chip.isCheckable = model.isCheckable
if (model.icon == 0) {
chip.chipIcon = null
chip.isChipIconVisible = false
} else {
chip.setChipIconResource(model.icon)
chip.isChipIconVisible = true
}
chip.isChecked = model.isChecked
chip.isCheckedIconVisible = chip.isCheckable && model.icon == 0
chip.isCloseIconVisible = if (onChipCloseClickListener != null || model.isDropdown) {
chip.setCloseIconResource(
if (model.isDropdown) R.drawable.ic_expand_more else materialR.drawable.ic_m3_chip_close,
)
true
} else {
false
}
chip.tag = model.data
}
private fun addChip(): Chip {
val chip = Chip(context)
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
chip.setChipDrawable(drawable)
chip.isChipIconVisible = false
chip.setOnCloseIconClickListener(chipOnCloseListener)
chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(chipOnClickListener)
chip.isElegantTextHeight = false
addView(chip)
return chip
}
private fun addChip() = DataChip(context).also { addView(it) }
private fun suppressLayoutCompat(suppress: Boolean) {
isLayoutSuppressedCompat = suppress
@@ -147,13 +95,71 @@ class ChipsView @JvmOverloads constructor(
val title: CharSequence? = null,
@StringRes val titleResId: Int = 0,
@DrawableRes val icon: Int = 0,
val isCheckable: Boolean = false,
@ColorRes val tint: Int = 0,
val isChecked: Boolean = false,
val isDropdown: Boolean = false,
val data: Any? = null,
)
private inner class DataChip(context: Context) : Chip(context) {
private var model: ChipModel? = null
init {
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
setChipDrawable(drawable)
isChipIconVisible = false
setOnCloseIconClickListener(chipOnCloseListener)
setEnsureMinTouchTargetSize(false)
setOnClickListener(chipOnClickListener)
isElegantTextHeight = false
}
fun bind(model: ChipModel) {
this.model = model
if (model.titleResId == 0) {
text = model.title
} else {
setText(model.titleResId)
}
isClickable = onChipClickListener != null
if (model.isChecked) {
isCheckable = true
isChecked = true
} else {
isChecked = false
isCheckable = false
}
if (model.icon == 0 || model.isChecked) {
chipIcon = null
isChipIconVisible = false
} else {
setChipIconResource(model.icon)
isChipIconVisible = true
}
isCheckedIconVisible = model.isChecked
isCloseIconVisible = if (onChipCloseClickListener != null || model.isDropdown) {
setCloseIconResource(
if (model.isDropdown) R.drawable.ic_expand_more else materialR.drawable.ic_m3_chip_close,
)
true
} else {
false
}
tag = model.data
}
override fun toggle() = Unit
}
private inner class InternalChipClickListener : OnClickListener {
override fun onClick(v: View?) {
val chip = v as? DataChip ?: return
onChipClickListener?.onChipClick(chip, chip.tag)
}
}
fun interface OnChipClickListener {
fun onChipClick(chip: Chip, data: Any?)

View File

@@ -195,8 +195,6 @@ abstract class FavouritesDao : MangaQueryBuilder.ConditionCallback {
ListFilterOption.Macro.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = favourites.manga_id) > 0"
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE favourites.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})"
ListFilterOption.Downloaded,
is ListFilterOption.Favorite,
ListFilterOption.Macro.FAVORITE -> null
else -> null
}
}

View File

@@ -393,7 +393,6 @@ class FilterCoordinator @Inject constructor(
for (tag in tags) {
val model = ChipsView.ChipModel(
title = tag.title,
isCheckable = true,
isChecked = selectedTags.remove(tag),
data = tag,
)
@@ -406,7 +405,6 @@ class FilterCoordinator @Inject constructor(
for (tag in selectedTags) {
val model = ChipsView.ChipModel(
title = tag.title,
isCheckable = true,
isChecked = true,
data = tag,
)

View File

@@ -39,7 +39,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
if (tag == null) {
TagsCatalogSheet.show(parentFragmentManager, isExcludeTag = false)
} else {
filter.setTag(tag, chip.isChecked)
filter.setTag(tag, !chip.isChecked)
}
}

View File

@@ -78,14 +78,14 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
override fun onChipClick(chip: Chip, data: Any?) {
val filter = requireFilter()
when (data) {
is MangaState -> filter.setState(data, chip.isChecked)
is MangaState -> filter.setState(data, !chip.isChecked)
is MangaTag -> if (chip.parentView?.id == R.id.chips_genresExclude) {
filter.setTagExcluded(data, chip.isChecked)
filter.setTagExcluded(data, !chip.isChecked)
} else {
filter.setTag(data, chip.isChecked)
filter.setTag(data, !chip.isChecked)
}
is ContentRating -> filter.setContentRating(data, chip.isChecked)
is ContentRating -> filter.setContentRating(data, !chip.isChecked)
null -> TagsCatalogSheet.show(childFragmentManager, chip.parentView?.id == R.id.chips_genresExclude)
}
}
@@ -142,7 +142,6 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
value.selectedItems.mapTo(chips) { tag ->
ChipsView.ChipModel(
title = tag.title,
isCheckable = true,
isChecked = true,
data = tag,
)
@@ -151,7 +150,6 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
if (tag !in value.selectedItems) {
ChipsView.ChipModel(
title = tag.title,
isCheckable = true,
isChecked = false,
data = tag,
)
@@ -181,7 +179,6 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
tint = 0,
title = tag.title,
icon = 0,
isCheckable = true,
isChecked = true,
data = tag,
)
@@ -190,7 +187,6 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
if (tag !in value.selectedItems) {
ChipsView.ChipModel(
title = tag.title,
isCheckable = true,
isChecked = false,
data = tag,
)
@@ -217,7 +213,6 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
val chips = value.availableItems.map { state ->
ChipsView.ChipModel(
title = getString(state.titleResId),
isCheckable = true,
isChecked = state in value.selectedItems,
data = state,
)
@@ -235,7 +230,6 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
val chips = value.availableItems.map { contentRating ->
ChipsView.ChipModel(
title = getString(contentRating.titleResId),
isCheckable = true,
isChecked = contentRating in value.selectedItems,
data = contentRating,
)

View File

@@ -153,12 +153,12 @@ abstract class HistoryDao : MangaQueryBuilder.ConditionCallback {
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<HistoryWithManga>>
override fun getCondition(option: ListFilterOption): String? = when (option) {
ListFilterOption.Downloaded -> null
is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id AND category_id = ${option.category.id})"
ListFilterOption.Macro.COMPLETED -> "percent >= $PROGRESS_COMPLETED"
ListFilterOption.Macro.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = history.manga_id) > 0"
ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id)"
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE history.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})"
else -> null
}
}

View File

@@ -87,4 +87,15 @@ sealed interface ListFilterOption {
override val groupKey: String
get() = "_favcat"
}
data class Inverted(
val option: ListFilterOption,
override val iconResId: Int,
override val titleResId: Int,
override val titleText: CharSequence?,
) : ListFilterOption {
override val groupKey: String
get() = "_inv" + option.groupKey
}
}

View File

@@ -23,7 +23,7 @@ abstract class MangaListQuickFilter(
override fun setFilterOption(option: ListFilterOption, isApplied: Boolean) {
appliedFilter.value = ArraySet(appliedFilter.value).also {
if (isApplied) {
it.add(option)
it.addNoConflicts(option)
} else {
it.remove(option)
}
@@ -35,7 +35,7 @@ abstract class MangaListQuickFilter(
if (option in it) {
it.remove(option)
} else {
it.add(option)
it.addNoConflicts(option)
}
}
}
@@ -55,7 +55,6 @@ abstract class MangaListQuickFilter(
title = option.titleText,
titleResId = option.titleResId,
icon = option.iconResId,
isCheckable = true,
isChecked = option in selectedOptions,
data = option,
)
@@ -68,4 +67,13 @@ abstract class MangaListQuickFilter(
}
protected abstract suspend fun getAvailableFilterOptions(): List<ListFilterOption>
private fun ArraySet<ListFilterOption>.addNoConflicts(option: ListFilterOption) {
add(option)
if (option is ListFilterOption.Inverted) {
remove(option.option)
} else {
removeIf { it is ListFilterOption.Inverted && it.option == option }
}
}
}

View File

@@ -57,8 +57,8 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
override fun onChipClick(chip: Chip, data: Any?) {
when (data) {
is ContentType -> viewModel.setTypeChecked(data, chip.isChecked)
is Locale -> viewModel.setLocaleChecked(data, chip.isChecked)
is ContentType -> viewModel.setTypeChecked(data, !chip.isChecked)
is Locale -> viewModel.setLocaleChecked(data, !chip.isChecked)
}
}
@@ -92,7 +92,6 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
value.availableItems.map {
ChipsView.ChipModel(
title = it.getDisplayName(chips.context),
isCheckable = true,
isChecked = it in value.selectedItems,
data = it,
)
@@ -106,7 +105,6 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
value.availableItems.map {
ChipsView.ChipModel(
title = getString(it.titleResId),
isCheckable = true,
isChecked = it in value.selectedItems,
data = it,
)

View File

@@ -82,8 +82,8 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
override fun onChipClick(chip: Chip, data: Any?) {
when (data) {
is ContentType -> viewModel.setContentType(data, chip.isChecked)
is Boolean -> viewModel.setNewOnly(chip.isChecked)
is ContentType -> viewModel.setContentType(data, !chip.isChecked)
is Boolean -> viewModel.setNewOnly(!chip.isChecked)
else -> showLocalesMenu(chip)
}
}
@@ -121,8 +121,7 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
if (hasNewSources) {
chips += ChipModel(
title = getString(R.string._new),
icon = R.drawable.ic_updated_selector,
isCheckable = true,
icon = R.drawable.ic_updated,
isChecked = appliedFilter.isNewOnly,
data = true,
)
@@ -133,7 +132,6 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
}
chips += ChipModel(
title = getString(type.titleResId),
isCheckable = true,
isChecked = type in appliedFilter.types,
data = type,
)

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.suggestions.domain
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.MangaListQuickFilter
@@ -16,6 +17,14 @@ class SuggestionsListQuickFilter @Inject constructor(
}
if (!settings.isNsfwContentDisabled && !settings.isSuggestionsExcludeNsfw) {
add(ListFilterOption.Macro.NSFW)
add(
ListFilterOption.Inverted(
option = ListFilterOption.Macro.NSFW,
iconResId = R.drawable.ic_sfw,
titleResId = R.string.sfw,
titleText = null,
),
)
}
}
}

View File

@@ -0,0 +1,24 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="0.72"
android:viewportHeight="0.72">
<clip-path android:pathData="M0,0L0,0.72L0.72,0.72L0.72,0L0,0zM0.12164,0.00691L0.71326,0.59883L0.67518,0.63691L0.08326,0.045L0.12164,0.00691z" />
<path
android:fillColor="#0f0f0f"
android:pathData="M0.14033,0.03836C0.13258,0.02803 0.1191,0.02382 0.10685,0.0279 0.0946,0.03198 0.08634,0.04344 0.08634,0.05635l0,0.23993c0,0.01656 0.01343,0.02999 0.02999,0.02999 0.01656,0 0.02999,-0.01343 0.02999,-0.02999L0.14632,0.14633l0.12596,0.16795c0.00775,0.01033 0.02123,0.01454 0.03348,0.01045C0.31801,0.32065 0.32627,0.30919 0.32627,0.29628L0.32627,0.05635c0,-0.01656 -0.01343,-0.02999 -0.02999,-0.02999 -0.01656,0 -0.02999,0.01343 -0.02999,0.02999l0,0.14995z" />
<path
android:fillColor="#0f0f0f"
android:pathData="m0.44623,0.02636c-0.03313,0 -0.05998,0.02685 -0.05998,0.05998l0,0.05998c0,0.03313 0.02685,0.05998 0.05998,0.05998l0.11996,0l0,0.05998l-0.14995,0c-0.01656,0 -0.02999,0.01343 -0.02999,0.02999 0,0.01656 0.01343,0.02999 0.02999,0.02999l0.14995,0c0.03313,0 0.05998,-0.02686 0.05998,-0.05998l0,-0.05998c0,-0.03313 -0.02685,-0.05998 -0.05998,-0.05998l-0.11996,0l0,-0.05998l0.14995,0c0.01656,0 0.02999,-0.01343 0.02999,-0.02999 0,-0.01656 -0.01343,-0.02999 -0.02999,-0.02999z" />
<path
android:fillColor="#0f0f0f"
android:pathData="m0.08634,0.41624c0,-0.01656 0.01343,-0.02999 0.02999,-0.02999l0.17994,0c0.01656,0 0.02999,0.01343 0.02999,0.02999 0,0.01656 -0.01343,0.02999 -0.02999,0.02999L0.14632,0.44623l0,0.05998l0.14995,0c0.01656,0 0.02999,0.01343 0.02999,0.02999 0,0.01656 -0.01343,0.02999 -0.02999,0.02999L0.14632,0.5662l0,0.08997c0,0.01656 -0.01343,0.02999 -0.02999,0.02999 -0.01656,0 -0.02999,-0.01343 -0.02999,-0.02999z" />
<path
android:fillColor="#0f0f0f"
android:pathData="m0.44623,0.41624c0,-0.01656 -0.01343,-0.02999 -0.02999,-0.02999 -0.01656,0 -0.02999,0.01343 -0.02999,0.02999l0,0.23993c0,0.01348 0.00899,0.0253 0.02199,0.0289 0.01299,0.0036 0.02678,-0.00191 0.03372,-0.01347L0.50621,0.5645l0.06425,0.10709c0.00694,0.01156 0.02073,0.01707 0.03372,0.01347C0.61718,0.68147 0.62617,0.66965 0.62617,0.65617l0,-0.23993c0,-0.01656 -0.01343,-0.02999 -0.02999,-0.02999 -0.01656,0 -0.02999,0.01343 -0.02999,0.02999l0,0.13165L0.53193,0.49078C0.52651,0.48175 0.51675,0.47622 0.50621,0.47622c-0.01054,0 -0.0203,0.00553 -0.02572,0.01456L0.44623,0.54789Z" />
<path
android:fillColor="#0f0f0f"
android:pathData="M0.045,0.0831 L0.0834,0.045 0.675,0.6369 0.6369,0.675Z" />
</vector>

View File

@@ -675,4 +675,5 @@
<string name="invalid_proxy_configuration">Invalid proxy configuration</string>
<string name="show_quick_filters">Show quick filters</string>
<string name="show_quick_filters_summary">Provides the ability to filter manga lists by certain parameters</string>
<string name="sfw">SFW</string>
</resources>