Refactor filters
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
package org.koitharu.kotatsu.list.domain
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.core.model.SortOrder
|
||||||
|
|
||||||
|
class AvailableFilters(
|
||||||
|
val sortOrders: Set<SortOrder>,
|
||||||
|
val tags: Set<MangaTag>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val size: Int
|
||||||
|
get() = sortOrders.size + tags.size
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
other as AvailableFilters
|
||||||
|
if (sortOrders != other.sortOrders) return false
|
||||||
|
if (tags != other.tags) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = sortOrders.hashCode()
|
||||||
|
result = 31 * result + tags.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isEmpty(): Boolean = sortOrders.isEmpty() && tags.isEmpty()
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaFilter
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.core.model.SortOrder
|
|
||||||
|
|
||||||
data class MangaFilterConfig(
|
|
||||||
val sortOrders: List<SortOrder>,
|
|
||||||
val tags: List<MangaTag>,
|
|
||||||
val currentFilter: MangaFilter?
|
|
||||||
)
|
|
||||||
@@ -22,19 +22,17 @@ import org.koitharu.kotatsu.base.ui.BaseFragment
|
|||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
|
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.ItemTypeDividerDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.ItemTypeDividerDecoration
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SectionItemDecoration
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||||
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
|
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
|
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
import org.koitharu.kotatsu.core.model.MangaFilter
|
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
||||||
import org.koitharu.kotatsu.list.ui.filter.FilterAdapter
|
import org.koitharu.kotatsu.list.ui.filter.FilterAdapter2
|
||||||
import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
|
import org.koitharu.kotatsu.list.ui.filter.FilterItem
|
||||||
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.main.ui.AppBarOwner
|
||||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
import org.koitharu.kotatsu.main.ui.MainActivity
|
||||||
@@ -42,10 +40,11 @@ import org.koitharu.kotatsu.utils.RecycledViewPoolHolder
|
|||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.*
|
||||||
|
|
||||||
abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||||
PaginationScrollListener.Callback, OnListItemClickListener<Manga>, OnFilterChangedListener,
|
PaginationScrollListener.Callback, OnListItemClickListener<Manga>,
|
||||||
SectionItemDecoration.Callback, SwipeRefreshLayout.OnRefreshListener {
|
SwipeRefreshLayout.OnRefreshListener {
|
||||||
|
|
||||||
private var listAdapter: MangaListAdapter? = null
|
private var listAdapter: MangaListAdapter? = null
|
||||||
|
private var filterAdapter: FilterAdapter2? = null
|
||||||
private var paginationListener: PaginationScrollListener? = null
|
private var paginationListener: PaginationScrollListener? = null
|
||||||
private val spanResolver = MangaListSpanResolver()
|
private val spanResolver = MangaListSpanResolver()
|
||||||
private val spanSizeLookup = SpanSizeLookup()
|
private val spanSizeLookup = SpanSizeLookup()
|
||||||
@@ -78,6 +77,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
|||||||
onRetryClick = ::resolveException,
|
onRetryClick = ::resolveException,
|
||||||
onTagRemoveClick = viewModel::onRemoveFilterTag
|
onTagRemoveClick = viewModel::onRemoveFilterTag
|
||||||
)
|
)
|
||||||
|
filterAdapter = FilterAdapter2(viewModel)
|
||||||
paginationListener = PaginationScrollListener(4, this)
|
paginationListener = PaginationScrollListener(4, this)
|
||||||
with(binding.recyclerView) {
|
with(binding.recyclerView) {
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
@@ -94,8 +94,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
|||||||
}
|
}
|
||||||
with(binding.recyclerViewFilter) {
|
with(binding.recyclerViewFilter) {
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
addItemDecoration(ItemTypeDividerDecoration(view.context))
|
adapter = filterAdapter
|
||||||
addItemDecoration(SectionItemDecoration(false, this@MangaListFragment))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(parentFragment as? RecycledViewPoolHolder)?.let {
|
(parentFragment as? RecycledViewPoolHolder)?.let {
|
||||||
@@ -113,6 +112,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
|||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
drawer = null
|
drawer = null
|
||||||
listAdapter = null
|
listAdapter = null
|
||||||
|
filterAdapter = null
|
||||||
paginationListener = null
|
paginationListener = null
|
||||||
spanSizeLookup.invalidateCache()
|
spanSizeLookup.invalidateCache()
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
@@ -203,28 +203,21 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun onInitFilter(config: MangaFilterConfig) {
|
protected fun onInitFilter(filter: List<FilterItem>) {
|
||||||
binding.recyclerViewFilter.adapter = FilterAdapter(
|
filterAdapter?.items = filter
|
||||||
sortOrders = config.sortOrders,
|
|
||||||
tags = config.tags,
|
|
||||||
state = config.currentFilter,
|
|
||||||
listener = this
|
|
||||||
)
|
|
||||||
drawer?.setDrawerLockMode(
|
drawer?.setDrawerLockMode(
|
||||||
if (config.sortOrders.isEmpty() && config.tags.isEmpty()) {
|
if (filter.isEmpty()) {
|
||||||
DrawerLayout.LOCK_MODE_LOCKED_CLOSED
|
DrawerLayout.LOCK_MODE_LOCKED_CLOSED
|
||||||
} else {
|
} else {
|
||||||
DrawerLayout.LOCK_MODE_UNLOCKED
|
DrawerLayout.LOCK_MODE_UNLOCKED
|
||||||
}
|
}
|
||||||
) ?: binding.dividerFilter?.let {
|
) ?: binding.dividerFilter?.let {
|
||||||
it.isGone = config.sortOrders.isEmpty() && config.tags.isEmpty()
|
it.isGone = filter.isEmpty()
|
||||||
binding.recyclerViewFilter.isVisible = it.isVisible
|
binding.recyclerViewFilter.isVisible = it.isVisible
|
||||||
}
|
}
|
||||||
activity?.invalidateOptionsMenu()
|
activity?.invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFilterChanged(filter: MangaFilter) = Unit
|
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
|
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
|
||||||
binding.recyclerViewFilter.updatePadding(
|
binding.recyclerViewFilter.updatePadding(
|
||||||
@@ -284,20 +277,6 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final override fun isSection(position: Int): Boolean {
|
|
||||||
return position == 0 || binding.recyclerViewFilter.adapter?.run {
|
|
||||||
getItemViewType(position) != getItemViewType(position - 1)
|
|
||||||
} ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
final override fun getSectionTitle(position: Int): CharSequence? {
|
|
||||||
return when (binding.recyclerViewFilter.adapter?.getItemViewType(position)) {
|
|
||||||
FilterAdapter.VIEW_TYPE_SORT -> getString(R.string.sort_order)
|
|
||||||
FilterAdapter.VIEW_TYPE_TAG -> getString(R.string.genres)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) = Unit
|
protected open fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) = Unit
|
||||||
|
|
||||||
protected open fun onPopupMenuItemSelected(item: MenuItem, data: Manga) = false
|
protected open fun onPopupMenuItemSelected(item: MenuItem, data: Manga) = false
|
||||||
|
|||||||
@@ -1,23 +1,32 @@
|
|||||||
package org.koitharu.kotatsu.list.ui
|
package org.koitharu.kotatsu.list.ui
|
||||||
|
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaFilter
|
||||||
import org.koitharu.kotatsu.core.model.MangaTag
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
|
import org.koitharu.kotatsu.list.domain.AvailableFilters
|
||||||
|
import org.koitharu.kotatsu.list.ui.filter.FilterItem
|
||||||
|
import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
|
|
||||||
abstract class MangaListViewModel(
|
abstract class MangaListViewModel(
|
||||||
private val settings: AppSettings
|
private val settings: AppSettings,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel(), OnFilterChangedListener {
|
||||||
|
|
||||||
abstract val content: LiveData<List<ListModel>>
|
abstract val content: LiveData<List<ListModel>>
|
||||||
val filter = MutableLiveData<MangaFilterConfig>()
|
val filter = MutableLiveData<List<FilterItem>>()
|
||||||
val listMode = MutableLiveData<ListMode>()
|
val listMode = MutableLiveData<ListMode>()
|
||||||
val gridScale = settings.observe()
|
val gridScale = settings.observe()
|
||||||
.filter { it == AppSettings.KEY_GRID_SIZE }
|
.filter { it == AppSettings.KEY_GRID_SIZE }
|
||||||
@@ -37,7 +46,62 @@ abstract class MangaListViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun onRemoveFilterTag(tag: MangaTag) = Unit
|
protected var currentFilter: MangaFilter = MangaFilter(null, emptySet())
|
||||||
|
private set(value) {
|
||||||
|
field = value
|
||||||
|
onFilterChanged()
|
||||||
|
}
|
||||||
|
protected var availableFilters: AvailableFilters? = null
|
||||||
|
private var filterJob: Job? = null
|
||||||
|
|
||||||
|
final override fun onSortItemClick(item: FilterItem.Sort) {
|
||||||
|
currentFilter = currentFilter.copy(sortOrder = item.order)
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onTagItemClick(item: FilterItem.Tag) {
|
||||||
|
val tags = if (item.isChecked) {
|
||||||
|
currentFilter.tags - item.tag
|
||||||
|
} else {
|
||||||
|
currentFilter.tags + item.tag
|
||||||
|
}
|
||||||
|
currentFilter = currentFilter.copy(tags = tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRemoveFilterTag(tag: MangaTag) {
|
||||||
|
val tags = currentFilter.tags
|
||||||
|
if (tag !in tags) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentFilter = currentFilter.copy(tags = tags - tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
open fun onFilterChanged() {
|
||||||
|
val previousJob = filterJob
|
||||||
|
filterJob = launchJob(Dispatchers.Default) {
|
||||||
|
previousJob?.cancelAndJoin()
|
||||||
|
filter.postValue(
|
||||||
|
availableFilters?.run {
|
||||||
|
val list = ArrayList<FilterItem>(size + 2)
|
||||||
|
if (sortOrders.isNotEmpty()) {
|
||||||
|
val selectedSort = currentFilter.sortOrder ?: sortOrders.first()
|
||||||
|
list += FilterItem.Header(R.string.sort_order)
|
||||||
|
sortOrders.sortedBy { it.ordinal }.mapTo(list) {
|
||||||
|
FilterItem.Sort(it, isSelected = it == selectedSort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tags.isNotEmpty()) {
|
||||||
|
list += FilterItem.Header(R.string.genres)
|
||||||
|
tags.sortedBy { it.title }.mapTo(list) {
|
||||||
|
FilterItem.Tag(it, isChecked = it in currentFilter.tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ensureActive()
|
||||||
|
list
|
||||||
|
}.orEmpty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
abstract fun onRefresh()
|
abstract fun onRefresh()
|
||||||
|
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaFilter
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.core.model.SortOrder
|
|
||||||
|
|
||||||
class FilterAdapter(
|
|
||||||
private val sortOrders: List<SortOrder> = emptyList(),
|
|
||||||
private val tags: List<MangaTag> = emptyList(),
|
|
||||||
state: MangaFilter?,
|
|
||||||
private val listener: OnFilterChangedListener
|
|
||||||
) : RecyclerView.Adapter<BaseViewHolder<*, Boolean, *>>() {
|
|
||||||
|
|
||||||
private var currentState = state ?: MangaFilter(sortOrders.firstOrNull(), emptySet())
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
|
|
||||||
VIEW_TYPE_SORT -> FilterSortHolder(parent).apply {
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
setCheckedSort(requireData())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
VIEW_TYPE_TAG -> FilterTagHolder(parent).apply {
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
setCheckedTag(boundData ?: return@setOnClickListener, !isChecked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> throw IllegalArgumentException("Unknown viewType $viewType")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() = sortOrders.size + tags.size
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BaseViewHolder<*, Boolean, *>, position: Int) {
|
|
||||||
when (holder) {
|
|
||||||
is FilterSortHolder -> {
|
|
||||||
val item = sortOrders[position]
|
|
||||||
holder.bind(item, item == currentState.sortOrder)
|
|
||||||
}
|
|
||||||
is FilterTagHolder -> {
|
|
||||||
val item = tags[position - sortOrders.size]
|
|
||||||
holder.bind(item, item in currentState.tags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) = when (position) {
|
|
||||||
in sortOrders.indices -> VIEW_TYPE_SORT
|
|
||||||
else -> VIEW_TYPE_TAG
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setCheckedTag(tag: MangaTag, isChecked: Boolean) {
|
|
||||||
currentState = if (tag in currentState.tags) {
|
|
||||||
if (!isChecked) {
|
|
||||||
currentState.copy(tags = currentState.tags - tag)
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isChecked) {
|
|
||||||
currentState.copy(tags = currentState.tags + tag)
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val index = tags.indexOf(tag)
|
|
||||||
if (index in tags.indices) {
|
|
||||||
notifyItemChanged(sortOrders.size + index)
|
|
||||||
}
|
|
||||||
listener.onFilterChanged(currentState)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setCheckedSort(sort: SortOrder) {
|
|
||||||
if (sort != currentState.sortOrder) {
|
|
||||||
val oldItemPos = sortOrders.indexOf(currentState.sortOrder)
|
|
||||||
val newItemPos = sortOrders.indexOf(sort)
|
|
||||||
currentState = currentState.copy(sortOrder = sort)
|
|
||||||
if (oldItemPos in sortOrders.indices) {
|
|
||||||
notifyItemChanged(oldItemPos)
|
|
||||||
}
|
|
||||||
if (newItemPos in sortOrders.indices) {
|
|
||||||
notifyItemChanged(newItemPos)
|
|
||||||
}
|
|
||||||
listener.onFilterChanged(currentState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val VIEW_TYPE_SORT = 0
|
|
||||||
const val VIEW_TYPE_TAG = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.filter
|
||||||
|
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
|
|
||||||
|
class FilterAdapter2(
|
||||||
|
listener: OnFilterChangedListener,
|
||||||
|
) : AsyncListDifferDelegationAdapter<FilterItem>(
|
||||||
|
FilterDiffCallback(),
|
||||||
|
filterSortDelegate(listener),
|
||||||
|
filterTagDelegate(listener),
|
||||||
|
filterHeaderDelegate(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.filter
|
||||||
|
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
|
||||||
|
|
||||||
|
fun filterSortDelegate(
|
||||||
|
listener: OnFilterChangedListener,
|
||||||
|
) = adapterDelegateViewBinding<FilterItem.Sort, FilterItem, ItemCheckableSingleBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemCheckableSingleBinding.inflate(layoutInflater, parent, false) }
|
||||||
|
) {
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
listener.onSortItemClick(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.root.setText(item.order.titleRes)
|
||||||
|
binding.root.isChecked = item.isSelected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun filterTagDelegate(
|
||||||
|
listener: OnFilterChangedListener,
|
||||||
|
) = adapterDelegateViewBinding<FilterItem.Tag, FilterItem, ItemCheckableMultipleBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) }
|
||||||
|
) {
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
listener.onTagItemClick(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.root.text = item.tag.title
|
||||||
|
binding.root.isChecked = item.isChecked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun filterHeaderDelegate() = adapterDelegateViewBinding<FilterItem.Header, FilterItem, ItemFilterHeaderBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }
|
||||||
|
) {
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.root.setText(item.titleResId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.filter
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
|
||||||
|
class FilterDiffCallback : DiffUtil.ItemCallback<FilterItem>() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
|
||||||
|
return when {
|
||||||
|
oldItem.javaClass != newItem.javaClass -> false
|
||||||
|
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
|
||||||
|
oldItem.titleResId == newItem.titleResId
|
||||||
|
}
|
||||||
|
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
||||||
|
oldItem.tag == newItem.tag
|
||||||
|
}
|
||||||
|
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
||||||
|
oldItem.order == newItem.order
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
|
||||||
|
return when {
|
||||||
|
oldItem is FilterItem.Header && newItem is FilterItem.Header -> true
|
||||||
|
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
||||||
|
oldItem.isChecked == newItem.isChecked
|
||||||
|
}
|
||||||
|
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
||||||
|
oldItem.isSelected == newItem.isSelected
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(oldItem: FilterItem, newItem: FilterItem): Any? {
|
||||||
|
val isCheckedChanged = when {
|
||||||
|
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
||||||
|
oldItem.isChecked != newItem.isChecked
|
||||||
|
}
|
||||||
|
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
||||||
|
oldItem.isSelected != newItem.isSelected
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
return if (isCheckedChanged) Unit else super.getChangePayload(oldItem, newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.filter
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.core.model.SortOrder
|
||||||
|
|
||||||
|
sealed interface FilterItem {
|
||||||
|
|
||||||
|
class Header(
|
||||||
|
@StringRes val titleResId: Int,
|
||||||
|
) : FilterItem
|
||||||
|
|
||||||
|
class Sort(
|
||||||
|
val order: SortOrder,
|
||||||
|
val isSelected: Boolean,
|
||||||
|
) : FilterItem
|
||||||
|
|
||||||
|
class Tag(
|
||||||
|
val tag: MangaTag,
|
||||||
|
val isChecked: Boolean,
|
||||||
|
) : FilterItem
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
|
||||||
import org.koitharu.kotatsu.core.model.SortOrder
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding
|
|
||||||
|
|
||||||
class FilterSortHolder(parent: ViewGroup) :
|
|
||||||
BaseViewHolder<SortOrder, Boolean, ItemCheckableSingleBinding>(
|
|
||||||
ItemCheckableSingleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
) {
|
|
||||||
|
|
||||||
override fun onBind(data: SortOrder, extra: Boolean) {
|
|
||||||
binding.root.setText(data.titleRes)
|
|
||||||
binding.root.isChecked = extra
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
|
|
||||||
|
|
||||||
class FilterTagHolder(parent: ViewGroup) :
|
|
||||||
BaseViewHolder<MangaTag, Boolean, ItemCheckableMultipleBinding>(
|
|
||||||
ItemCheckableMultipleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
) {
|
|
||||||
|
|
||||||
val isChecked: Boolean
|
|
||||||
get() = binding.root.isChecked
|
|
||||||
|
|
||||||
override fun onBind(data: MangaTag, extra: Boolean) {
|
|
||||||
binding.root.text = data.title
|
|
||||||
binding.root.isChecked = extra
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
package org.koitharu.kotatsu.list.ui.filter
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaFilter
|
interface OnFilterChangedListener {
|
||||||
|
|
||||||
fun interface OnFilterChangedListener {
|
fun onSortItemClick(item: FilterItem.Sort)
|
||||||
|
|
||||||
fun onFilterChanged(filter: MangaFilter)
|
fun onTagItemClick(item: FilterItem.Tag)
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ import android.view.MenuItem
|
|||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.model.MangaFilter
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity
|
import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity
|
||||||
@@ -29,10 +28,6 @@ class RemoteListFragment : MangaListFragment() {
|
|||||||
return source.title
|
return source.title
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFilterChanged(filter: MangaFilter) {
|
|
||||||
viewModel.applyFilter(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
inflater.inflate(R.menu.opt_list_remote, menu)
|
inflater.inflate(R.menu.opt_list_remote, menu)
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ import org.koitharu.kotatsu.BuildConfig
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
import org.koitharu.kotatsu.core.model.MangaFilter
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.list.ui.MangaFilterConfig
|
import org.koitharu.kotatsu.list.domain.AvailableFilters
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.*
|
import org.koitharu.kotatsu.list.ui.model.*
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
@@ -27,7 +25,6 @@ class RemoteListViewModel(
|
|||||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||||
private val hasNextPage = MutableStateFlow(false)
|
private val hasNextPage = MutableStateFlow(false)
|
||||||
private val listError = MutableStateFlow<Throwable?>(null)
|
private val listError = MutableStateFlow<Throwable?>(null)
|
||||||
private var appliedFilter: MangaFilter? = null
|
|
||||||
private var loadingJob: Job? = null
|
private var loadingJob: Job? = null
|
||||||
private val headerModel = ListHeader((repository as RemoteMangaRepository).title, 0)
|
private val headerModel = ListHeader((repository as RemoteMangaRepository).title, 0)
|
||||||
|
|
||||||
@@ -68,16 +65,6 @@ class RemoteListViewModel(
|
|||||||
loadList(append = !mangaList.value.isNullOrEmpty())
|
loadList(append = !mangaList.value.isNullOrEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRemoveFilterTag(tag: MangaTag) {
|
|
||||||
val filter = appliedFilter ?: return
|
|
||||||
if (tag !in filter.tags) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
applyFilter(
|
|
||||||
filter.copy(tags = filter.tags - tag)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadNextPage() {
|
fun loadNextPage() {
|
||||||
if (hasNextPage.value && listError.value == null) {
|
if (hasNextPage.value && listError.value == null) {
|
||||||
loadList(append = true)
|
loadList(append = true)
|
||||||
@@ -93,8 +80,8 @@ class RemoteListViewModel(
|
|||||||
listError.value = null
|
listError.value = null
|
||||||
val list = repository.getList2(
|
val list = repository.getList2(
|
||||||
offset = if (append) mangaList.value?.size ?: 0 else 0,
|
offset = if (append) mangaList.value?.size ?: 0 else 0,
|
||||||
sortOrder = appliedFilter?.sortOrder,
|
sortOrder = currentFilter.sortOrder,
|
||||||
tags = appliedFilter?.tags,
|
tags = currentFilter.tags,
|
||||||
)
|
)
|
||||||
if (!append) {
|
if (!append) {
|
||||||
mangaList.value = list
|
mangaList.value = list
|
||||||
@@ -111,26 +98,29 @@ class RemoteListViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyFilter(newFilter: MangaFilter) {
|
override fun onFilterChanged() {
|
||||||
appliedFilter = newFilter
|
super.onFilterChanged()
|
||||||
mangaList.value = null
|
mangaList.value = null
|
||||||
hasNextPage.value = false
|
hasNextPage.value = false
|
||||||
loadList(false)
|
loadList(false)
|
||||||
filter.value?.run {
|
|
||||||
filter.value = copy(currentFilter = newFilter)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createFilterModel() = appliedFilter?.run {
|
private fun createFilterModel(): CurrentFilterModel? {
|
||||||
CurrentFilterModel(tags.map { ChipsView.ChipModel(0, it.title, it) })
|
val tags = currentFilter.tags
|
||||||
|
return if (tags.isEmpty()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
CurrentFilterModel(tags.map { ChipsView.ChipModel(0, it.title, it) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFilter() {
|
private fun loadFilter() {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
try {
|
try {
|
||||||
val sorts = repository.sortOrders.sortedBy { it.ordinal }
|
val sorts = repository.sortOrders
|
||||||
val tags = repository.getTags().sortedBy { it.title }
|
val tags = repository.getTags()
|
||||||
filter.postValue(MangaFilterConfig(sorts, tags, appliedFilter))
|
availableFilters = AvailableFilters(sorts, tags)
|
||||||
|
onFilterChanged()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
Reference in New Issue
Block a user