New filter implementation

This commit is contained in:
Koitharu
2023-12-06 13:37:23 +02:00
parent 6c07abec56
commit 1908ce3e46
29 changed files with 626 additions and 545 deletions

View File

@@ -12,7 +12,12 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.annotation.*
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.DimenRes
import androidx.annotation.DrawableRes
import androidx.annotation.Px
import androidx.annotation.StyleableRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.coordinatorlayout.widget.CoordinatorLayout
@@ -131,19 +136,19 @@ class FastScroller @JvmOverloads constructor(
var showTrack = false
context.withStyledAttributes(attrs, R.styleable.FastScroller, defStyleAttr) {
bubbleColor = getColor(R.styleable.FastScroller_bubbleColor, bubbleColor)
handleColor = getColor(R.styleable.FastScroller_thumbColor, handleColor)
trackColor = getColor(R.styleable.FastScroller_trackColor, trackColor)
textColor = getColor(R.styleable.FastScroller_bubbleTextColor, textColor)
hideScrollbar = getBoolean(R.styleable.FastScroller_hideScrollbar, hideScrollbar)
showBubble = getBoolean(R.styleable.FastScroller_showBubble, showBubble)
showBubbleAlways = getBoolean(R.styleable.FastScroller_showBubbleAlways, showBubbleAlways)
showTrack = getBoolean(R.styleable.FastScroller_showTrack, showTrack)
bubbleSize = getBubbleSize(R.styleable.FastScroller_bubbleSize, BubbleSize.NORMAL)
val textSize = getDimension(R.styleable.FastScroller_bubbleTextSize, bubbleSize.textSize)
context.withStyledAttributes(attrs, R.styleable.FastScrollRecyclerView, defStyleAttr) {
bubbleColor = getColor(R.styleable.FastScrollRecyclerView_bubbleColor, bubbleColor)
handleColor = getColor(R.styleable.FastScrollRecyclerView_thumbColor, handleColor)
trackColor = getColor(R.styleable.FastScrollRecyclerView_trackColor, trackColor)
textColor = getColor(R.styleable.FastScrollRecyclerView_bubbleTextColor, textColor)
hideScrollbar = getBoolean(R.styleable.FastScrollRecyclerView_hideScrollbar, hideScrollbar)
showBubble = getBoolean(R.styleable.FastScrollRecyclerView_showBubble, showBubble)
showBubbleAlways = getBoolean(R.styleable.FastScrollRecyclerView_showBubbleAlways, showBubbleAlways)
showTrack = getBoolean(R.styleable.FastScrollRecyclerView_showTrack, showTrack)
bubbleSize = getBubbleSize(R.styleable.FastScrollRecyclerView_bubbleSize, BubbleSize.NORMAL)
val textSize = getDimension(R.styleable.FastScrollRecyclerView_bubbleTextSize, bubbleSize.textSize)
binding.bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
offset = getDimensionPixelOffset(R.styleable.FastScroller_scrollerOffset, offset)
offset = getDimensionPixelOffset(R.styleable.FastScrollRecyclerView_scrollerOffset, offset)
}
setTrackColor(trackColor)

View File

@@ -1,21 +1,17 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import android.view.View.OnClickListener
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.content.res.getColorStateListOrThrow
import androidx.core.view.children
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.google.android.material.chip.ChipGroup
import com.google.android.material.theme.overlay.MaterialThemeOverlay
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(
context: Context,
@@ -31,9 +27,7 @@ class ChipsView @JvmOverloads constructor(
private val chipOnCloseListener = OnClickListener {
onChipCloseClickListener?.onChipCloseClick(it as Chip, it.tag)
}
private val defaultChipStrokeColor: ColorStateList
private val defaultChipTextColor: ColorStateList
private val defaultChipIconTint: ColorStateList
private val chipStyle: Int
var onChipClickListener: OnChipClickListener? = null
set(value) {
field = value
@@ -48,13 +42,9 @@ class ChipsView @JvmOverloads constructor(
}
init {
@SuppressLint("CustomViewStyleable")
val a = context.obtainStyledAttributes(null, materialR.styleable.Chip, 0, R.style.Widget_Kotatsu_Chip)
defaultChipStrokeColor = a.getColorStateListOrThrow(materialR.styleable.Chip_chipStrokeColor)
defaultChipTextColor = a.getColorStateListOrThrow(materialR.styleable.Chip_android_textColor)
defaultChipIconTint = a.getColorStateListOrThrow(materialR.styleable.Chip_chipIconTint)
a.recycle()
chipStyle = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0).use {
it.getResourceId(R.styleable.ChipsView_chipStyle, R.style.Widget_Kotatsu_Chip)
}
if (isInEditMode) {
setChips(
List(5) {
@@ -99,15 +89,6 @@ class ChipsView @JvmOverloads constructor(
private fun bindChip(chip: Chip, model: ChipModel) {
chip.text = model.title
val tint = if (model.tint == 0) {
null
} else {
ContextCompat.getColorStateList(context, model.tint)
}
chip.chipIconTint = tint ?: defaultChipIconTint
chip.checkedIconTint = tint ?: defaultChipIconTint
chip.chipStrokeColor = tint ?: defaultChipStrokeColor
chip.setTextColor(tint ?: defaultChipTextColor)
chip.isClickable = onChipClickListener != null || model.isCheckable
chip.isCheckable = model.isCheckable
if (model.icon == 0) {
@@ -122,13 +103,14 @@ class ChipsView @JvmOverloads constructor(
}
private fun addChip(): Chip {
val chip = Chip(context)
val drawable = ChipDrawable.createFromAttributes(context, null, 0, R.style.Widget_Kotatsu_Chip)
val themedContext = MaterialThemeOverlay.wrap(context, null, 0, chipStyle)
val chip = Chip(themedContext, null)
val drawable = ChipDrawable.createFromAttributes(themedContext, null, 0, chipStyle)
chip.setChipDrawable(drawable)
chip.isCheckedIconVisible = true
chip.isChipIconVisible = false
chip.setCheckedIconResource(R.drawable.ic_check)
chip.checkedIconTint = defaultChipIconTint
// chip.checkedIconTint = chip.ic
chip.isCloseIconVisible = onChipCloseClickListener != null
chip.setOnCloseIconClickListener(chipOnCloseListener)
chip.setEnsureMinTouchTargetSize(false)

View File

@@ -21,7 +21,11 @@ class ViewBadge(
get() = badgeDrawable?.number ?: 0
set(value) {
val badge = badgeDrawable ?: initBadge()
badge.number = value
if (maxCharacterCount != 0) {
badge.number = value
} else {
badge.clearNumber()
}
badge.isVisible = value > 0
}
@@ -51,7 +55,13 @@ class ViewBadge(
fun setMaxCharacterCount(value: Int) {
maxCharacterCount = value
badgeDrawable?.maxCharacterCount = value
badgeDrawable?.let {
if (value == 0) {
it.clearNumber()
} else {
it.maxCharacterCount = value
}
}
}
private fun initBadge(): BadgeDrawable {

View File

@@ -20,7 +20,7 @@ fun mangaCategoryAD(
}
bind { payloads ->
binding.checkableImageView.setChecked(item.isChecked, ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED !in payloads)
binding.checkableImageView.setChecked(item.isChecked, ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads)
binding.textViewTitle.text = item.category.title
binding.imageViewTracker.isVisible = item.category.isTrackingEnabled && item.isTrackerEnabled
binding.imageViewVisible.isVisible = item.category.isVisibleInLibrary

View File

@@ -1,48 +0,0 @@
package org.koitharu.kotatsu.filter.ui
import android.content.Context
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.filter.ui.model.FilterItem
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
class FilterAdapter(
listener: OnFilterChangedListener,
listListener: ListListener<ListModel>,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
addDelegate(ListItemType.FILTER_SORT, filterSortDelegate(listener))
addDelegate(ListItemType.FILTER_TAG, filterTagDelegate(listener))
addDelegate(ListItemType.FILTER_TAG_MULTI, filterTagMultipleDelegate(listener))
addDelegate(ListItemType.FILTER_STATE, filterStateDelegate(listener))
addDelegate(ListItemType.FILTER_LANGUAGE, filterLanguageDelegate(listener))
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.FOOTER_ERROR, filterErrorDelegate())
differ.addListListener(listListener)
}
override fun getSectionText(context: Context, position: Int): CharSequence? {
val list = items
for (i in (0..position).reversed()) {
val item = list.getOrNull(i) as? FilterItem ?: continue
when (item) {
is FilterItem.Error -> null
is FilterItem.Language -> item.getTitle(context.resources)
is FilterItem.Sort -> context.getString(item.order.titleRes)
is FilterItem.State -> context.getString(item.state.titleResId)
is FilterItem.Tag -> item.tag.title
}?.firstOrNull()?.uppercase()
}
return null
}
}

View File

@@ -1,103 +0,0 @@
package org.koitharu.kotatsu.filter.ui
import android.widget.TextView
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.core.util.ext.setChecked
import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding
import org.koitharu.kotatsu.filter.ui.model.FilterItem
import org.koitharu.kotatsu.list.ui.model.ListModel
fun filterSortDelegate(
listener: OnFilterChangedListener,
) = adapterDelegateViewBinding<FilterItem.Sort, ListModel, ItemCheckableSingleBinding>(
{ layoutInflater, parent -> ItemCheckableSingleBinding.inflate(layoutInflater, parent, false) },
) {
itemView.setOnClickListener {
listener.setSortOrder(item.order)
}
bind { payloads ->
binding.root.setText(item.order.titleRes)
binding.root.setChecked(item.isSelected, payloads.isNotEmpty())
}
}
fun filterStateDelegate(
listener: OnFilterChangedListener,
) = adapterDelegateViewBinding<FilterItem.State, ListModel, ItemCheckableMultipleBinding>(
{ layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
) {
itemView.setOnClickListener {
listener.setState(item.state, !item.isChecked)
}
bind { payloads ->
binding.root.setText(item.state.titleResId)
binding.root.setChecked(item.isChecked, payloads.isNotEmpty())
}
}
fun filterLanguageDelegate(
listener: OnFilterChangedListener,
) = adapterDelegateViewBinding<FilterItem.Language, ListModel, ItemCheckableSingleBinding>(
{ layoutInflater, parent -> ItemCheckableSingleBinding.inflate(layoutInflater, parent, false) },
) {
itemView.setOnClickListener {
listener.setLanguage(item.locale)
}
bind { payloads ->
binding.root.text = item.getTitle(context.resources)
binding.root.setChecked(item.isChecked, payloads.isNotEmpty())
}
}
fun filterTagDelegate(
listener: OnFilterChangedListener,
) = adapterDelegateViewBinding<FilterItem.Tag, ListModel, ItemCheckableSingleBinding>(
{ layoutInflater, parent -> ItemCheckableSingleBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is FilterItem.Tag && !item.isMultiple },
) {
itemView.setOnClickListener {
listener.setTag(item.tag, !item.isChecked)
}
bind { payloads ->
binding.root.text = item.tag.title
binding.root.setChecked(item.isChecked, payloads.isNotEmpty())
}
}
fun filterTagMultipleDelegate(
listener: OnFilterChangedListener,
) = adapterDelegateViewBinding<FilterItem.Tag, ListModel, ItemCheckableMultipleBinding>(
{ layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is FilterItem.Tag && item.isMultiple },
) {
itemView.setOnClickListener {
listener.setTag(item.tag, !item.isChecked)
}
bind { payloads ->
binding.root.text = item.tag.title
binding.root.setChecked(item.isChecked, payloads.isNotEmpty())
}
}
fun filterErrorDelegate() = adapterDelegate<FilterItem.Error, ListModel>(R.layout.item_sources_empty) {
bind {
(itemView as TextView).setText(item.textResId)
}
}

View File

@@ -6,7 +6,9 @@ import dagger.hilt.android.ViewModelLifecycle
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -16,20 +18,29 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.LocaleComparator
import org.koitharu.kotatsu.core.util.ext.asArrayList
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.filter.ui.model.TagCatalogItem
import org.koitharu.kotatsu.list.ui.model.ErrorFooter
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorFooter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -63,13 +74,22 @@ class FilterCoordinator @Inject constructor(
}
private var availableTagsDeferred = loadTagsAsync()
private var availableLocalesDeferred = loadLocalesAsync()
private var allTagsLoadJob: Job? = null
override val allTags = MutableStateFlow<List<ListModel>>(listOf(LoadingState))
get() {
if (allTagsLoadJob == null || field.value.any { it is ErrorFooter }) {
loadAllTags()
}
return field
}
override val filterTags: StateFlow<FilterProperty<MangaTag>> = combine(
currentState.distinctUntilChangedBy { it.tags },
getTagsAsFlow(),
getTopTagsAsFlow(currentState.map { it.tags }, 16),
) { state, tags ->
FilterProperty(
availableItems = tags.items.sortedBy { it.title },
availableItems = tags.items.asArrayList(),
selectedItems = state.tags,
isLoading = tags.isLoading,
error = tags.error,
@@ -131,8 +151,7 @@ class FilterCoordinator @Inject constructor(
initialValue = FilterHeaderModel(
chips = emptyList(),
sortOrder = repository.defaultSortOrder,
hasSelectedTags = false,
allowMultipleTags = repository.isMultipleTagsSupported,
isFilterApplied = false,
),
)
@@ -225,32 +244,48 @@ class FilterCoordinator @Inject constructor(
FilterHeaderModel(
chips = chips,
sortOrder = state.sortOrder,
hasSelectedTags = state.tags.isNotEmpty(),
allowMultipleTags = repository.isMultipleTagsSupported,
isFilterApplied = !state.isEmpty(),
)
}
private fun getTagsAsFlow() = flow {
val localTags = localTags.get()
emit(PendingSet(localTags, isLoading = true, error = null))
emit(PendingData(localTags, isLoading = true, error = null))
tryLoadTags()
.onSuccess { remoteTags ->
emit(PendingSet(mergeTags(remoteTags, localTags), isLoading = false, error = null))
emit(PendingData(mergeTags(remoteTags, localTags), isLoading = false, error = null))
}.onFailure {
emit(PendingSet(localTags, isLoading = false, error = it))
emit(PendingData(localTags, isLoading = false, error = it))
}
}
private fun getLocalesAsFlow(): Flow<PendingSet<Locale>> = flow {
emit(PendingSet(emptySet(), isLoading = true, error = null))
private fun getLocalesAsFlow(): Flow<PendingData<Locale>> = flow {
emit(PendingData(emptySet(), isLoading = true, error = null))
tryLoadLocales()
.onSuccess { locales ->
emit(PendingSet(locales, isLoading = false, error = null))
emit(PendingData(locales, isLoading = false, error = null))
}.onFailure {
emit(PendingSet(emptySet(), isLoading = false, error = it))
emit(PendingData(emptySet(), isLoading = false, error = it))
}
}
private fun getTopTagsAsFlow(selectedTags: Flow<Set<MangaTag>>, limit: Int): Flow<PendingData<MangaTag>> = combine(
selectedTags.map {
if (it.isEmpty()) {
searchRepository.getTagsSuggestion("", limit, repository.source)
} else {
searchRepository.getTagsSuggestion(it).take(limit)
}
},
getTagsAsFlow(),
) { suggested, all ->
val res = suggested.toMutableList()
if (res.size < limit) {
res.addAll(all.items.shuffled().take(limit - res.size))
}
PendingData(res, all.isLoading, all.error.takeIf { res.size < limit })
}
private suspend fun createChipsList(
filterState: MangaListFilter.Advanced,
availableTags: Set<MangaTag>,
@@ -341,8 +376,35 @@ class FilterCoordinator @Inject constructor(
return result
}
private data class PendingSet<T>(
val items: Set<T>,
private fun loadAllTags() {
val prevJob = allTagsLoadJob
allTagsLoadJob = coroutineScope.launch(Dispatchers.Default) {
runCatchingCancellable {
prevJob?.cancelAndJoin()
appendTagsList(localTags.get(), isLoading = true)
appendTagsList(availableTagsDeferred.await().getOrThrow(), isLoading = false)
}.onFailure { e ->
allTags.value = allTags.value.filterIsInstance<TagCatalogItem>() + e.toErrorFooter()
}
}
}
private fun appendTagsList(newTags: Collection<MangaTag>, isLoading: Boolean) = allTags.update { oldList ->
val oldTags = oldList.filterIsInstance<TagCatalogItem>()
buildList(oldTags.size + newTags.size + if (isLoading) 1 else 0) {
addAll(oldTags)
newTags.mapTo(this) { TagCatalogItem(it, isChecked = false) }
val tempSet = HashSet<MangaTag>(size)
removeAll { x -> x is TagCatalogItem && !tempSet.add(x.tag) }
sortBy { (it as TagCatalogItem).tag.title }
if (isLoading) {
add(LoadingFooter())
}
}
}
private data class PendingData<T>(
val items: Collection<T>,
val isLoading: Boolean,
val error: Throwable?,
)

View File

@@ -13,7 +13,7 @@ import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
import org.koitharu.kotatsu.filter.ui.tags.TagsCatalogSheet
import org.koitharu.kotatsu.parsers.model.MangaTag
import com.google.android.material.R as materialR
@@ -37,7 +37,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
override fun onChipClick(chip: Chip, data: Any?) {
val tag = data as? MangaTag
if (tag == null) {
FilterSheetFragment.show(parentFragmentManager)
TagsCatalogSheet.show(parentFragmentManager)
} else {
filter.setTag(tag, chip.isChecked)
}

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.filter.ui
import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
@@ -10,6 +11,8 @@ import java.util.Locale
interface MangaFilter : OnFilterChangedListener {
val allTags: StateFlow<List<ListModel>>
val filterTags: StateFlow<FilterProperty<MangaTag>>
val filterSortOrder: StateFlow<FilterProperty<SortOrder>>

View File

@@ -3,33 +3,12 @@ package org.koitharu.kotatsu.filter.ui.model
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.parsers.model.SortOrder
class FilterHeaderModel(
data class FilterHeaderModel(
val chips: Collection<ChipsView.ChipModel>,
val sortOrder: SortOrder?,
val hasSelectedTags: Boolean,
val allowMultipleTags: Boolean,
val isFilterApplied: Boolean,
) {
val textSummary: String
get() = chips.mapNotNull { if (it.isChecked) it.title else null }.joinToString()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FilterHeaderModel
if (chips != other.chips) return false
if (allowMultipleTags != other.allowMultipleTags) return false
return sortOrder == other.sortOrder
// Not need to check hasSelectedTags
}
override fun hashCode(): Int {
var result = chips.hashCode()
result = 31 * result + allowMultipleTags.hashCode()
result = 31 * result + (sortOrder?.hashCode() ?: 0)
return result
}
}

View File

@@ -1,101 +0,0 @@
package org.koitharu.kotatsu.filter.ui.model
import android.content.res.Resources
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
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.util.toTitleCase
import java.util.Locale
sealed interface FilterItem : ListModel {
data class Sort(
val order: SortOrder,
val isSelected: Boolean,
) : FilterItem {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Sort && other.order == order
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is Sort && previousState.isSelected != isSelected) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
}
data class Tag(
val tag: MangaTag,
val isMultiple: Boolean,
val isChecked: Boolean,
) : FilterItem {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Tag && other.isMultiple == isMultiple && other.tag == tag
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is Tag && previousState.isChecked != isChecked) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
}
data class State(
val state: MangaState,
val isChecked: Boolean
) : FilterItem {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is State && other.state == state
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is State && previousState.isChecked != isChecked) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
}
data class Language(
val locale: Locale?,
val isChecked: Boolean,
) : FilterItem {
private val displayText = locale?.getDisplayLanguage(locale)?.toTitleCase(locale)
fun getTitle(resources: Resources) = displayText ?: resources.getString(R.string.various_languages)
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Language && other.locale == locale
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is Language && previousState.isChecked != isChecked) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
}
data class Error(
@StringRes val textResId: Int,
) : FilterItem {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Error && textResId == other.textResId
}
}
}

View File

@@ -0,0 +1,23 @@
package org.koitharu.kotatsu.filter.ui.model
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaTag
data class TagCatalogItem(
val tag: MangaTag,
val isChecked: Boolean,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is TagCatalogItem && other.tag == tag
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is TagCatalogItem && previousState.isChecked != isChecked) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
}

View File

@@ -15,11 +15,14 @@ import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.databinding.SheetFilter2Binding
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.filter.ui.FilterOwner
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.filter.ui.tags.TagsCatalogSheet
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
@@ -28,14 +31,13 @@ import java.util.Locale
import com.google.android.material.R as materialR
class FilterSheetFragment :
BaseAdaptiveSheet<SheetFilter2Binding>(), AdapterView.OnItemSelectedListener, ChipsView.OnChipClickListener,
ChipsView.OnChipCloseClickListener {
BaseAdaptiveSheet<SheetFilterBinding>(), AdapterView.OnItemSelectedListener, ChipsView.OnChipClickListener {
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilter2Binding {
return SheetFilter2Binding.inflate(inflater, container, false)
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
return SheetFilterBinding.inflate(inflater, container, false)
}
override fun onViewBindingCreated(binding: SheetFilter2Binding, savedInstanceState: Bundle?) {
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
binding.scrollView.scrollIndicators = 0
@@ -50,7 +52,6 @@ class FilterSheetFragment :
binding.spinnerOrder.onItemSelectedListener = this
binding.chipsState.onChipClickListener = this
binding.chipsGenres.onChipClickListener = this
binding.chipsGenres.onChipCloseClickListener = this
}
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
@@ -67,14 +68,11 @@ class FilterSheetFragment :
val filter = requireFilter()
when (data) {
is MangaState -> filter.setState(data, chip.isChecked)
is MangaTag -> filter.setTag(data, chip.isChecked)
null -> TagsCatalogSheet.show(childFragmentManager)
}
}
override fun onChipCloseClick(chip: Chip, data: Any?) {
val tag = data as? MangaTag ?: return
requireFilter().setTag(tag, false)
}
private fun onSortOrderChanged(value: FilterProperty<SortOrder>) {
val b = viewBinding ?: return
b.textViewOrderTitle.isGone = value.isEmpty()
@@ -122,20 +120,35 @@ class FilterSheetFragment :
val b = viewBinding ?: return
b.textViewGenresTitle.isGone = value.isEmpty()
b.chipsGenres.isGone = value.isEmpty()
b.textViewGenresHint.textAndVisible = value.error?.getDisplayMessage(resources)
if (value.isEmpty()) {
return
}
val chips = ArrayList<ChipsView.ChipModel>(value.selectedItems.size + 1)
val chips = ArrayList<ChipsView.ChipModel>(value.selectedItems.size + value.availableItems.size + 1)
value.selectedItems.mapTo(chips) { tag ->
ChipsView.ChipModel(
tint = 0,
title = tag.title,
icon = 0,
isCheckable = false,
isChecked = false,
isCheckable = true,
isChecked = true,
data = tag,
)
}
value.availableItems.mapNotNullTo(chips) { tag ->
if (tag !in value.selectedItems) {
ChipsView.ChipModel(
tint = 0,
title = tag.title,
icon = 0,
isCheckable = true,
isChecked = false,
data = tag,
)
} else {
null
}
}
chips.add(
ChipsView.ChipModel(
tint = 0,

View File

@@ -0,0 +1,48 @@
package org.koitharu.kotatsu.filter.ui.tags
import android.content.Context
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.util.ext.setChecked
import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding
import org.koitharu.kotatsu.filter.ui.model.TagCatalogItem
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.errorFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
class TagsCatalogAdapter(
listener: OnListItemClickListener<TagCatalogItem>,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
addDelegate(ListItemType.FILTER_TAG, tagCatalogDelegate(listener))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.FOOTER_ERROR, errorFooterAD(null))
}
override fun getSectionText(context: Context, position: Int): CharSequence? {
return (items.getOrNull(position) as? TagCatalogItem)?.tag?.title?.firstOrNull()?.uppercase()
}
private fun tagCatalogDelegate(
listener: OnListItemClickListener<TagCatalogItem>,
) = adapterDelegateViewBinding<TagCatalogItem, ListModel, ItemCheckableNewBinding>(
{ layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) },
) {
itemView.setOnClickListener {
listener.onItemClick(item, itemView)
}
bind { payloads ->
binding.root.text = item.tag.title
binding.root.setChecked(item.isChecked, ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads)
}
}
}

View File

@@ -0,0 +1,106 @@
package org.koitharu.kotatsu.filter.ui.tags
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.withCreationCallback
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.databinding.SheetTagsBinding
import org.koitharu.kotatsu.filter.ui.FilterOwner
import org.koitharu.kotatsu.filter.ui.model.TagCatalogItem
@AndroidEntryPoint
class TagsCatalogSheet : BaseAdaptiveSheet<SheetTagsBinding>(), OnListItemClickListener<TagCatalogItem>, TextWatcher,
AdaptiveSheetCallback, View.OnClickListener, View.OnFocusChangeListener, TextView.OnEditorActionListener {
private val viewModel by viewModels<TagsCatalogViewModel>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<TagsCatalogViewModel.Factory> { factory ->
factory.create((requireActivity() as FilterOwner).filter)
}
},
)
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetTagsBinding {
return SheetTagsBinding.inflate(inflater, container, false)
}
override fun onViewBindingCreated(binding: SheetTagsBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = TagsCatalogAdapter(this)
binding.recyclerView.adapter = adapter
binding.recyclerView.setHasFixedSize(true)
binding.editSearch.setText(viewModel.searchQuery.value)
binding.editSearch.addTextChangedListener(this)
binding.editSearch.onFocusChangeListener = this
binding.editSearch.setOnEditorActionListener(this)
binding.buttonSearchClear.setOnClickListener(this)
viewModel.content.observe(viewLifecycleOwner, adapter)
addSheetCallback(this)
disableFitToContents()
}
override fun onItemClick(item: TagCatalogItem, view: View) {
val filter = (requireActivity() as FilterOwner).filter
filter.setTag(item.tag, true)
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_search_clear -> viewBinding?.editSearch?.text?.clear()
}
}
override fun onFocusChange(v: View?, hasFocus: Boolean) {
setExpanded(
isExpanded = hasFocus || isExpanded,
isLocked = hasFocus,
)
}
override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent?): Boolean {
return if (actionId == EditorInfo.IME_ACTION_SEARCH) {
v.clearFocus()
true
} else {
false
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
override fun afterTextChanged(s: Editable?) {
val q = s?.toString().orEmpty()
viewModel.searchQuery.value = q
viewBinding?.buttonSearchClear?.isVisible = q.isNotEmpty()
}
override fun onStateChanged(sheet: View, newState: Int) {
viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED
}
companion object {
private const val TAG = "TagsCatalogSheet"
fun show(fm: FragmentManager) = TagsCatalogSheet().showDistinct(fm, TAG)
}
}

View File

@@ -0,0 +1,60 @@
package org.koitharu.kotatsu.filter.ui.tags
import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.filter.ui.MangaFilter
import org.koitharu.kotatsu.filter.ui.model.TagCatalogItem
import org.koitharu.kotatsu.list.ui.model.LoadingState
@HiltViewModel(assistedFactory = TagsCatalogViewModel.Factory::class)
class TagsCatalogViewModel @AssistedInject constructor(
@Assisted filter: MangaFilter,
mangaRepositoryFactory: MangaRepository.Factory,
dataRepository: MangaDataRepository,
) : BaseViewModel() {
val searchQuery = MutableStateFlow("")
private val tags = combine(
filter.allTags,
filter.filterTags.map { it.selectedItems },
) { all, selected ->
all.map { x ->
if (x is TagCatalogItem) {
val checked = x.tag in selected
if (x.isChecked == checked) {
x
} else {
x.copy(isChecked = checked)
}
} else {
x
}
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, filter.allTags.value)
val content = combine(tags, searchQuery) { raw, query ->
raw.filter { x ->
x !is TagCatalogItem || x.tag.title.contains(query, ignoreCase = true)
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingState))
@AssistedFactory
interface Factory {
fun create(filter: MangaFilter): TagsCatalogViewModel
}
}

View File

@@ -7,13 +7,15 @@ import org.koitharu.kotatsu.list.ui.model.ErrorFooter
import org.koitharu.kotatsu.list.ui.model.ListModel
fun errorFooterAD(
listener: MangaListListener,
listener: MangaListListener?,
) = adapterDelegateViewBinding<ErrorFooter, ListModel, ItemErrorFooterBinding>(
{ inflater, parent -> ItemErrorFooterBinding.inflate(inflater, parent, false) },
) {
binding.root.setOnClickListener {
listener.onRetryClick(item.exception)
if (listener != null) {
binding.root.setOnClickListener {
listener.onRetryClick(item.exception)
}
}
bind {

View File

@@ -78,7 +78,7 @@ open class RemoteListViewModel @Inject constructor(
when {
list.isNullOrEmpty() && error != null -> add(error.toErrorState(canRetry = true))
list == null -> add(LoadingState)
list.isEmpty() -> add(createEmptyState(header.value.hasSelectedTags))
list.isEmpty() -> add(createEmptyState(canResetFilter = header.value.isFilterApplied))
else -> {
list.toUi(this, mode, listExtraProvider)
when {

View File

@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.core.util.ViewBadge
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.observe
@@ -146,8 +147,11 @@ class MangaListActivity :
val filter = filterOwner.filter
val chipSort = viewBinding.buttonOrder
if (chipSort != null) {
val filterBadge = ViewBadge(chipSort, this)
filterBadge.setMaxCharacterCount(0)
filter.header.observe(this) {
chipSort.setTextAndVisible(it.sortOrder?.titleRes ?: 0)
filterBadge.counter = if (it.isFilterApplied) 1 else 0
}
} else {
filter.header.map {

View File

@@ -53,7 +53,7 @@
android:id="@+id/container_side"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/sheet_filter" />
tools:layout="@layout/sheet_tags" />
</com.google.android.material.card.MaterialCardView>

View File

@@ -14,6 +14,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="@dimen/margin_small"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
app:selectionRequired="false"
app:singleLine="true"
app:singleSelection="false" />

View File

@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingTop="4dp"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:paddingBottom="6dp"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp" />
android:paddingBottom="6dp" />

View File

@@ -13,20 +13,140 @@
android:layout_height="wrap_content"
app:title="@string/filter" />
<FrameLayout
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:scrollIndicators="top">
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:layout_height="wrap_content"
android:orientation="vertical"
android:scrollIndicators="top"
app:bubbleSize="normal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:ignore="UnusedAttribute"
tools:listitem="@layout/item_checkable_new" />
</FrameLayout>
android:paddingBottom="@dimen/margin_normal">
<TextView
android:id="@+id/textView_order_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/sort_order"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_order"
style="?materialCardViewOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="@dimen/margin_normal"
android:visibility="gone"
app:cardBackgroundColor="@color/m3_chip_background_color"
app:shapeAppearance="?shapeAppearanceCornerMedium"
app:strokeColor="@color/m3_button_outline_color_selector"
app:strokeWidth="@dimen/m3_comp_outlined_button_outline_width"
tools:visibility="visible">
<Spinner
android:id="@+id/spinner_order"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?listPreferredItemHeightSmall" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/textView_locale_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/language"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_locale"
style="?materialCardViewOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="@dimen/margin_normal"
android:visibility="gone"
app:cardBackgroundColor="@color/m3_chip_background_color"
app:shapeAppearance="?shapeAppearanceCornerMedium"
app:strokeColor="@color/m3_button_outline_color_selector"
app:strokeWidth="@dimen/m3_comp_outlined_button_outline_width"
tools:visibility="visible">
<Spinner
android:id="@+id/spinner_locale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?listPreferredItemHeightSmall" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/textView_genres_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/genres"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_genres"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:paddingHorizontal="@dimen/margin_normal"
android:visibility="gone"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_genres_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:paddingHorizontal="@dimen/margin_normal"
android:textAppearance="?textAppearanceBodySmall"
android:visibility="gone"
tools:text="@string/error_multiple_genres_not_supported"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_state_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/state"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:paddingHorizontal="@dimen/margin_normal"
android:visibility="gone"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
tools:visibility="visible" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -1,143 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
android:id="@+id/headerBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/filter" />
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollIndicators="top">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/margin_normal">
<TextView
android:id="@+id/textView_order_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/sort_order"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_order"
style="?materialCardViewOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="@dimen/margin_normal"
android:visibility="gone"
app:cardBackgroundColor="?m3ColorBackground"
app:shapeAppearance="?shapeAppearanceCornerMedium"
app:strokeColor="@color/m3_button_outline_color_selector"
app:strokeWidth="@dimen/m3_comp_outlined_button_outline_width"
tools:visibility="visible">
<Spinner
android:id="@+id/spinner_order"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?listPreferredItemHeightSmall" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/textView_locale_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/language"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_locale"
style="?materialCardViewOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="@dimen/margin_normal"
android:visibility="gone"
app:cardBackgroundColor="?m3ColorBackground"
app:shapeAppearance="?shapeAppearanceCornerMedium"
app:strokeColor="@color/m3_button_outline_color_selector"
app:strokeWidth="@dimen/m3_comp_outlined_button_outline_width"
tools:visibility="visible">
<Spinner
android:id="@+id/spinner_locale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?listPreferredItemHeightSmall" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/textView_genres_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/genres"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_genres"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:paddingHorizontal="@dimen/margin_normal"
android:visibility="gone"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_state_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/state"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:paddingHorizontal="@dimen/margin_normal"
android:visibility="gone"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp"
tools:visibility="visible" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -119,7 +119,7 @@
android:layout_marginHorizontal="16dp"
android:layout_marginTop="@dimen/margin_normal"
android:visibility="gone"
app:cardBackgroundColor="?m3ColorBackground"
app:cardBackgroundColor="@color/m3_chip_background_color"
app:shapeAppearance="?shapeAppearanceCornerMedium"
app:strokeColor="@color/m3_button_outline_color_selector"
app:strokeWidth="@dimen/m3_comp_outlined_button_outline_width"

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
android:id="@+id/headerBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/filter" />
<EditText
android:id="@+id/edit_search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:drawableStart="?android:actionModeWebSearchDrawable"
android:drawablePadding="12dp"
android:hint="@string/genres_search_hint"
android:imeOptions="actionSearch|flagNoFullscreen"
android:importantForAutofill="no"
android:inputType="text"
android:paddingHorizontal="6dp"
android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/headerBar"
tools:ignore="LabelFor" />
<ImageButton
android:id="@+id/button_search_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/clear"
android:minWidth="?minTouchTargetSize"
android:minHeight="?minTouchTargetSize"
android:src="@drawable/abc_ic_clear_material"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/edit_search"
app:layout_constraintEnd_toEndOf="@id/edit_search"
app:layout_constraintTop_toTopOf="@id/edit_search"
tools:visibility="visible" />
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:orientation="vertical"
app:bubbleSize="normal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_search"
app:scrollerOffset="8dp"
tools:listitem="@layout/item_checkable_new" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -70,7 +70,7 @@
<attr name="progressStyle" format="reference" />
</declare-styleable>
<declare-styleable name="FastScroller">
<declare-styleable name="FastScrollRecyclerView">
<attr name="hideScrollbar" format="boolean" />
<attr name="showBubble" format="boolean" />
<attr name="showBubbleAlways" format="boolean" />
@@ -133,32 +133,36 @@
</declare-styleable>
<declare-styleable name="PieChart">
<attr name="pieChartColors" format="reference"/>
<attr name="pieChartColors" format="reference" />
<attr name="pieChartMarginTextFirst" format="dimension"/>
<attr name="pieChartMarginTextSecond" format="dimension"/>
<attr name="pieChartMarginTextThird" format="dimension"/>
<attr name="pieChartMarginSmallCircle" format="dimension"/>
<attr name="pieChartMarginTextFirst" format="dimension" />
<attr name="pieChartMarginTextSecond" format="dimension" />
<attr name="pieChartMarginTextThird" format="dimension" />
<attr name="pieChartMarginSmallCircle" format="dimension" />
<attr name="pieChartCircleStrokeWidth" format="dimension"/>
<attr name="pieChartCirclePadding" format="dimension"/>
<attr name="pieChartCirclePaintRoundSize" format="boolean"/>
<attr name="pieChartCircleSectionSpace" format="float"/>
<attr name="pieChartCircleStrokeWidth" format="dimension" />
<attr name="pieChartCirclePadding" format="dimension" />
<attr name="pieChartCirclePaintRoundSize" format="boolean" />
<attr name="pieChartCircleSectionSpace" format="float" />
<attr name="pieChartTextCircleRadius" format="dimension"/>
<attr name="pieChartTextAmountSize" format="dimension"/>
<attr name="pieChartTextNumberSize" format="dimension"/>
<attr name="pieChartTextDescriptionSize" format="dimension"/>
<attr name="pieChartTextCircleRadius" format="dimension" />
<attr name="pieChartTextAmountSize" format="dimension" />
<attr name="pieChartTextNumberSize" format="dimension" />
<attr name="pieChartTextDescriptionSize" format="dimension" />
<attr name="pieChartTextAmountColor" format="color"/>
<attr name="pieChartTextNumberColor" format="color"/>
<attr name="pieChartTextDescriptionColor" format="color"/>
<attr name="pieChartTextAmountColor" format="color" />
<attr name="pieChartTextNumberColor" format="color" />
<attr name="pieChartTextDescriptionColor" format="color" />
<attr name="pieChartTextAmount" format="string"/>
<attr name="pieChartTextAmount" format="string" />
</declare-styleable>
<declare-styleable name="NestedRecyclerView">
<attr name="maxHeight" />
</declare-styleable>
<declare-styleable name="ChipsView">
<attr name="chipStyle" />
</declare-styleable>
</resources>

View File

@@ -542,4 +542,5 @@
<string name="apply">Apply</string>
<string name="error_filter_locale_genre_not_supported">Filtering by both genres and locale is not supported by this source</string>
<string name="error_filter_states_genre_not_supported">Filtering by both genres and states is not supported by this source</string>
<string name="genres_search_hint">Start typing the genre name</string>
</resources>

View File

@@ -108,21 +108,11 @@
</style>
<style name="Widget.Kotatsu.Chip" parent="Widget.Material3.Chip.Suggestion">
<item name="chipBackgroundColor">?attr/m3ColorBackground</item>
<item name="android:textColor">?attr/colorOnSurfaceVariant</item>
<item name="chipStrokeColor">?attr/colorOutline</item>
<item name="chipIconTint">?attr/colorControlNormal</item>
<item name="rippleColor">?attr/colorControlHighlight</item>
<!-- Custom chip states -->
<item name="android:clickable">true</item>
<item name="android:focusable">true</item>
</style>
<!-- Smaller text/height -->
<item name="chipMinHeight">28dp</item>
<style name="Widget.Kotatsu.Chip.Filter" parent="Widget.Material3.Chip.Filter">
<!-- Collapse horizontal margin -->
<item name="chipMinTouchTargetSize">28dp</item>
</style>
<style name="Widget.Kotatsu.Button.More" parent="Widget.Material3.Button.TextButton">