Add new filter fields

This commit is contained in:
Koitharu
2024-09-21 09:11:58 +03:00
parent 6f45a44070
commit b73e44874d
6 changed files with 256 additions and 9 deletions

View File

@@ -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)
}

View File

@@ -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<RecyclerView>().forEach {
it.invalidateItemDecorations()

View File

@@ -67,6 +67,9 @@ class FilterCoordinator @Inject constructor(
val isFilterApplied: Boolean
get() = !currentListFilter.value.isEmpty()
val query: StateFlow<String?> = currentListFilter.map { it.query }
.stateIn(coroutineScope, SharingStarted.Lazily, null)
val sortOrder: StateFlow<FilterProperty<SortOrder>> = 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) {

View File

@@ -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<SheetFilterBinding>(),
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<SheetFilterBinding>(),
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<SheetFilterBinding>(),
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<SheetFilterBinding>(),
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<SheetFilterBinding>(),
b.chipsState.setChips(chips)
}
private fun onContentTypesChanged(value: FilterProperty<ContentType>) {
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<ContentRating>) {
val b = viewBinding ?: return
b.textViewContentRatingTitle.isGone = value.isEmpty()
@@ -287,16 +335,58 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
b.chipsContentRating.setChips(chips)
}
private fun onDemographicsChanged(value: FilterProperty<Demographic>) {
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<Int>) {
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<Int>) {
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

View File

@@ -204,6 +204,26 @@
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_types_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/type"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_types"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:visibility="gone"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_state_title"
android:layout_width="match_parent"
@@ -245,16 +265,55 @@
tools:visibility="visible" />
<TextView
android:id="@+id/textView_year"
android:id="@+id/textView_demographics_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/year"
android:text="@string/demographics"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_demographics"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:visibility="gone"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/header_year"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:baselineAligned="true"
android:baselineAlignedChildIndex="0"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/textView_year"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:text="@string/year"
android:textAppearance="?textAppearanceTitleSmall" />
<TextView
android:id="@+id/textView_year_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?textAppearanceBodyMedium"
tools:text="2024" />
</LinearLayout>
<com.google.android.material.slider.Slider
android:id="@+id/slider_year"
android:layout_width="match_parent"
@@ -262,8 +321,55 @@
android:layout_marginTop="@dimen/margin_normal"
android:stepSize="1"
android:visibility="gone"
app:labelBehavior="visible"
app:labelBehavior="gone"
app:tickVisible="true"
tools:value="2020"
tools:valueFrom="1900"
tools:valueTo="2090"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/header_yearsRange"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:baselineAligned="true"
android:baselineAlignedChildIndex="0"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/textView_yearsRange"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:text="@string/year"
android:textAppearance="?textAppearanceTitleSmall" />
<TextView
android:id="@+id/textView_yearsRange_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?textAppearanceBodyMedium"
tools:text="2024" />
</LinearLayout>
<com.google.android.material.slider.RangeSlider
android:id="@+id/slider_yearsRange"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:stepSize="1"
android:visibility="gone"
app:labelBehavior="gone"
app:tickVisible="true"
tools:valueFrom="1900"
tools:valueTo="2090"
tools:visibility="visible" />
</LinearLayout>

View File

@@ -719,4 +719,9 @@
<string name="popular_in_year">Popular this year</string>
<string name="original_language">Original language</string>
<string name="year">Year</string>
<string name="demographics">Demographics</string>
<string name="demographic_shounen">Shounen</string>
<string name="demographic_shoujo">Shoujo</string>
<string name="demographic_seinen">Seinen</string>
<string name="demographic_josei">Josei</string>
</resources>