Update manga list header

This commit is contained in:
Koitharu
2022-07-08 14:57:53 +03:00
parent 7d41318d15
commit 602a5eb2ab
19 changed files with 187 additions and 81 deletions

View File

@@ -9,6 +9,7 @@ import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.google.android.material.chip.ChipGroup
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.castOrNull
class ChipsView @JvmOverloads constructor(
context: Context,
@@ -18,10 +19,10 @@ class ChipsView @JvmOverloads constructor(
private var isLayoutSuppressedCompat = false
private var isLayoutCalledOnSuppressed = false
private var chipOnClickListener = OnClickListener {
private val chipOnClickListener = OnClickListener {
onChipClickListener?.onChipClick(it as Chip, it.tag)
}
private var chipOnCloseListener = OnClickListener {
private val chipOnCloseListener = OnClickListener {
onChipCloseClickListener?.onChipCloseClick(it as Chip, it.tag)
}
var onChipClickListener: OnChipClickListener? = null
@@ -60,15 +61,27 @@ class ChipsView @JvmOverloads constructor(
}
}
fun <T> getCheckedData(cls: Class<T>): Set<T> {
val result = LinkedHashSet<T>(childCount)
for (child in children) {
if (child is Chip && child.isChecked) {
result += cls.castOrNull(child.tag) ?: continue
}
}
return result
}
private fun bindChip(chip: Chip, model: ChipModel) {
chip.text = model.title
if (model.icon == 0) {
chip.isChipIconVisible = false
} else {
chip.isCheckedIconVisible = true
chip.isChipIconVisible = true
chip.setChipIconResource(model.icon)
}
chip.isClickable = onChipClickListener != null
chip.isClickable = onChipClickListener != null || model.isCheckable
chip.isCheckable = model.isCheckable
chip.isChecked = model.isChecked
chip.tag = model.data
}
@@ -76,11 +89,12 @@ class ChipsView @JvmOverloads constructor(
val chip = Chip(context)
val drawable = ChipDrawable.createFromAttributes(context, null, 0, R.style.Widget_Kotatsu_Chip)
chip.setChipDrawable(drawable)
chip.isCheckedIconVisible = true
chip.setCheckedIconResource(R.drawable.ic_check)
chip.isCloseIconVisible = onChipCloseClickListener != null
chip.setOnCloseIconClickListener(chipOnCloseListener)
chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(chipOnClickListener)
chip.isCheckable = false
addView(chip)
return chip
}
@@ -98,7 +112,9 @@ class ChipsView @JvmOverloads constructor(
class ChipModel(
@DrawableRes val icon: Int,
val title: CharSequence,
val data: Any? = null
val isCheckable: Boolean,
val isChecked: Boolean,
val data: Any? = null,
) {
override fun equals(other: Any?): Boolean {
@@ -109,6 +125,8 @@ class ChipsView @JvmOverloads constructor(
if (icon != other.icon) return false
if (title != other.title) return false
if (isCheckable != other.isCheckable) return false
if (isChecked != other.isChecked) return false
if (data != other.data) return false
return true
@@ -117,7 +135,9 @@ class ChipsView @JvmOverloads constructor(
override fun hashCode(): Int {
var result = icon
result = 31 * result + title.hashCode()
result = 31 * result + data.hashCode()
result = 31 * result + isCheckable.hashCode()
result = 31 * result + isChecked.hashCode()
result = 31 * result + (data?.hashCode() ?: 0)
return result
}
}

View File

@@ -341,6 +341,8 @@ class DetailsFragment :
title = tag.title,
icon = 0,
data = tag,
isCheckable = false,
isChecked = false,
)
}
)

View File

@@ -193,8 +193,8 @@ abstract class MangaListFragment :
resolveException(error)
}
override fun onTagRemoveClick(tag: MangaTag) {
viewModel.onRemoveFilterTag(tag)
override fun onUpdateFilter(tags: Set<MangaTag>) {
viewModel.onUpdateFilter(tags)
}
private fun onGridScaleChanged(scale: Float) {

View File

@@ -25,7 +25,7 @@ abstract class MangaListViewModel(
valueProducer = { gridSize / 100f },
)
open fun onRemoveFilterTag(tag: MangaTag) = Unit
open fun onUpdateFilter(tags: Set<MangaTag>) = Unit
protected fun createListModeFlow() = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
.onEach {

View File

@@ -1,23 +0,0 @@
package org.koitharu.kotatsu.list.ui.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.list.ui.model.CurrentFilterModel
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaTag
fun currentFilterAD(
listener: MangaListListener,
) = adapterDelegate<CurrentFilterModel, ListModel>(R.layout.item_current_filter) {
val chipGroup = itemView as ChipsView
chipGroup.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { _, data ->
listener.onTagRemoveClick(data as? MangaTag ?: return@OnChipCloseClickListener)
}
bind {
chipGroup.setChips(item.chips)
}
}

View File

@@ -0,0 +1,36 @@
package org.koitharu.kotatsu.list.ui.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.titleRes
import org.koitharu.kotatsu.databinding.ItemHeader2Binding
import org.koitharu.kotatsu.list.ui.model.ListHeader2
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
fun listHeader2AD(
listener: MangaListListener,
) = adapterDelegateViewBinding<ListHeader2, ListModel, ItemHeader2Binding>(
{ layoutInflater, parent -> ItemHeader2Binding.inflate(layoutInflater, parent, false) }
) {
var ignoreChecking = false
binding.textViewFilter.setOnClickListener {
listener.onFilterClick()
}
binding.chipsTags.setOnCheckedStateChangeListener { _, _ ->
if (!ignoreChecking) {
listener.onUpdateFilter(binding.chipsTags.getCheckedData(MangaTag::class.java))
}
}
bind { payloads ->
if (payloads.isNotEmpty()) {
binding.scrollView.smoothScrollTo(0, 0)
}
ignoreChecking = true
binding.chipsTags.setChips(item.chips)
ignoreChecking = false
binding.textViewFilter.setTextAndVisible(item.sortOrder?.titleRes ?: 0)
}
}

View File

@@ -26,7 +26,7 @@ class MangaListAdapter(
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD())
.addDelegate(ITEM_TYPE_FILTER, currentFilterAD(listener))
.addDelegate(ITEM_TYPE_HEADER_2, listHeader2AD(listener))
.addDelegate(ITEM_TYPE_HEADER_FILTER, listHeaderWithFilterAD(listener))
}
@@ -62,7 +62,7 @@ class MangaListAdapter(
Unit
}
}
is CurrentFilterModel -> Unit
is ListHeader2 -> Unit
else -> super.getChangePayload(oldItem, newItem)
}
}
@@ -80,7 +80,7 @@ class MangaListAdapter(
const val ITEM_TYPE_ERROR_FOOTER = 7
const val ITEM_TYPE_EMPTY = 8
const val ITEM_TYPE_HEADER = 9
const val ITEM_TYPE_FILTER = 10
const val ITEM_TYPE_HEADER_2 = 10
const val ITEM_TYPE_HEADER_FILTER = 11
val PAYLOAD_PROGRESS = Any()

View File

@@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.MangaTag
interface MangaListListener : OnListItemClickListener<Manga>, ListStateHolderListener {
fun onTagRemoveClick(tag: MangaTag)
fun onUpdateFilter(tags: Set<MangaTag>)
fun onFilterClick()
}

View File

@@ -56,12 +56,6 @@ class FilterCoordinator(
fun observeState() = currentState.asStateFlow()
fun removeTag(tag: MangaTag) {
currentState.update { oldValue ->
FilterState(oldValue.sortOrder, oldValue.tags - tag)
}
}
fun setTags(tags: Set<MangaTag>) {
currentState.update { oldValue ->
FilterState(oldValue.sortOrder, tags)

View File

@@ -1,7 +1,9 @@
package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.parsers.model.SortOrder
data class CurrentFilterModel(
data class ListHeader2(
val chips: Collection<ChipsView.ChipModel>,
val sortOrder: SortOrder?,
) : ListModel

View File

@@ -14,6 +14,7 @@ val remoteListModule
repository = MangaRepository(params[0]) as RemoteMangaRepository,
settings = get(),
dataRepository = get(),
searchRepository = get(),
)
}
}

View File

@@ -19,13 +19,16 @@ import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import java.util.*
private const val FILTER_MIN_INTERVAL = 750L
private const val FILTER_MIN_INTERVAL = 250L
class RemoteListViewModel(
private val repository: RemoteMangaRepository,
private val searchRepository: MangaSearchRepository,
settings: AppSettings,
dataRepository: MangaDataRepository,
) : MangaListViewModel(settings), OnFilterChangedListener {
@@ -46,9 +49,8 @@ class RemoteListViewModel(
listError,
hasNextPage,
) { list, mode, filterState, error, hasNext ->
buildList(list?.size?.plus(3) ?: 3) {
add(ListHeader(repository.source.title, 0, filterState.sortOrder))
createFilterModel(filterState)?.let { add(it) }
buildList(list?.size?.plus(2) ?: 2) {
add(ListHeader2(createChipsList(filterState), filterState.sortOrder))
when {
list.isNullOrEmpty() && error != null -> add(error.toErrorState(canRetry = true))
list == null -> add(LoadingState)
@@ -88,10 +90,6 @@ class RemoteListViewModel(
loadList(filter.snapshot(), append = !mangaList.value.isNullOrEmpty())
}
override fun onRemoveFilterTag(tag: MangaTag) {
filter.removeTag(tag)
}
override fun onSortItemClick(item: FilterItem.Sort) {
filter.onSortItemClick(item)
}
@@ -110,6 +108,10 @@ class RemoteListViewModel(
fun resetFilter() = filter.reset()
override fun onUpdateFilter(tags: Set<MangaTag>) {
applyFilter(tags)
}
fun applyFilter(tags: Set<MangaTag>) {
filter.setTags(tags)
}
@@ -142,18 +144,41 @@ class RemoteListViewModel(
}
}
private fun createFilterModel(filterState: FilterState): CurrentFilterModel? {
return if (filterState.tags.isEmpty()) {
null
} else {
CurrentFilterModel(filterState.tags.map { ChipsView.ChipModel(0, it.title, it) })
}
}
private fun createEmptyState(filterState: FilterState) = EmptyState(
icon = R.drawable.ic_empty_search,
textPrimary = R.string.nothing_found,
textSecondary = 0,
actionStringRes = if (filterState.tags.isEmpty()) 0 else R.string.reset_filter,
)
private suspend fun createChipsList(filterState: FilterState): List<ChipsView.ChipModel> {
val selectedTags = filterState.tags.toMutableSet()
val tags = searchRepository.getTagsSuggestion("", 6, repository.source)
val result = LinkedList<ChipsView.ChipModel>()
for (tag in tags) {
val model = ChipsView.ChipModel(
icon = 0,
title = tag.title,
isCheckable = true,
isChecked = selectedTags.remove(tag),
data = tag,
)
if (model.isChecked) {
result.addFirst(model)
} else {
result.addLast(model)
}
}
for (tag in selectedTags) {
val model = ChipsView.ChipModel(
icon = 0,
title = tag.title,
isCheckable = true,
isChecked = true,
data = tag,
)
result.addFirst(model)
}
return result
}
}

View File

@@ -3,9 +3,7 @@ package org.koitharu.kotatsu.search.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
@@ -28,13 +26,14 @@ class MangaListActivity : BaseActivity<ActivityContainerBinding>() {
setContentView(ActivityContainerBinding.inflate(layoutInflater))
val tags = intent.getParcelableExtra<ParcelableMangaTags>(EXTRA_TAGS)?.tags
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val source = intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: tags?.firstOrNull()?.source
if (source == null) {
finishAfterTransition()
return
}
title = source.title
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
val source = intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: tags?.firstOrNull()?.source
if (source == null) {
finishAfterTransition()
return
}
fm.commit {
val fragment = if (source == MangaSource.LOCAL) {
LocalListFragment.newInstance()

View File

@@ -103,7 +103,7 @@ class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaLis
viewModel.doSearch(viewModel.query.value.orEmpty())
}
override fun onTagRemoveClick(tag: MangaTag) = Unit
override fun onUpdateFilter(tags: Set<MangaTag>) = Unit
override fun onFilterClick() = Unit

View File

@@ -47,7 +47,7 @@ class SearchSuggestionViewModel(
setupSuggestion()
}
}
fun onSourceToggle(source: MangaSource, isEnabled: Boolean) {
settings.hiddenSources = if (isEnabled) {
settings.hiddenSources - source.name
@@ -113,6 +113,8 @@ class SearchSuggestionViewModel(
icon = 0,
title = tag.title,
data = tag,
isCheckable = false,
isChecked = false,
)
}
}

View File

@@ -17,14 +17,12 @@ import org.koitharu.kotatsu.databinding.FragmentFeedBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.main.ui.AppBarOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.tracker.ui.adapter.FeedAdapter
import org.koitharu.kotatsu.tracker.work.TrackWorker
import org.koitharu.kotatsu.utils.ext.addMenuProvider
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.measureHeight
class FeedFragment :
BaseFragment<FragmentFeedBinding>(),
@@ -84,7 +82,7 @@ class FeedFragment :
override fun onRetryClick(error: Throwable) = Unit
override fun onTagRemoveClick(tag: MangaTag) = Unit
override fun onUpdateFilter(tags: Set<MangaTag>) = Unit
override fun onFilterClick() = Unit

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.utils.ext
import android.icu.lang.UCharacter.GraphemeClusterBreak.T
@Suppress("UNCHECKED_CAST")
fun <T> Class<T>.castOrNull(obj: Any?): T? {
if (obj == null || !isInstance(obj)) {
return null
}
return obj as T
}

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<org.koitharu.kotatsu.base.ui.widgets.ChipsView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/chips_tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:closeIconEnabled="true" />

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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">
<HorizontalScrollView
android:id="@+id/scrollView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/margin_small"
android:layout_toStartOf="@id/textView_filter"
android:requiresFadingEdge="horizontal"
android:scrollbars="none">
<org.koitharu.kotatsu.base.ui.widgets.ChipsView
android:id="@+id/chips_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="@dimen/margin_small"
app:selectionRequired="false"
app:singleLine="true"
app:singleSelection="false" />
</HorizontalScrollView>
<TextView
android:id="@+id/textView_filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@drawable/list_selector"
android:gravity="center_vertical"
android:paddingStart="6dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
app:drawableEndCompat="@drawable/ic_expand_more"
app:drawableTint="?android:attr/textColorSecondary"
tools:ignore="RtlSymmetry"
tools:text="@string/popular" />
</RelativeLayout>