New filter implementation
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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?,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
66
app/src/main/res/layout/sheet_tags.xml
Normal file
66
app/src/main/res/layout/sheet_tags.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user