diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScroller.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScroller.kt index 454b5c453..55479d2d2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScroller.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScroller.kt @@ -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) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt index 884e802e4..9c9487057 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt @@ -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) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ViewBadge.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ViewBadge.kt index 3b8ccf955..fd7554006 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ViewBadge.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ViewBadge.kt @@ -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 { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt index 0042fe95c..67b269c0d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt @@ -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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapter.kt deleted file mode 100644 index df204fabb..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapter.kt +++ /dev/null @@ -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, -) : BaseListAdapter(), 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 - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapterDelegates.kt deleted file mode 100644 index 645c143fb..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapterDelegates.kt +++ /dev/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( - { 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( - { 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( - { 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( - { 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( - { 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(R.layout.item_sources_empty) { - - bind { - (itemView as TextView).setText(item.textResId) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt index fe65a3bb5..dec244837 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt @@ -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>(listOf(LoadingState)) + get() { + if (allTagsLoadJob == null || field.value.any { it is ErrorFooter }) { + loadAllTags() + } + return field + } override val filterTags: StateFlow> = 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> = flow { - emit(PendingSet(emptySet(), isLoading = true, error = null)) + private fun getLocalesAsFlow(): Flow> = 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>, limit: Int): Flow> = 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, @@ -341,8 +376,35 @@ class FilterCoordinator @Inject constructor( return result } - private data class PendingSet( - val items: Set, + 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() + e.toErrorFooter() + } + } + } + + private fun appendTagsList(newTags: Collection, isLoading: Boolean) = allTags.update { oldList -> + val oldTags = oldList.filterIsInstance() + buildList(oldTags.size + newTags.size + if (isLoading) 1 else 0) { + addAll(oldTags) + newTags.mapTo(this) { TagCatalogItem(it, isChecked = false) } + val tempSet = HashSet(size) + removeAll { x -> x is TagCatalogItem && !tempSet.add(x.tag) } + sortBy { (it as TagCatalogItem).tag.title } + if (isLoading) { + add(LoadingFooter()) + } + } + } + + private data class PendingData( + val items: Collection, val isLoading: Boolean, val error: Throwable?, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt index c64f08188..18bbc62c4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt @@ -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(), 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) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/MangaFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/MangaFilter.kt index d22c8d7bc..f5f54f682 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/MangaFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/MangaFilter.kt @@ -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> + val filterTags: StateFlow> val filterSortOrder: StateFlow> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterHeaderModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterHeaderModel.kt index 40e9bba4b..468c1130b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterHeaderModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterHeaderModel.kt @@ -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, 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 - } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterItem.kt deleted file mode 100644 index 799475401..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterItem.kt +++ /dev/null @@ -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 - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/TagCatalogItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/TagCatalogItem.kt new file mode 100644 index 000000000..9cd7fc2b9 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/TagCatalogItem.kt @@ -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) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt index 0bda210ae..1dcdf9814 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt @@ -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(), AdapterView.OnItemSelectedListener, ChipsView.OnChipClickListener, - ChipsView.OnChipCloseClickListener { + BaseAdaptiveSheet(), 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) { 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(value.selectedItems.size + 1) + val chips = ArrayList(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, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogAdapter.kt new file mode 100644 index 000000000..1fad15e5b --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogAdapter.kt @@ -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, +) : BaseListAdapter(), 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, + ) = adapterDelegateViewBinding( + { 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) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogSheet.kt new file mode 100644 index 000000000..cbdade0cc --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogSheet.kt @@ -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(), OnListItemClickListener, TextWatcher, + AdaptiveSheetCallback, View.OnClickListener, View.OnFocusChangeListener, TextView.OnEditorActionListener { + + private val viewModel by viewModels( + extrasProducer = { + defaultViewModelCreationExtras.withCreationCallback { 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) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogViewModel.kt new file mode 100644 index 000000000..01dbf3741 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogViewModel.kt @@ -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 + } + +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt index 4e25a5eb7..aa1a018f7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt @@ -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( { 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 { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 35b01a548..be03efa3b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -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 { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt index 21ebed9f4..6a18aa976 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt @@ -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 { diff --git a/app/src/main/res/layout-w600dp-land/activity_manga_list.xml b/app/src/main/res/layout-w600dp-land/activity_manga_list.xml index 36cf59f1a..a97a4e0fd 100644 --- a/app/src/main/res/layout-w600dp-land/activity_manga_list.xml +++ b/app/src/main/res/layout-w600dp-land/activity_manga_list.xml @@ -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" /> diff --git a/app/src/main/res/layout/fragment_filter_header.xml b/app/src/main/res/layout/fragment_filter_header.xml index 0ffd785ee..6436d3c94 100644 --- a/app/src/main/res/layout/fragment_filter_header.xml +++ b/app/src/main/res/layout/fragment_filter_header.xml @@ -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" /> diff --git a/app/src/main/res/layout/item_search_suggestion_tags.xml b/app/src/main/res/layout/item_search_suggestion_tags.xml index 4fe329755..0c44390aa 100644 --- a/app/src/main/res/layout/item_search_suggestion_tags.xml +++ b/app/src/main/res/layout/item_search_suggestion_tags.xml @@ -1,12 +1,9 @@ + android:paddingBottom="6dp" /> diff --git a/app/src/main/res/layout/sheet_filter.xml b/app/src/main/res/layout/sheet_filter.xml index 56f00ccbf..4350f8c04 100644 --- a/app/src/main/res/layout/sheet_filter.xml +++ b/app/src/main/res/layout/sheet_filter.xml @@ -13,20 +13,140 @@ android:layout_height="wrap_content" app:title="@string/filter" /> - + android:layout_height="match_parent" + android:scrollIndicators="top"> - - + android:paddingBottom="@dimen/margin_normal"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/sheet_filter2.xml b/app/src/main/res/layout/sheet_filter2.xml deleted file mode 100644 index 77200c6f6..000000000 --- a/app/src/main/res/layout/sheet_filter2.xml +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/sheet_list_mode.xml b/app/src/main/res/layout/sheet_list_mode.xml index 4ff390586..6f38ad09d 100644 --- a/app/src/main/res/layout/sheet_list_mode.xml +++ b/app/src/main/res/layout/sheet_list_mode.xml @@ -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" diff --git a/app/src/main/res/layout/sheet_tags.xml b/app/src/main/res/layout/sheet_tags.xml new file mode 100644 index 000000000..859104ce4 --- /dev/null +++ b/app/src/main/res/layout/sheet_tags.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 7f76899d7..e36b5b405 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -70,7 +70,7 @@ - + @@ -133,32 +133,36 @@ - + - - - - + + + + - - - - + + + + - - - - + + + + - - - + + + - + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0bcf03e6f..623d2a328 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -542,4 +542,5 @@ Apply Filtering by both genres and locale is not supported by this source Filtering by both genres and states is not supported by this source + Start typing the genre name diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 8fd970973..246f45e52 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -108,21 +108,11 @@ - - 28dp +