Update manga list header

pull/163/head
Koitharu 4 years ago
parent 7d41318d15
commit 602a5eb2ab
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

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

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

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

@ -25,7 +25,7 @@ abstract class MangaListViewModel(
valueProducer = { gridSize / 100f }, 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 } protected fun createListModeFlow() = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
.onEach { .onEach {

@ -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)
}
}

@ -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)
}
}

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

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

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

@ -1,7 +1,9 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.base.ui.widgets.ChipsView 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 chips: Collection<ChipsView.ChipModel>,
val sortOrder: SortOrder?,
) : ListModel ) : ListModel

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

@ -19,13 +19,16 @@ import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag 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.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug 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( class RemoteListViewModel(
private val repository: RemoteMangaRepository, private val repository: RemoteMangaRepository,
private val searchRepository: MangaSearchRepository,
settings: AppSettings, settings: AppSettings,
dataRepository: MangaDataRepository, dataRepository: MangaDataRepository,
) : MangaListViewModel(settings), OnFilterChangedListener { ) : MangaListViewModel(settings), OnFilterChangedListener {
@ -46,9 +49,8 @@ class RemoteListViewModel(
listError, listError,
hasNextPage, hasNextPage,
) { list, mode, filterState, error, hasNext -> ) { list, mode, filterState, error, hasNext ->
buildList(list?.size?.plus(3) ?: 3) { buildList(list?.size?.plus(2) ?: 2) {
add(ListHeader(repository.source.title, 0, filterState.sortOrder)) add(ListHeader2(createChipsList(filterState), filterState.sortOrder))
createFilterModel(filterState)?.let { add(it) }
when { when {
list.isNullOrEmpty() && error != null -> add(error.toErrorState(canRetry = true)) list.isNullOrEmpty() && error != null -> add(error.toErrorState(canRetry = true))
list == null -> add(LoadingState) list == null -> add(LoadingState)
@ -88,10 +90,6 @@ class RemoteListViewModel(
loadList(filter.snapshot(), append = !mangaList.value.isNullOrEmpty()) loadList(filter.snapshot(), append = !mangaList.value.isNullOrEmpty())
} }
override fun onRemoveFilterTag(tag: MangaTag) {
filter.removeTag(tag)
}
override fun onSortItemClick(item: FilterItem.Sort) { override fun onSortItemClick(item: FilterItem.Sort) {
filter.onSortItemClick(item) filter.onSortItemClick(item)
} }
@ -110,6 +108,10 @@ class RemoteListViewModel(
fun resetFilter() = filter.reset() fun resetFilter() = filter.reset()
override fun onUpdateFilter(tags: Set<MangaTag>) {
applyFilter(tags)
}
fun applyFilter(tags: Set<MangaTag>) { fun applyFilter(tags: Set<MangaTag>) {
filter.setTags(tags) 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( private fun createEmptyState(filterState: FilterState) = EmptyState(
icon = R.drawable.ic_empty_search, icon = R.drawable.ic_empty_search,
textPrimary = R.string.nothing_found, textPrimary = R.string.nothing_found,
textSecondary = 0, textSecondary = 0,
actionStringRes = if (filterState.tags.isEmpty()) 0 else R.string.reset_filter, 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
}
} }

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

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

@ -113,6 +113,8 @@ class SearchSuggestionViewModel(
icon = 0, icon = 0,
title = tag.title, title = tag.title,
data = tag, data = tag,
isCheckable = false,
isChecked = false,
) )
} }
} }

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

@ -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
}

@ -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" />

@ -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>
Loading…
Cancel
Save