From b73e44874d27cefd5db8455000af03de81583b72 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 21 Sep 2024 09:11:58 +0300 Subject: [PATCH] Add new filter fields --- .../org/koitharu/kotatsu/core/model/Manga.kt | 11 ++ .../koitharu/kotatsu/core/util/ext/View.kt | 12 ++ .../kotatsu/filter/ui/FilterCoordinator.kt | 23 ++++ .../filter/ui/sheet/FilterSheetFragment.kt | 102 +++++++++++++++- app/src/main/res/layout/sheet_filter.xml | 112 +++++++++++++++++- app/src/main/res/values/strings.xml | 5 + 6 files changed, 256 insertions(+), 9 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt index 4114c6f58..aa5bd3330 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt @@ -9,6 +9,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.util.ext.iterator import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.parsers.model.ContentRating +import org.koitharu.kotatsu.parsers.model.Demographic import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaState @@ -68,6 +69,16 @@ val ContentRating.titleResId: Int ContentRating.ADULT -> R.string.rating_adult } +@get:StringRes +val Demographic.titleResId: Int + get() = when (this) { + Demographic.SHOUNEN -> R.string.demographic_shounen + Demographic.SHOUJO -> R.string.demographic_shoujo + Demographic.SEINEN -> R.string.demographic_seinen + Demographic.JOSEI -> R.string.demographic_josei + Demographic.NONE -> R.string.none + } + fun Manga.findChapter(id: Long): MangaChapter? { return chapters?.findById(id) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt index 3a6b9f1ca..f995e098e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt @@ -18,6 +18,7 @@ import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip import com.google.android.material.progressindicator.BaseProgressIndicator +import com.google.android.material.slider.RangeSlider import com.google.android.material.slider.Slider import com.google.android.material.tabs.TabLayout import kotlin.math.roundToInt @@ -88,6 +89,17 @@ fun Slider.setValueRounded(newValue: Float) { value = roundedValue.coerceIn(valueFrom, valueTo) } +fun RangeSlider.setValuesRounded(vararg newValues: Float) { + val step = stepSize + values = newValues.map { newValue -> + if (step <= 0f) { + newValue + } else { + (newValue / step).roundToInt() * step + } + } +} + fun RecyclerView.invalidateNestedItemDecorations() { descendants.filterIsInstance().forEach { it.invalidateItemDecorations() 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 c2680c6de..9aa8de63a 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 @@ -67,6 +67,9 @@ class FilterCoordinator @Inject constructor( val isFilterApplied: Boolean get() = !currentListFilter.value.isEmpty() + val query: StateFlow = currentListFilter.map { it.query } + .stateIn(coroutineScope, SharingStarted.Lazily, null) + val sortOrder: StateFlow> = currentSortOrder.map { selected -> FilterProperty( availableItems = availableSortOrders.sortedByOrdinal(), @@ -261,6 +264,12 @@ class FilterCoordinator @Inject constructor( currentListFilter.value = value } + fun setQuery(value: String?) { + currentListFilter.update { oldValue -> + oldValue.copy(query = value?.trim()?.takeUnless { it.isEmpty() }) + } + } + fun setLocale(value: Locale?) { currentListFilter.update { oldValue -> oldValue.copy(locale = value) @@ -273,6 +282,12 @@ class FilterCoordinator @Inject constructor( } } + fun setYearRange(valueFrom: Int, valueTo: Int) { + currentListFilter.update { oldValue -> + oldValue.copy(yearFrom = valueFrom, yearTo = valueTo) + } + } + fun toggleState(value: MangaState, isSelected: Boolean) { currentListFilter.update { oldValue -> oldValue.copy( @@ -289,6 +304,14 @@ class FilterCoordinator @Inject constructor( } } + fun toggleContentType(value: ContentType, isSelected: Boolean) { + currentListFilter.update { oldValue -> + oldValue.copy( + types = if (isSelected) oldValue.types + value else oldValue.types - value, + ) + } + } + fun toggleTag(value: MangaTag, isSelected: Boolean) { currentListFilter.update { oldValue -> val newTags = if (capabilities.isMultipleTagsSupported) { 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 84dfd9034..282361ddb 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 @@ -13,6 +13,8 @@ import androidx.core.view.updatePadding import androidx.fragment.app.FragmentManager import com.google.android.material.button.MaterialButtonToggleGroup import com.google.android.material.chip.Chip +import com.google.android.material.slider.BaseOnChangeListener +import com.google.android.material.slider.RangeSlider import com.google.android.material.slider.Slider import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.SortDirection @@ -25,6 +27,7 @@ import org.koitharu.kotatsu.core.util.ext.getDisplayName import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.parentView import org.koitharu.kotatsu.core.util.ext.setValueRounded +import org.koitharu.kotatsu.core.util.ext.setValuesRounded import org.koitharu.kotatsu.core.util.ext.showDistinct import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.SheetFilterBinding @@ -32,16 +35,19 @@ import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.filter.ui.model.FilterProperty import org.koitharu.kotatsu.filter.ui.tags.TagsCatalogSheet 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.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.YEAR_UNKNOWN +import org.koitharu.kotatsu.parsers.util.toIntUp import java.util.Locale import com.google.android.material.R as materialR class FilterSheetFragment : BaseAdaptiveSheet(), AdapterView.OnItemSelectedListener, - ChipsView.OnChipClickListener, MaterialButtonToggleGroup.OnButtonCheckedListener, Slider.OnChangeListener { + ChipsView.OnChipClickListener, MaterialButtonToggleGroup.OnButtonCheckedListener { override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding { return SheetFilterBinding.inflate(inflater, container, false) @@ -62,16 +68,21 @@ class FilterSheetFragment : BaseAdaptiveSheet(), filter.tags.observe(viewLifecycleOwner, this::onTagsChanged) filter.tagsExcluded.observe(viewLifecycleOwner, this::onTagsExcludedChanged) filter.states.observe(viewLifecycleOwner, this::onStateChanged) + filter.contentTypes.observe(viewLifecycleOwner, this::onContentTypesChanged) filter.contentRating.observe(viewLifecycleOwner, this::onContentRatingChanged) + filter.demographics.observe(viewLifecycleOwner, this::onDemographicsChanged) filter.year.observe(viewLifecycleOwner, this::onYearChanged) + filter.yearRange.observe(viewLifecycleOwner, this::onYearRangeChanged) binding.spinnerLocale.onItemSelectedListener = this binding.spinnerOrder.onItemSelectedListener = this binding.chipsState.onChipClickListener = this + binding.chipsTypes.onChipClickListener = this binding.chipsContentRating.onChipClickListener = this binding.chipsGenres.onChipClickListener = this binding.chipsGenresExclude.onChipClickListener = this - binding.sliderYear.addOnChangeListener(this) + binding.sliderYear.addOnChangeListener(this::onSliderValueChange) + binding.sliderYearsRange.addOnChangeListener(this::onRangeSliderValueChange) binding.layoutSortDirection.addOnButtonCheckedListener(this) } @@ -95,14 +106,33 @@ class FilterSheetFragment : BaseAdaptiveSheet(), override fun onNothingSelected(parent: AdapterView<*>?) = Unit - override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { + private fun onSliderValueChange(slider: Slider, value: Float, fromUser: Boolean) { if (!fromUser) { return } val intValue = value.toInt() val filter = requireFilter() when (slider.id) { - R.id.slider_year -> filter.setYear(intValue) + R.id.slider_year -> filter.setYear( + if (intValue <= slider.valueFrom.toIntUp()) { + YEAR_UNKNOWN + } else { + intValue + }, + ) + } + } + + private fun onRangeSliderValueChange(slider: RangeSlider, value: Float, fromUser: Boolean) { + if (!fromUser) { + return + } + val filter = requireFilter() + when (slider.id) { + R.id.slider_yearsRange -> filter.setYearRange( + valueFrom = slider.valueFrom.toInt(), + valueTo = slider.valueTo.toInt(), + ) } } @@ -116,6 +146,7 @@ class FilterSheetFragment : BaseAdaptiveSheet(), filter.toggleTag(data, !chip.isChecked) } + is ContentType -> filter.toggleContentType(data, !chip.isChecked) is ContentRating -> filter.toggleContentRating(data, !chip.isChecked) null -> TagsCatalogSheet.show(getChildFragmentManager(), chip.parentView?.id == R.id.chips_genresExclude) } @@ -270,6 +301,23 @@ class FilterSheetFragment : BaseAdaptiveSheet(), b.chipsState.setChips(chips) } + private fun onContentTypesChanged(value: FilterProperty) { + val b = viewBinding ?: return + b.textViewTypesTitle.isGone = value.isEmpty() + b.chipsTypes.isGone = value.isEmpty() + if (value.isEmpty()) { + return + } + val chips = value.availableItems.map { type -> + ChipsView.ChipModel( + title = getString(type.titleResId), + isChecked = type in value.selectedItems, + data = type, + ) + } + b.chipsTypes.setChips(chips) + } + private fun onContentRatingChanged(value: FilterProperty) { val b = viewBinding ?: return b.textViewContentRatingTitle.isGone = value.isEmpty() @@ -287,16 +335,58 @@ class FilterSheetFragment : BaseAdaptiveSheet(), b.chipsContentRating.setChips(chips) } + private fun onDemographicsChanged(value: FilterProperty) { + val b = viewBinding ?: return + b.textViewDemographicsTitle.isGone = value.isEmpty() + b.chipsDemographics.isGone = value.isEmpty() + if (value.isEmpty()) { + return + } + val chips = value.availableItems.map { demographic -> + ChipsView.ChipModel( + title = getString(demographic.titleResId), + isChecked = demographic in value.selectedItems, + data = demographic, + ) + } + b.chipsDemographics.setChips(chips) + } + private fun onYearChanged(value: FilterProperty) { val b = viewBinding ?: return - b.textViewYear.isGone = value.isEmpty() + b.headerYear.isGone = value.isEmpty() b.sliderYear.isGone = value.isEmpty() if (value.isEmpty()) { return } + val currentValue = value.selectedItems.singleOrNull() ?: YEAR_UNKNOWN + b.textViewYearValue.text = if (currentValue == YEAR_UNKNOWN) { + getString(R.string.none) + } else { + currentValue.toString() + } b.sliderYear.valueFrom = value.availableItems.first().toFloat() b.sliderYear.valueTo = value.availableItems.last().toFloat() - b.sliderYear.setValueRounded((value.selectedItems.singleOrNull() ?: YEAR_UNKNOWN).toFloat()) + b.sliderYear.setValueRounded(currentValue.toFloat()) + } + + private fun onYearRangeChanged(value: FilterProperty) { + val b = viewBinding ?: return + b.headerYearsRange.isGone = value.isEmpty() + b.sliderYearsRange.isGone = value.isEmpty() + if (value.isEmpty()) { + return + } + b.sliderYearsRange.valueFrom = value.availableItems.first().toFloat() + b.sliderYearsRange.valueTo = value.availableItems.last().toFloat() + val currentValueFrom = value.selectedItems.firstOrNull()?.toFloat() ?: b.sliderYearsRange.valueFrom + val currentValueTo = value.selectedItems.lastOrNull()?.toFloat() ?: b.sliderYearsRange.valueTo + b.textViewYearsRangeValue.text = getString( + R.string.memory_usage_pattern, + currentValueFrom.toString(), + currentValueTo.toString(), + ) + b.sliderYearsRange.setValuesRounded(currentValueFrom, currentValueTo) } private fun requireFilter() = (requireActivity() as FilterCoordinator.Owner).filterCoordinator diff --git a/app/src/main/res/layout/sheet_filter.xml b/app/src/main/res/layout/sheet_filter.xml index 1c9860652..2ad6ab344 100644 --- a/app/src/main/res/layout/sheet_filter.xml +++ b/app/src/main/res/layout/sheet_filter.xml @@ -204,6 +204,26 @@ app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" tools:visibility="visible" /> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 962b8d8f9..b219423ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -719,4 +719,9 @@ Popular this year Original language Year + Demographics + Shounen + Shoujo + Seinen + Josei