Support ascending/descending sort orders

This commit is contained in:
Koitharu
2024-08-29 07:40:53 +03:00
parent 05b5953f35
commit c70e3547d1
14 changed files with 264 additions and 62 deletions

View File

@@ -83,7 +83,7 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:2e138da3d5') { implementation('com.github.KotatsuApp:kotatsu-parsers:7528480f54') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }

View File

@@ -0,0 +1,31 @@
package org.koitharu.kotatsu.core.model
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.model.SortOrder
enum class GenericSortOrder(
@StringRes val titleResId: Int,
val ascending: SortOrder,
val descending: SortOrder,
) {
UPDATED(R.string.updated, SortOrder.UPDATED_ASC, SortOrder.UPDATED),
RATING(R.string.by_rating, SortOrder.RATING_ASC, SortOrder.RATING),
POPULARITY(R.string.popularity, SortOrder.POPULARITY_ASC, SortOrder.POPULARITY),
DATE(R.string.by_date, SortOrder.NEWEST_ASC, SortOrder.NEWEST),
NAME(R.string.by_name, SortOrder.ALPHABETICAL, SortOrder.ALPHABETICAL_DESC),
;
operator fun get(direction: SortDirection): SortOrder = when (direction) {
SortDirection.ASC -> ascending
SortDirection.DESC -> descending
}
companion object {
fun of(order: SortOrder): GenericSortOrder = entries.first { e ->
e.ascending == order || e.descending == order
}
}
}

View File

@@ -0,0 +1,6 @@
package org.koitharu.kotatsu.core.model
enum class SortDirection {
ASC, DESC;
}

View File

@@ -31,7 +31,7 @@ class ExternalPluginContentSource(
@Blocking @Blocking
@WorkerThread @WorkerThread
fun getList(offset: Int, filter: MangaListFilter?): List<Manga> = runCatchingCompatibility { fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val uri = "content://${source.authority}/manga".toUri().buildUpon() val uri = "content://${source.authority}/manga".toUri().buildUpon()
uri.appendQueryParameter("offset", offset.toString()) uri.appendQueryParameter("offset", offset.toString())
when (filter) { when (filter) {
@@ -49,7 +49,7 @@ class ExternalPluginContentSource(
null -> Unit null -> Unit
} }
contentResolver.query(uri.build(), null, null, null, filter?.sortOrder?.name) return contentResolver.query(uri.build(), null, null, null, filter?.sortOrder?.name)
.safe() .safe()
.use { cursor -> .use { cursor ->
val result = ArrayList<Manga>(cursor.count) val result = ArrayList<Manga>(cursor.count)
@@ -64,10 +64,10 @@ class ExternalPluginContentSource(
@Blocking @Blocking
@WorkerThread @WorkerThread
fun getDetails(manga: Manga) = runCatchingCompatibility { fun getDetails(manga: Manga): Manga {
val chapters = queryChapters(manga.url) val chapters = queryChapters(manga.url)
val details = queryDetails(manga.url) val details = queryDetails(manga.url)
Manga( return Manga(
id = manga.id, id = manga.id,
title = details.title.ifBlank { manga.title }, title = details.title.ifBlank { manga.title },
altTitle = details.altTitle.ifNullOrEmpty { manga.altTitle }, altTitle = details.altTitle.ifNullOrEmpty { manga.altTitle },
@@ -88,12 +88,12 @@ class ExternalPluginContentSource(
@Blocking @Blocking
@WorkerThread @WorkerThread
fun getPages(chapter: MangaChapter): List<MangaPage> = runCatchingCompatibility { fun getPages(chapter: MangaChapter): List<MangaPage> {
val uri = "content://${source.authority}/chapters".toUri() val uri = "content://${source.authority}/chapters".toUri()
.buildUpon() .buildUpon()
.appendPath(chapter.url) .appendPath(chapter.url)
.build() .build()
contentResolver.query(uri, null, null, null, null) return contentResolver.query(uri, null, null, null, null)
.safe() .safe()
.use { cursor -> .use { cursor ->
val result = ArrayList<MangaPage>(cursor.count) val result = ArrayList<MangaPage>(cursor.count)
@@ -113,9 +113,9 @@ class ExternalPluginContentSource(
@Blocking @Blocking
@WorkerThread @WorkerThread
fun getTags(): Set<MangaTag> = runCatchingCompatibility { fun getTags(): Set<MangaTag> {
val uri = "content://${source.authority}/tags".toUri() val uri = "content://${source.authority}/tags".toUri()
contentResolver.query(uri, null, null, null, null) return contentResolver.query(uri, null, null, null, null)
.safe() .safe()
.use { cursor -> .use { cursor ->
val result = ArraySet<MangaTag>(cursor.count) val result = ArraySet<MangaTag>(cursor.count)
@@ -212,7 +212,7 @@ class ExternalPluginContentSource(
} }
} }
private fun SafeCursor.getManga() = Manga( private fun ExternalPluginCursor.getManga() = Manga(
id = getLong(COLUMN_ID), id = getLong(COLUMN_ID),
title = getString(COLUMN_TITLE), title = getString(COLUMN_TITLE),
altTitle = getStringOrNull(COLUMN_ALT_TITLE), altTitle = getStringOrNull(COLUMN_ALT_TITLE),
@@ -233,15 +233,10 @@ class ExternalPluginContentSource(
source = source, source = source,
) )
private inline fun <R> runCatchingCompatibility(block: () -> R): R = try { private fun Cursor?.safe() = ExternalPluginCursor(
block() source = source,
} catch (e: NoSuchElementException) { // unknown column name cursor = this ?: throw IncompatiblePluginException(source.name, null),
throw IncompatiblePluginException(source.name, e) )
} catch (e: IllegalArgumentException) {
throw IncompatiblePluginException(source.name, e)
}
private fun Cursor?.safe() = SafeCursor(this ?: throw IncompatiblePluginException(source.name, null))
class MangaSourceCapabilities( class MangaSourceCapabilities(
val availableSortOrders: Set<SortOrder>, val availableSortOrders: Set<SortOrder>,

View File

@@ -2,14 +2,19 @@ package org.koitharu.kotatsu.core.parser.external
import android.database.Cursor import android.database.Cursor
import android.database.CursorWrapper import android.database.CursorWrapper
import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException
import org.koitharu.kotatsu.core.util.ext.getBoolean import org.koitharu.kotatsu.core.util.ext.getBoolean
class SafeCursor(cursor: Cursor) : CursorWrapper(cursor) { class ExternalPluginCursor(private val source: ExternalMangaSource, cursor: Cursor) : CursorWrapper(cursor) {
fun getString(columnName: String): String { override fun getColumnIndexOrThrow(columnName: String?): Int = try {
return getString(getColumnIndexOrThrow(columnName)) super.getColumnIndexOrThrow(columnName)
} catch (e: Exception) {
throw IncompatiblePluginException(source.name, e)
} }
fun getString(columnName: String): String = getString(getColumnIndexOrThrow(columnName))
fun getStringOrNull(columnName: String): String? { fun getStringOrNull(columnName: String): String? {
val columnIndex = getColumnIndex(columnName) val columnIndex = getColumnIndex(columnName)
return when { return when {
@@ -19,9 +24,7 @@ class SafeCursor(cursor: Cursor) : CursorWrapper(cursor) {
} }
} }
fun getBoolean(columnName: String): Boolean { fun getBoolean(columnName: String): Boolean = getBoolean(getColumnIndexOrThrow(columnName))
return getBoolean(getColumnIndexOrThrow(columnName))
}
fun getBooleanOrDefault(columnName: String, defaultValue: Boolean): Boolean { fun getBooleanOrDefault(columnName: String, defaultValue: Boolean): Boolean {
val columnIndex = getColumnIndex(columnName) val columnIndex = getColumnIndex(columnName)
@@ -32,9 +35,7 @@ class SafeCursor(cursor: Cursor) : CursorWrapper(cursor) {
} }
} }
fun getInt(columnName: String): Int { fun getInt(columnName: String): Int = getInt(getColumnIndexOrThrow(columnName))
return getInt(getColumnIndexOrThrow(columnName))
}
fun getIntOrDefault(columnName: String, defaultValue: Int): Int { fun getIntOrDefault(columnName: String, defaultValue: Int): Int {
val columnIndex = getColumnIndex(columnName) val columnIndex = getColumnIndex(columnName)
@@ -45,9 +46,7 @@ class SafeCursor(cursor: Cursor) : CursorWrapper(cursor) {
} }
} }
fun getLong(columnName: String): Long { fun getLong(columnName: String): Long = getLong(getColumnIndexOrThrow(columnName))
return getLong(getColumnIndexOrThrow(columnName))
}
fun getLongOrDefault(columnName: String, defaultValue: Long): Long { fun getLongOrDefault(columnName: String, defaultValue: Long): Long {
val columnIndex = getColumnIndex(columnName) val columnIndex = getColumnIndex(columnName)
@@ -58,9 +57,7 @@ class SafeCursor(cursor: Cursor) : CursorWrapper(cursor) {
} }
} }
fun getFloat(columnName: String): Float { fun getFloat(columnName: String): Float = getFloat(getColumnIndexOrThrow(columnName))
return getFloat(getColumnIndexOrThrow(columnName))
}
fun getFloatOrDefault(columnName: String, defaultValue: Float): Float { fun getFloatOrDefault(columnName: String, defaultValue: Float): Float {
val columnIndex = getColumnIndex(columnName) val columnIndex = getColumnIndex(columnName)

View File

@@ -2,15 +2,45 @@ package org.koitharu.kotatsu.core.ui.model
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.SortDirection
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.model.SortOrder.ALPHABETICAL
import org.koitharu.kotatsu.parsers.model.SortOrder.ALPHABETICAL_DESC
import org.koitharu.kotatsu.parsers.model.SortOrder.NEWEST
import org.koitharu.kotatsu.parsers.model.SortOrder.NEWEST_ASC
import org.koitharu.kotatsu.parsers.model.SortOrder.POPULARITY
import org.koitharu.kotatsu.parsers.model.SortOrder.POPULARITY_ASC
import org.koitharu.kotatsu.parsers.model.SortOrder.RATING
import org.koitharu.kotatsu.parsers.model.SortOrder.RATING_ASC
import org.koitharu.kotatsu.parsers.model.SortOrder.UPDATED
import org.koitharu.kotatsu.parsers.model.SortOrder.UPDATED_ASC
@get:StringRes @get:StringRes
val SortOrder.titleRes: Int val SortOrder.titleRes: Int
get() = when (this) { get() = when (this) {
SortOrder.UPDATED -> R.string.updated UPDATED -> R.string.updated
SortOrder.POPULARITY -> R.string.popular POPULARITY -> R.string.popular
SortOrder.RATING -> R.string.by_rating RATING -> R.string.by_rating
SortOrder.NEWEST -> R.string.newest NEWEST -> R.string.newest
SortOrder.ALPHABETICAL -> R.string.by_name ALPHABETICAL -> R.string.by_name
SortOrder.ALPHABETICAL_DESC -> R.string.by_name_reverse ALPHABETICAL_DESC -> R.string.by_name_reverse
UPDATED_ASC -> R.string.updated_long_ago
POPULARITY_ASC -> R.string.unpopular
RATING_ASC -> R.string.low_rating
NEWEST_ASC -> R.string.order_oldest
}
val SortOrder.direction: SortDirection
get() = when (this) {
UPDATED_ASC,
POPULARITY_ASC,
RATING_ASC,
NEWEST_ASC,
ALPHABETICAL -> SortDirection.ASC
UPDATED,
POPULARITY,
RATING,
NEWEST,
ALPHABETICAL_DESC -> SortDirection.DESC
} }

View File

@@ -24,14 +24,18 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.GenericSortOrder
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.SortDirection
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.model.direction
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.LocaleComparator import org.koitharu.kotatsu.core.util.LocaleComparator
import org.koitharu.kotatsu.core.util.ext.asArrayList import org.koitharu.kotatsu.core.util.ext.asArrayList
import org.koitharu.kotatsu.core.util.ext.lifecycleScope import org.koitharu.kotatsu.core.util.ext.lifecycleScope
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
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.filter.ui.model.TagCatalogItem import org.koitharu.kotatsu.filter.ui.model.TagCatalogItem
@@ -52,6 +56,7 @@ import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import java.text.Collator import java.text.Collator
import java.util.EnumSet
import java.util.LinkedList import java.util.LinkedList
import java.util.Locale import java.util.Locale
import java.util.TreeSet import java.util.TreeSet
@@ -131,24 +136,42 @@ class FilterCoordinator @Inject constructor(
MutableStateFlow(emptyProperty()) MutableStateFlow(emptyProperty())
} }
override val filterSortOrder: StateFlow<FilterProperty<SortOrder>> = combine( override val filterSortOrder: StateFlow<FilterProperty<GenericSortOrder>> =
currentState.distinctUntilChangedBy { it.sortOrder }, currentState.distinctUntilChangedBy { it.sortOrder }.map { state ->
flowOf(repository.sortOrders), val orders = repository.sortOrders
) { state, orders -> FilterProperty(
FilterProperty( availableItems = orders.mapTo(EnumSet.noneOf(GenericSortOrder::class.java)) {
availableItems = orders.sortedBy { it.ordinal }, GenericSortOrder.of(it)
selectedItems = setOf(state.sortOrder), }.sortedByOrdinal(),
isLoading = false, selectedItems = setOf(GenericSortOrder.of(state.sortOrder)),
error = null, isLoading = false,
) error = null,
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty()) )
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
override val filterSortDirection: StateFlow<FilterProperty<SortDirection>> =
currentState.distinctUntilChangedBy { it.sortOrder }.map { state ->
val orders = repository.sortOrders
FilterProperty(
availableItems = state.sortOrder.let {
val genericOrder = GenericSortOrder.of(it)
val result = EnumSet.noneOf(SortDirection::class.java)
if (genericOrder.ascending in orders) result.add(SortDirection.ASC)
if (genericOrder.descending in orders) result.add(SortDirection.DESC)
result
}?.sortedByOrdinal().orEmpty(),
selectedItems = setOf(state.sortOrder.direction),
isLoading = false,
error = null,
)
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
override val filterState: StateFlow<FilterProperty<MangaState>> = combine( override val filterState: StateFlow<FilterProperty<MangaState>> = combine(
currentState.distinctUntilChangedBy { it.states }, currentState.distinctUntilChangedBy { it.states },
flowOf(repository.states), flowOf(repository.states),
) { state, states -> ) { state, states ->
FilterProperty( FilterProperty(
availableItems = states.sortedBy { it.ordinal }, availableItems = states.sortedByOrdinal(),
selectedItems = state.states, selectedItems = state.states,
isLoading = false, isLoading = false,
error = null, error = null,
@@ -160,7 +183,7 @@ class FilterCoordinator @Inject constructor(
flowOf(repository.contentRatings), flowOf(repository.contentRatings),
) { rating, ratings -> ) { rating, ratings ->
FilterProperty( FilterProperty(
availableItems = ratings.sortedBy { it.ordinal }, availableItems = ratings.sortedByOrdinal(),
selectedItems = rating.contentRating, selectedItems = rating.contentRating,
isLoading = false, isLoading = false,
error = null, error = null,

View File

@@ -1,13 +1,14 @@
package org.koitharu.kotatsu.filter.ui package org.koitharu.kotatsu.filter.ui
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.core.model.GenericSortOrder
import org.koitharu.kotatsu.core.model.SortDirection
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.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.ContentRating import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.util.Locale import java.util.Locale
interface MangaFilter : OnFilterChangedListener { interface MangaFilter : OnFilterChangedListener {
@@ -18,7 +19,9 @@ interface MangaFilter : OnFilterChangedListener {
val filterTagsExcluded: StateFlow<FilterProperty<MangaTag>> val filterTagsExcluded: StateFlow<FilterProperty<MangaTag>>
val filterSortOrder: StateFlow<FilterProperty<SortOrder>> val filterSortOrder: StateFlow<FilterProperty<GenericSortOrder>>
val filterSortDirection: StateFlow<FilterProperty<SortDirection>>
val filterState: StateFlow<FilterProperty<MangaState>> val filterState: StateFlow<FilterProperty<MangaState>>

View File

@@ -7,13 +7,17 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.annotation.IdRes
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.GenericSortOrder
import org.koitharu.kotatsu.core.model.SortDirection
import org.koitharu.kotatsu.core.model.titleResId import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.ui.model.titleRes import org.koitharu.kotatsu.core.ui.model.direction
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
@@ -21,6 +25,7 @@ import org.koitharu.kotatsu.core.util.ext.getDisplayName
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.parentView import org.koitharu.kotatsu.core.util.ext.parentView
import org.koitharu.kotatsu.core.util.ext.showDistinct import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.SheetFilterBinding import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.filter.ui.FilterOwner import org.koitharu.kotatsu.filter.ui.FilterOwner
@@ -30,12 +35,14 @@ import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.mapToSet
import java.util.EnumSet
import java.util.Locale import java.util.Locale
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(), class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
AdapterView.OnItemSelectedListener, AdapterView.OnItemSelectedListener,
ChipsView.OnChipClickListener { ChipsView.OnChipClickListener, MaterialButtonToggleGroup.OnButtonCheckedListener {
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding { override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
return SheetFilterBinding.inflate(inflater, container, false) return SheetFilterBinding.inflate(inflater, container, false)
@@ -51,6 +58,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
} }
val filter = requireFilter() val filter = requireFilter()
filter.filterSortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged) filter.filterSortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged)
filter.filterSortDirection.observe(viewLifecycleOwner, this::onSortDirectionChanged)
filter.filterLocale.observe(viewLifecycleOwner, this::onLocaleChanged) filter.filterLocale.observe(viewLifecycleOwner, this::onLocaleChanged)
filter.filterTags.observe(viewLifecycleOwner, this::onTagsChanged) filter.filterTags.observe(viewLifecycleOwner, this::onTagsChanged)
filter.filterTagsExcluded.observe(viewLifecycleOwner, this::onTagsExcludedChanged) filter.filterTagsExcluded.observe(viewLifecycleOwner, this::onTagsExcludedChanged)
@@ -63,12 +71,24 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
binding.chipsContentRating.onChipClickListener = this binding.chipsContentRating.onChipClickListener = this
binding.chipsGenres.onChipClickListener = this binding.chipsGenres.onChipClickListener = this
binding.chipsGenresExclude.onChipClickListener = this binding.chipsGenresExclude.onChipClickListener = this
binding.layoutSortDirection.addOnButtonCheckedListener(this)
}
override fun onButtonChecked(group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean) {
if (isChecked) {
setSortDirection(getSortDirection(checkedId))
}
} }
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
val filter = requireFilter() val filter = requireFilter()
when (parent.id) { when (parent.id) {
R.id.spinner_order -> filter.setSortOrder(filter.filterSortOrder.value.availableItems[position]) R.id.spinner_order -> {
val genericOrder = filter.filterSortOrder.value.availableItems[position]
val direction = getSortDirection(requireViewBinding().layoutSortDirection.checkedButtonId)
filter.setSortOrder(genericOrder[direction])
}
R.id.spinner_locale -> filter.setLanguage(filter.filterLocale.value.availableItems[position]) R.id.spinner_locale -> filter.setLanguage(filter.filterLocale.value.availableItems[position])
} }
} }
@@ -90,7 +110,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
} }
} }
private fun onSortOrderChanged(value: FilterProperty<SortOrder>) { private fun onSortOrderChanged(value: FilterProperty<GenericSortOrder>) {
val b = viewBinding ?: return val b = viewBinding ?: return
b.textViewOrderTitle.isGone = value.isEmpty() b.textViewOrderTitle.isGone = value.isEmpty()
b.cardOrder.isGone = value.isEmpty() b.cardOrder.isGone = value.isEmpty()
@@ -102,7 +122,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
b.spinnerOrder.context, b.spinnerOrder.context,
android.R.layout.simple_spinner_dropdown_item, android.R.layout.simple_spinner_dropdown_item,
android.R.id.text1, android.R.id.text1,
value.availableItems.map { b.spinnerOrder.context.getString(it.titleRes) }, value.availableItems.map { b.spinnerOrder.context.getString(it.titleResId) },
) )
val selectedIndex = value.availableItems.indexOf(selected) val selectedIndex = value.availableItems.indexOf(selected)
if (selectedIndex >= 0) { if (selectedIndex >= 0) {
@@ -110,6 +130,25 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
} }
} }
private fun onSortDirectionChanged(value: FilterProperty<SortDirection>) {
val b = viewBinding ?: return
b.layoutSortDirection.isGone = value.isEmpty()
if (value.isEmpty()) {
return
}
val selected = value.selectedItems.single()
b.buttonOrderAsc.isEnabled = SortDirection.ASC in value.availableItems
b.buttonOrderDesc.isEnabled = SortDirection.DESC in value.availableItems
b.layoutSortDirection.removeOnButtonCheckedListener(this)
b.layoutSortDirection.check(
when (selected) {
SortDirection.ASC -> R.id.button_order_asc
SortDirection.DESC -> R.id.button_order_desc
},
)
b.layoutSortDirection.addOnButtonCheckedListener(this)
}
private fun onLocaleChanged(value: FilterProperty<Locale?>) { private fun onLocaleChanged(value: FilterProperty<Locale?>) {
val b = viewBinding ?: return val b = viewBinding ?: return
b.textViewLocaleTitle.isGone = value.isEmpty() b.textViewLocaleTitle.isGone = value.isEmpty()
@@ -239,6 +278,19 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
private fun requireFilter() = (requireActivity() as FilterOwner).filter private fun requireFilter() = (requireActivity() as FilterOwner).filter
private fun setSortDirection(direction: SortDirection) {
val filter = requireFilter()
val currentOrder = filter.filterSortOrder.value.selectedItems.single()
val newOrder = currentOrder[direction]
filter.setSortOrder(newOrder)
}
private fun getSortDirection(@IdRes buttonId: Int): SortDirection = when (buttonId) {
R.id.button_order_asc -> SortDirection.ASC
R.id.button_order_desc -> SortDirection.DESC
else -> throw IllegalArgumentException("Wrong button id $buttonId")
}
companion object { companion object {
private const val TAG = "FilterSheet" private const val TAG = "FilterSheet"

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M19 17H22L18 21L14 17H17V3H19M2 17H12V19H2M6 5V7H2V5M2 11H9V13H2V11Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M19 7H22L18 3L14 7H17V21H19M2 17H12V19H2M6 5V7H2V5M2 11H9V13H2V11Z" />
</vector>

View File

@@ -39,6 +39,7 @@
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/card_order" android:id="@+id/card_order"
style="?materialCardViewOutlinedStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal" android:layout_marginTop="@dimen/margin_normal"
@@ -54,6 +55,37 @@
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/layout_sort_direction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:baselineAligned="false"
android:orientation="horizontal"
android:weightSum="2"
app:selectionRequired="true"
app:singleSelection="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_order_asc"
style="@style/Widget.Kotatsu.ToggleButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/sort_order_asc"
app:icon="@drawable/ic_sort_asc" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_order_desc"
style="@style/Widget.Kotatsu.ToggleButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/sort_order_desc"
app:icon="@drawable/ic_sort_desc" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<TextView <TextView
android:id="@+id/textView_locale_title" android:id="@+id/textView_locale_title"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -67,6 +99,7 @@
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/card_locale" android:id="@+id/card_locale"
style="?materialCardViewOutlinedStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal" android:layout_marginTop="@dimen/margin_normal"

View File

@@ -113,6 +113,7 @@
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/card_order" android:id="@+id/card_order"
style="?materialCardViewOutlinedStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
@@ -124,8 +125,8 @@
android:id="@+id/spinner_order" android:id="@+id/spinner_order"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="56dp"
android:paddingHorizontal="8dp" android:minHeight="?listPreferredItemHeightSmall"
android:minHeight="?listPreferredItemHeightSmall" /> android:paddingHorizontal="8dp" />
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>

View File

@@ -686,4 +686,11 @@
<string name="skip_all">Skip all</string> <string name="skip_all">Skip all</string>
<string name="stuck">Stuck</string> <string name="stuck">Stuck</string>
<string name="not_in_favorites">Not in favoites</string> <string name="not_in_favorites">Not in favoites</string>
<string name="updated_long_ago">Updated long ago</string>
<string name="unpopular">Unpopular</string>
<string name="low_rating">Low rating</string>
<string name="sort_order_asc">Ascending</string>
<string name="sort_order_desc">Descending</string>
<string name="by_date">Date</string>
<string name="popularity">Popularity</string>
</resources> </resources>