diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaQueryBuilder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaQueryBuilder.kt index 16c99b563..d9823ba13 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaQueryBuilder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaQueryBuilder.kt @@ -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 { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt index 66a5e9227..873746bce 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt @@ -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 getCheckedData(cls: Class): Set { - val result = LinkedHashSet(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?) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt index 8ab368661..51d5761f2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt @@ -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 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt index 304a0be28..bd5ea3c17 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt @@ -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, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt index c7a6f0ef9..1df723a18 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt @@ -39,7 +39,7 @@ class FilterHeaderFragment : BaseFragment(), ChipsV if (tag == null) { TagsCatalogSheet.show(parentFragmentManager, isExcludeTag = false) } else { - filter.setTag(tag, chip.isChecked) + filter.setTag(tag, !chip.isChecked) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt index f677603b4..8e210d988 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt @@ -78,14 +78,14 @@ class FilterSheetFragment : BaseAdaptiveSheet(), 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(), value.selectedItems.mapTo(chips) { tag -> ChipsView.ChipModel( title = tag.title, - isCheckable = true, isChecked = true, data = tag, ) @@ -151,7 +150,6 @@ class FilterSheetFragment : BaseAdaptiveSheet(), if (tag !in value.selectedItems) { ChipsView.ChipModel( title = tag.title, - isCheckable = true, isChecked = false, data = tag, ) @@ -181,7 +179,6 @@ class FilterSheetFragment : BaseAdaptiveSheet(), tint = 0, title = tag.title, icon = 0, - isCheckable = true, isChecked = true, data = tag, ) @@ -190,7 +187,6 @@ class FilterSheetFragment : BaseAdaptiveSheet(), if (tag !in value.selectedItems) { ChipsView.ChipModel( title = tag.title, - isCheckable = true, isChecked = false, data = tag, ) @@ -217,7 +213,6 @@ class FilterSheetFragment : BaseAdaptiveSheet(), 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(), val chips = value.availableItems.map { contentRating -> ChipsView.ChipModel( title = getString(contentRating.titleResId), - isCheckable = true, isChecked = contentRating in value.selectedItems, data = contentRating, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt index b8d335a64..1389e6524 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt @@ -153,12 +153,12 @@ abstract class HistoryDao : MangaQueryBuilder.ConditionCallback { protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow> 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 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt index 2e77f7ebf..9c0c2f702 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt @@ -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 + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt index f0f544b8b..9b224cb2f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt @@ -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 + + private fun ArraySet.addNoConflicts(option: ListFilterOption) { + add(option) + if (option is ListFilterOption.Inverted) { + remove(option.option) + } else { + removeIf { it is ListFilterOption.Inverted && it.option == option } + } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeSheet.kt index e8f0b95fd..50bed1ffc 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeSheet.kt @@ -57,8 +57,8 @@ class WelcomeSheet : BaseAdaptiveSheet(), 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(), 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(), ChipsView.OnChipC value.availableItems.map { ChipsView.ChipModel( title = getString(it.titleResId), - isCheckable = true, isChecked = it in value.selectedItems, data = it, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt index bc613b0ea..30005249b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt @@ -82,8 +82,8 @@ class SourcesCatalogActivity : BaseActivity(), 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(), 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(), } chips += ChipModel( title = getString(type.titleResId), - isCheckable = true, isChecked = type in appliedFilter.types, data = type, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionsListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionsListQuickFilter.kt index 172fa7291..d75cb9520 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionsListQuickFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionsListQuickFilter.kt @@ -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, + ), + ) } } } diff --git a/app/src/main/res/drawable/ic_sfw.xml b/app/src/main/res/drawable/ic_sfw.xml new file mode 100644 index 000000000..f8dc8abd0 --- /dev/null +++ b/app/src/main/res/drawable/ic_sfw.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 82fe8cdd6..25edfd29e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -675,4 +675,5 @@ Invalid proxy configuration Show quick filters Provides the ability to filter manga lists by certain parameters + SFW