Refactor ListModel

pull/421/head
Koitharu 3 years ago
parent 80db817ff2
commit 942d4fe5ab
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -1,7 +1,6 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
@ -9,15 +8,14 @@ import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.parsers.model.Manga
import kotlin.jvm.internal.Intrinsics
class BookmarksGroupAdapter(
coil: ImageLoader,
@ -26,7 +24,7 @@ class BookmarksGroupAdapter(
listener: ListStateHolderListener,
bookmarkClickListener: OnListItemClickListener<Bookmark>,
groupClickListener: OnListItemClickListener<BookmarksGroup>,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
init {
val pool = RecyclerView.RecycledViewPool()
@ -46,32 +44,4 @@ class BookmarksGroupAdapter(
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listener))
.addDelegate(errorStateListAD(listener))
}
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return when {
oldItem is BookmarksGroup && newItem is BookmarksGroup -> {
oldItem.manga.id == newItem.manga.id
}
oldItem is LoadingFooter && newItem is LoadingFooter -> {
oldItem.key == newItem.key
}
else -> oldItem.javaClass == newItem.javaClass
}
}
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
return when {
oldItem is BookmarksGroup && newItem is BookmarksGroup -> Unit
else -> super.getChangePayload(oldItem, newItem)
}
}
}
}

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.bookmarks.ui.model
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.areItemsEquals
@ -10,6 +11,18 @@ class BookmarksGroup(
val bookmarks: List<Bookmark>,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is BookmarksGroup && other.manga.id == manga.id
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is BookmarksGroup && previousState.bookmarks != bookmarks) {
ListModelDiffCallback.PAYLOAD_NESTED_LIST_CHANGED
} else {
super.getChangePayload(previousState)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

@ -15,7 +15,7 @@ class TrimTransformation(
private val tolerance: Int = 20,
) : Transformation {
override val cacheKey: String = javaClass.name
override val cacheKey: String = "${javaClass.name}-$tolerance"
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
var left = 0
@ -98,14 +98,23 @@ class TrimTransformation(
}
}
override fun equals(other: Any?) = other is TrimTransformation
override fun hashCode() = javaClass.hashCode()
private fun isColorTheSame(@ColorInt a: Int, @ColorInt b: Int): Boolean {
return abs(a.red - b.red) <= tolerance &&
abs(a.green - b.green) <= tolerance &&
abs(a.blue - b.blue) <= tolerance &&
abs(a.alpha - b.alpha) <= tolerance
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TrimTransformation
return tolerance == other.tolerance
}
override fun hashCode(): Int {
return tolerance
}
}

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.core.util.ext.format
import org.koitharu.kotatsu.list.ui.model.ListModel
import java.util.Date
sealed class DateTimeAgo : ListModel {
sealed class DateTimeAgo {
abstract fun format(resources: Resources): String

@ -1,5 +1,7 @@
package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
class MangaBranch(
@ -8,6 +10,18 @@ class MangaBranch(
val isSelected: Boolean,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is MangaBranch && other.name == name
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is MangaBranch && previousState.isSelected != isSelected) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

@ -9,13 +9,14 @@ import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemScrobblingInfoBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
fun scrobblingInfoAD(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
fragmentManager: FragmentManager,
) = adapterDelegateViewBinding<ScrobblingInfo, ScrobblingInfo, ItemScrobblingInfoBinding>(
) = adapterDelegateViewBinding<ScrobblingInfo, ListModel, ItemScrobblingInfoBinding>(
{ layoutInflater, parent -> ItemScrobblingInfoBinding.inflate(layoutInflater, parent, false) },
) {
binding.root.setOnClickListener {

@ -2,33 +2,18 @@ package org.koitharu.kotatsu.details.ui.scrobbling
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
class ScrollingInfoAdapter(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
fragmentManager: FragmentManager,
) : AsyncListDifferDelegationAdapter<ScrobblingInfo>(DiffCallback()) {
) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
init {
delegatesManager.addDelegate(scrobblingInfoAD(lifecycleOwner, coil, fragmentManager))
}
private class DiffCallback : DiffUtil.ItemCallback<ScrobblingInfo>() {
override fun areItemsTheSame(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Boolean {
return oldItem.scrobbler == newItem.scrobbler
}
override fun areContentsTheSame(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Any {
return Unit
}
}
}

@ -47,6 +47,24 @@ class DownloadItemModel(
return timestamp.compareTo(other.timestamp)
}
override fun areItemsTheSame(other: ListModel): Boolean {
return other is DownloadItemModel && other.id == id
}
override fun getChangePayload(previousState: ListModel): Any? {
return when (previousState) {
is DownloadItemModel -> {
if (workState == previousState.workState) {
Unit
} else {
null
}
}
else -> super.getChangePayload(previousState)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

@ -1,62 +1,25 @@
package org.koitharu.kotatsu.download.ui.list
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.adapter.relatedDateItemAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import kotlin.jvm.internal.Intrinsics
class DownloadsAdapter(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
listener: DownloadItemListener,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
init {
delegatesManager.addDelegate(ITEM_TYPE_DOWNLOAD, downloadItemAD(lifecycleOwner, coil, listener))
.addDelegate(loadingStateAD())
.addDelegate(emptyStateListAD(coil, lifecycleOwner, null))
.addDelegate(relatedDateItemAD())
}
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when {
oldItem is DownloadItemModel && newItem is DownloadItemModel -> {
oldItem.id == newItem.id
}
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
oldItem == newItem
}
else -> oldItem.javaClass == newItem.javaClass
}
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
return when (newItem) {
is DownloadItemModel -> {
oldItem as DownloadItemModel
if (oldItem.workState == newItem.workState) {
Unit
} else {
null
}
}
else -> super.getChangePayload(oldItem, newItem)
}
}
.addDelegate(listHeaderAD(null))
}
companion object {

@ -26,6 +26,7 @@ import org.koitharu.kotatsu.core.util.ext.daysDiff
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.parsers.model.Manga
@ -183,7 +184,7 @@ class DownloadsViewModel @Inject constructor(
for (item in this) {
val date = timeAgo(item.timestamp)
if (prevDate != date) {
destination += date
destination += ListHeader(date, 0, null)
}
prevDate = date
destination += item

@ -31,9 +31,10 @@ import org.koitharu.kotatsu.databinding.FragmentExploreBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.explore.ui.adapter.ExploreAdapter
import org.koitharu.kotatsu.explore.ui.adapter.ExploreListEventListener
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.history.ui.HistoryActivity
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.ui.MangaListActivity
@ -46,7 +47,7 @@ class ExploreFragment :
BaseFragment<FragmentExploreBinding>(),
RecyclerViewOwner,
ExploreListEventListener,
OnListItemClickListener<ExploreItem.Source> {
OnListItemClickListener<MangaSourceItem> {
@Inject
lateinit var coil: ImageLoader
@ -96,7 +97,7 @@ class ExploreFragment :
)
}
override fun onManageClick(view: View) {
override fun onListHeaderClick(item: ListHeader, view: View) {
startActivity(SettingsActivity.newManageSourcesIntent(view.context))
}
@ -117,12 +118,12 @@ class ExploreFragment :
startActivity(intent)
}
override fun onItemClick(item: ExploreItem.Source, view: View) {
override fun onItemClick(item: MangaSourceItem, view: View) {
val intent = MangaListActivity.newIntent(view.context, item.source)
startActivity(intent)
}
override fun onItemLongClick(item: ExploreItem.Source, view: View): Boolean {
override fun onItemLongClick(item: MangaSourceItem, view: View): Boolean {
val menu = PopupMenu(view.context, view)
menu.inflate(R.menu.popup_source)
menu.setOnMenuItemClickListener(SourceMenuListener(item))
@ -132,7 +133,9 @@ class ExploreFragment :
override fun onRetryClick(error: Throwable) = Unit
override fun onEmptyActionClick() = onManageClick(requireView())
override fun onEmptyActionClick() {
startActivity(SettingsActivity.newManageSourcesIntent(context ?: return))
}
private fun onOpenManga(manga: Manga) {
val intent = DetailsActivity.newIntent(context ?: return, manga)
@ -164,7 +167,7 @@ class ExploreFragment :
}
private inner class SourceMenuListener(
private val sourceItem: ExploreItem.Source,
private val sourceItem: MangaSourceItem,
) : PopupMenu.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean {

@ -22,7 +22,13 @@ import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.explore.ui.model.RecommendationsItem
import org.koitharu.kotatsu.list.ui.model.EmptyHint
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
@ -46,13 +52,13 @@ class ExploreViewModel @Inject constructor(
val onActionDone = MutableEventFlow<ReversibleAction>()
val onShowSuggestionsTip = MutableEventFlow<Unit>()
val content: StateFlow<List<ExploreItem>> = isLoading.flatMapLatest { loading ->
val content: StateFlow<List<ListModel>> = isLoading.flatMapLatest { loading ->
if (loading) {
flowOf(listOf(ExploreItem.Loading))
flowOf(getLoadingStateList())
} else {
createContentFlow()
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(ExploreItem.Loading))
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, getLoadingStateList())
init {
launchJob(Dispatchers.Default) {
@ -98,13 +104,11 @@ class ExploreViewModel @Inject constructor(
.map { settings.getMangaSources(includeHidden = false) }
.combine(isGrid) { content, grid -> buildList(content, grid) }
private fun buildList(sources: List<MangaSource>, isGrid: Boolean): List<ExploreItem> {
val result = ArrayList<ExploreItem>(sources.size + 3)
result += ExploreItem.Buttons(
isSuggestionsEnabled = settings.isSuggestionsEnabled,
)
result += ExploreItem.Header(R.string.suggestions, isButtonVisible = false)
result += ExploreItem.Recommendation(
private fun buildList(sources: List<MangaSource>, isGrid: Boolean): List<ListModel> {
val result = ArrayList<ListModel>(sources.size + 4)
result += ExploreButtons()
result += ListHeader(R.string.suggestions, 0, null)
result += RecommendationsItem(
Manga(
0,
"Test",
@ -123,11 +127,11 @@ class ExploreViewModel @Inject constructor(
MangaSource.DESUME,
),
) // TODO
result += ExploreItem.Header(R.string.remote_sources, sources.isNotEmpty())
if (sources.isNotEmpty()) {
sources.mapTo(result) { ExploreItem.Source(it, isGrid) }
result += ListHeader(R.string.remote_sources, R.string.manage, null)
sources.mapTo(result) { MangaSourceItem(it, isGrid) }
} else {
result += ExploreItem.EmptyHint(
result += EmptyHint(
icon = R.drawable.ic_empty_common,
textPrimary = R.string.no_manga_sources,
textSecondary = R.string.no_manga_sources_text,
@ -136,4 +140,9 @@ class ExploreViewModel @Inject constructor(
}
return result
}
private fun getLoadingStateList() = listOf(
ExploreButtons(),
LoadingState,
)
}

@ -4,25 +4,29 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
class ExploreAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: ExploreListEventListener,
clickListener: OnListItemClickListener<ExploreItem.Source>,
) : AsyncListDifferDelegationAdapter<ExploreItem>(ExploreDiffCallback()) {
clickListener: OnListItemClickListener<MangaSourceItem>,
) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
init {
delegatesManager
.addDelegate(ITEM_TYPE_BUTTONS, exploreButtonsAD(listener))
.addDelegate(ITEM_TYPE_RECOMMENDATION_HEADER, exploreRecommendationHeaderAD())
.addDelegate(ITEM_TYPE_RECOMMENDATION, exploreRecommendationItemAD(coil, listener, lifecycleOwner))
.addDelegate(ITEM_TYPE_HEADER, exploreSourcesHeaderAD(listener))
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD(listener))
.addDelegate(ITEM_TYPE_SOURCE_LIST, exploreSourceListItemAD(coil, clickListener, lifecycleOwner))
.addDelegate(ITEM_TYPE_SOURCE_GRID, exploreSourceGridItemAD(coil, clickListener, lifecycleOwner))
.addDelegate(ITEM_TYPE_HINT, exploreEmptyHintListAD(listener))
.addDelegate(ITEM_TYPE_LOADING, exploreLoadingAD())
.addDelegate(ITEM_TYPE_HINT, emptyHintAD(coil, lifecycleOwner, listener))
.addDelegate(ITEM_TYPE_LOADING, loadingStateAD())
}
companion object {
@ -33,7 +37,6 @@ class ExploreAdapter(
const val ITEM_TYPE_SOURCE_GRID = 3
const val ITEM_TYPE_HINT = 4
const val ITEM_TYPE_LOADING = 5
const val ITEM_TYPE_RECOMMENDATION_HEADER = 6
const val ITEM_TYPE_RECOMMENDATION = 7
const val ITEM_TYPE_RECOMMENDATION = 6
}
}

@ -1,10 +1,8 @@
package org.koitharu.kotatsu.explore.ui.adapter
import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
@ -14,20 +12,19 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
import org.koitharu.kotatsu.databinding.ItemExploreSourceGridBinding
import org.koitharu.kotatsu.databinding.ItemExploreSourceListBinding
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
import org.koitharu.kotatsu.databinding.ItemRecommendationBinding
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.explore.ui.model.RecommendationsItem
import org.koitharu.kotatsu.list.ui.model.ListModel
fun exploreButtonsAD(
clickListener: View.OnClickListener,
) = adapterDelegateViewBinding<ExploreItem.Buttons, ExploreItem, ItemExploreButtonsBinding>(
) = adapterDelegateViewBinding<ExploreButtons, ListModel, ItemExploreButtonsBinding>(
{ layoutInflater, parent -> ItemExploreButtonsBinding.inflate(layoutInflater, parent, false) },
) {
@ -43,21 +40,11 @@ fun exploreButtonsAD(
//}
}
fun exploreRecommendationHeaderAD() = adapterDelegateViewBinding<ExploreItem.Header, ExploreItem, ItemHeaderButtonBinding>(
{ layoutInflater, parent -> ItemHeaderButtonBinding.inflate(layoutInflater, parent, false) }
) {
bind {
binding.textViewTitle.setText(item.titleResId)
binding.buttonMore.isVisible = false
}
}
fun exploreRecommendationItemAD(
coil: ImageLoader,
clickListener: View.OnClickListener,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<ExploreItem.Recommendation, ExploreItem, ItemRecommendationBinding>(
) = adapterDelegateViewBinding<RecommendationsItem, ListModel, ItemRecommendationBinding>(
{ layoutInflater, parent -> ItemRecommendationBinding.inflate(layoutInflater, parent, false) }
) {
@ -77,31 +64,13 @@ fun exploreRecommendationItemAD(
}
}
fun exploreSourcesHeaderAD(
listener: ExploreListEventListener,
) = adapterDelegateViewBinding<ExploreItem.Header, ExploreItem, ItemHeaderButtonBinding>(
{ layoutInflater, parent -> ItemHeaderButtonBinding.inflate(layoutInflater, parent, false) },
) {
val listenerAdapter = View.OnClickListener {
listener.onManageClick(itemView)
}
binding.buttonMore.setOnClickListener(listenerAdapter)
bind {
binding.textViewTitle.setText(item.titleResId)
binding.buttonMore.isVisible = item.isButtonVisible
}
}
fun exploreSourceListItemAD(
coil: ImageLoader,
listener: OnListItemClickListener<ExploreItem.Source>,
listener: OnListItemClickListener<MangaSourceItem>,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<ExploreItem.Source, ExploreItem, ItemExploreSourceListBinding>(
) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceListBinding>(
{ layoutInflater, parent -> ItemExploreSourceListBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is ExploreItem.Source && !item.isGrid },
on = { item, _, _ -> item is MangaSourceItem && !item.isGrid },
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
@ -128,11 +97,11 @@ fun exploreSourceListItemAD(
fun exploreSourceGridItemAD(
coil: ImageLoader,
listener: OnListItemClickListener<ExploreItem.Source>,
listener: OnListItemClickListener<MangaSourceItem>,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<ExploreItem.Source, ExploreItem, ItemExploreSourceGridBinding>(
) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceGridBinding>(
{ layoutInflater, parent -> ItemExploreSourceGridBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is ExploreItem.Source && item.isGrid },
on = { item, _, _ -> item is MangaSourceItem && item.isGrid },
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
@ -156,21 +125,3 @@ fun exploreSourceGridItemAD(
binding.imageViewIcon.disposeImageRequest()
}
}
fun exploreEmptyHintListAD(
listener: ListStateHolderListener,
) = adapterDelegateViewBinding<ExploreItem.EmptyHint, ExploreItem, ItemEmptyCardBinding>(
{ inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) },
) {
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
bind {
binding.icon.setImageResource(item.icon)
binding.textPrimary.setText(item.textPrimary)
binding.textSecondary.setTextAndVisible(item.textSecondary)
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
}
}
fun exploreLoadingAD() = adapterDelegate<ExploreItem.Loading, ExploreItem>(R.layout.item_loading_state) {}

@ -1,29 +0,0 @@
package org.koitharu.kotatsu.explore.ui.adapter
import androidx.recyclerview.widget.DiffUtil
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
class ExploreDiffCallback : DiffUtil.ItemCallback<ExploreItem>() {
override fun areItemsTheSame(oldItem: ExploreItem, newItem: ExploreItem): Boolean {
return when {
oldItem.javaClass != newItem.javaClass -> false
oldItem is ExploreItem.Buttons && newItem is ExploreItem.Buttons -> true
oldItem is ExploreItem.Loading && newItem is ExploreItem.Loading -> true
oldItem is ExploreItem.EmptyHint && newItem is ExploreItem.EmptyHint -> true
oldItem is ExploreItem.Source && newItem is ExploreItem.Source -> {
oldItem.source == newItem.source && oldItem.isGrid == newItem.isGrid
}
oldItem is ExploreItem.Header && newItem is ExploreItem.Header -> {
oldItem.titleResId == newItem.titleResId
}
else -> false
}
}
override fun areContentsTheSame(oldItem: ExploreItem, newItem: ExploreItem): Boolean {
return oldItem == newItem
}
}

@ -1,9 +1,7 @@
package org.koitharu.kotatsu.explore.ui.adapter
import android.view.View
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
interface ExploreListEventListener : ListStateHolderListener, View.OnClickListener {
fun onManageClick(view: View)
}
interface ExploreListEventListener : ListStateHolderListener, View.OnClickListener, ListHeaderClickListener

@ -0,0 +1,19 @@
package org.koitharu.kotatsu.explore.ui.model
import org.koitharu.kotatsu.list.ui.model.ListModel
class ExploreButtons : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is ExploreButtons
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
return javaClass == other?.javaClass
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}

@ -1,104 +0,0 @@
package org.koitharu.kotatsu.explore.ui.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
sealed interface ExploreItem : ListModel {
class Buttons(
val isSuggestionsEnabled: Boolean
) : ExploreItem {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Buttons
return isSuggestionsEnabled == other.isSuggestionsEnabled
}
override fun hashCode(): Int {
return isSuggestionsEnabled.hashCode()
}
}
class Header(
@StringRes val titleResId: Int,
val isButtonVisible: Boolean,
) : ExploreItem {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Header
if (titleResId != other.titleResId) return false
return isButtonVisible == other.isButtonVisible
}
override fun hashCode(): Int {
var result = titleResId
result = 31 * result + isButtonVisible.hashCode()
return result
}
}
class Recommendation(
val manga: Manga
) : ExploreItem {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Recommendation
return manga == other.manga
}
override fun hashCode(): Int {
return 31 * manga.hashCode()
}
}
class Source(
val source: MangaSource,
val isGrid: Boolean,
) : ExploreItem {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Source
if (source != other.source) return false
return isGrid == other.isGrid
}
override fun hashCode(): Int {
var result = source.hashCode()
result = 31 * result + isGrid.hashCode()
return result
}
}
class EmptyHint(
@DrawableRes icon: Int,
@StringRes textPrimary: Int,
@StringRes textSecondary: Int,
@StringRes actionStringRes: Int,
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes), ExploreItem
object Loading : ExploreItem {
override fun equals(other: Any?): Boolean = other === Loading
}
}

@ -0,0 +1,30 @@
package org.koitharu.kotatsu.explore.ui.model
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaSource
class MangaSourceItem(
val source: MangaSource,
val isGrid: Boolean,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is MangaSourceItem && other.source == source
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaSourceItem
if (source != other.source) return false
return isGrid == other.isGrid
}
override fun hashCode(): Int {
var result = source.hashCode()
result = 31 * result + isGrid.hashCode()
return result
}
}

@ -0,0 +1,27 @@
package org.koitharu.kotatsu.explore.ui.model
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
class RecommendationsItem(
val manga: Manga
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is RecommendationsItem
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as RecommendationsItem
return manga == other.manga
}
override fun hashCode(): Int {
return 31 * manga.hashCode()
}
}

@ -1,61 +1,25 @@
package org.koitharu.kotatsu.favourites.ui.categories.adapter
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import kotlin.jvm.internal.Intrinsics
class CategoriesAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
onItemClickListener: FavouriteCategoriesListListener,
listListener: ListStateHolderListener,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
init {
delegatesManager.addDelegate(categoryAD(coil, lifecycleOwner, onItemClickListener))
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listListener))
.addDelegate(loadingStateAD())
}
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return when {
oldItem is CategoryListModel && newItem is CategoryListModel -> {
oldItem.category.id == newItem.category.id
}
else -> oldItem.javaClass == newItem.javaClass
}
}
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
return when {
oldItem is CategoryListModel && newItem is CategoryListModel -> {
if (oldItem.category == newItem.category &&
oldItem.mangaCount == newItem.mangaCount &&
oldItem.covers == newItem.covers &&
oldItem.isReorderMode != newItem.isReorderMode
) {
Unit
} else {
super.getChangePayload(oldItem, newItem)
}
}
else -> super.getChangePayload(oldItem, newItem)
}
}
}
}

@ -11,6 +11,10 @@ class CategoryListModel(
val isReorderMode: Boolean,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is CategoryListModel && other.category.id == category.id
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

@ -4,5 +4,16 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
class CategoriesHeaderItem : ListModel {
override fun equals(other: Any?): Boolean = other?.javaClass == CategoriesHeaderItem::class.java
override fun areItemsTheSame(other: ListModel): Boolean {
return other is CategoriesHeaderItem
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
return javaClass == other?.javaClass
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}

@ -1,9 +1,41 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.model
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
data class MangaCategoryItem(
class MangaCategoryItem(
val id: Long,
val name: String,
val isChecked: Boolean,
) : ListModel
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is MangaCategoryItem && other.id == id
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is MangaCategoryItem && previousState.isChecked != isChecked) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaCategoryItem
if (id != other.id) return false
if (name != other.name) return false
return isChecked == other.isChecked
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + isChecked.hashCode()
return result
}
}

@ -5,7 +5,7 @@ import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.filter.ui.model.FilterItem
import org.koitharu.kotatsu.list.ui.adapter.listSimpleHeaderAD
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
@ -16,12 +16,8 @@ class FilterAdapter(
) : AsyncListDifferDelegationAdapter<ListModel>(FilterDiffCallback()), FastScroller.SectionIndexer {
init {
delegatesManager
.addDelegate(filterSortDelegate(listener))
.addDelegate(filterTagDelegate(listener))
.addDelegate(listSimpleHeaderAD())
.addDelegate(loadingStateAD())
.addDelegate(loadingFooterAD())
delegatesManager.addDelegate(filterSortDelegate(listener)).addDelegate(filterTagDelegate(listener))
.addDelegate(listHeaderAD(null)).addDelegate(loadingStateAD()).addDelegate(loadingFooterAD())
.addDelegate(filterErrorDelegate())
differ.addListListener(listListener)
}
@ -36,11 +32,4 @@ class FilterAdapter(
}
return null
}
companion object {
const val ITEM_TYPE_HEADER = 0
const val ITEM_TYPE_SORT = 1
const val ITEM_TYPE_TAG = 2
}
}

@ -8,7 +8,7 @@ class FilterHeaderModel(
val chips: Collection<ChipsView.ChipModel>,
val sortOrder: SortOrder?,
val hasSelectedTags: Boolean,
) : ListModel {
) {
val textSummary: String
get() = chips.mapNotNull { if (it.isChecked) it.title else null }.joinToString()

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.filter.ui.model
import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
@ -12,6 +13,18 @@ sealed interface FilterItem : ListModel {
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)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@ -34,6 +47,18 @@ sealed interface FilterItem : ListModel {
val isChecked: Boolean,
) : FilterItem {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Tag && 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)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@ -55,6 +80,10 @@ sealed interface FilterItem : ListModel {
@StringRes val textResId: Int,
) : FilterItem {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Error && textResId == other.textResId
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

@ -24,6 +24,7 @@ import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
@ -108,7 +109,7 @@ class HistoryListViewModel @Inject constructor(
if (grouped) {
val date = timeAgo(history.updatedAt)
if (prevDate != date) {
result += date
result += ListHeader(date, 0, null)
}
prevDate = date
}

@ -0,0 +1,22 @@
package org.koitharu.kotatsu.list.ui
import androidx.recyclerview.widget.DiffUtil
import org.koitharu.kotatsu.list.ui.model.ListModel
object ListModelDiffCallback : DiffUtil.ItemCallback<ListModel>() {
val PAYLOAD_CHECKED_CHANGED = Any()
val PAYLOAD_NESTED_LIST_CHANGED = Any()
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return oldItem.areItemsTheSame(newItem)
}
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
return newItem.getChangePayload(oldItem)
}
}

@ -1,36 +0,0 @@
package org.koitharu.kotatsu.list.ui.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaTag
@Deprecated("")
fun listHeader2AD(
listener: MangaListListener,
) = adapterDelegateViewBinding<FilterHeaderModel, ListModel, FragmentFilterHeaderBinding>(
{ layoutInflater, parent -> FragmentFilterHeaderBinding.inflate(layoutInflater, parent, false) },
) {
var ignoreChecking = false
binding.chipsTags.setOnCheckedStateChangeListener { _, _ ->
if (!ignoreChecking) {
listener.onUpdateFilter(binding.chipsTags.getCheckedData(MangaTag::class.java))
}
}
bind { payloads ->
if (payloads.isNotEmpty()) {
if (context.isAnimationsEnabled) {
binding.scrollView.smoothScrollTo(0, 0)
} else {
binding.scrollView.scrollTo(0, 0)
}
}
ignoreChecking = true
binding.chipsTags.setChips(item.chips) // TODO use recyclerview
ignoreChecking = false
}
}

@ -3,7 +3,6 @@ package org.koitharu.kotatsu.list.ui.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
import org.koitharu.kotatsu.databinding.ItemHeaderSingleBinding
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
@ -23,12 +22,3 @@ fun listHeaderAD(
binding.buttonMore.setTextAndVisible(item.buttonTextRes)
}
}
fun listSimpleHeaderAD() = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderSingleBinding>(
{ inflater, parent -> ItemHeaderSingleBinding.inflate(inflater, parent, false) },
) {
bind {
binding.textViewTitle.text = item.getText(context)
}
}

@ -6,6 +6,7 @@ import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
@ -44,6 +45,7 @@ fun mangaGridItemAD(
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
transformations(TrimTransformation())
allowRgb565(true)
source(item.source)
enqueueWith(coil)

@ -1,25 +1,16 @@
package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
import org.koitharu.kotatsu.list.ui.model.MangaListModel
import kotlin.jvm.internal.Intrinsics
open class MangaListAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: MangaListListener,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
init {
delegatesManager
@ -28,64 +19,10 @@ open class MangaListAdapter(
.addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, listener, null))
.addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD())
.addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD())
.addDelegate(ITEM_TYPE_DATE, relatedDateItemAD())
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD(listener))
.addDelegate(ITEM_TYPE_HEADER_2, listHeader2AD(listener))
}
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when {
oldItem is MangaListModel && newItem is MangaListModel -> {
oldItem.id == newItem.id
}
oldItem is MangaListDetailedModel && newItem is MangaListDetailedModel -> {
oldItem.id == newItem.id
}
oldItem is MangaGridModel && newItem is MangaGridModel -> {
oldItem.id == newItem.id
}
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
oldItem == newItem
}
oldItem is ListHeader && newItem is ListHeader -> {
oldItem.textRes == newItem.textRes &&
oldItem.text == newItem.text &&
oldItem.dateTimeAgo == newItem.dateTimeAgo
}
oldItem is LoadingFooter && newItem is LoadingFooter -> {
oldItem.key == newItem.key
}
else -> oldItem.javaClass == newItem.javaClass
}
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
return when (newItem) {
is MangaItemModel -> {
oldItem as MangaItemModel
if (oldItem.progress != newItem.progress) {
PAYLOAD_PROGRESS
} else {
}
}
is FilterHeaderModel -> Unit
else -> super.getChangePayload(oldItem, newItem)
}
}
}
companion object {
@ -95,12 +32,10 @@ open class MangaListAdapter(
const val ITEM_TYPE_MANGA_GRID = 2
const val ITEM_TYPE_LOADING_FOOTER = 3
const val ITEM_TYPE_LOADING_STATE = 4
const val ITEM_TYPE_DATE = 5
const val ITEM_TYPE_ERROR_STATE = 6
const val ITEM_TYPE_ERROR_FOOTER = 7
const val ITEM_TYPE_EMPTY = 8
const val ITEM_TYPE_HEADER = 9
const val ITEM_TYPE_HEADER_2 = 10
val PAYLOAD_PROGRESS = Any()
}

@ -9,6 +9,7 @@ import com.google.android.material.chip.Chip
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
@ -57,6 +58,7 @@ fun mangaListDetailedItemAD(
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
transformations(TrimTransformation())
allowRgb565(true)
source(item.source)
enqueueWith(coil)

@ -5,6 +5,7 @@ import coil.ImageLoader
import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
@ -40,6 +41,7 @@ fun mangaListItemAD(
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
transformations(TrimTransformation())
source(item.source)
enqueueWith(coil)
}

@ -1,14 +0,0 @@
package org.koitharu.kotatsu.list.ui.adapter
import android.widget.TextView
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.list.ui.model.ListModel
fun relatedDateItemAD() = adapterDelegate<DateTimeAgo, ListModel>(R.layout.item_header) {
bind {
(itemView as TextView).text = item.format(context.resources)
}
}

@ -4,11 +4,35 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
class EmptyHint(
@DrawableRes icon: Int,
@StringRes textPrimary: Int,
@StringRes textSecondary: Int,
@StringRes actionStringRes: Int,
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes) {
@DrawableRes val icon: Int,
@StringRes val textPrimary: Int,
@StringRes val textSecondary: Int,
@StringRes val actionStringRes: Int,
) : ListModel {
fun toState() = EmptyState(icon, textPrimary, textSecondary, actionStringRes)
override fun areItemsTheSame(other: ListModel): Boolean {
return other is EmptyHint && textPrimary == other.textPrimary
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as EmptyHint
if (icon != other.icon) return false
if (textPrimary != other.textPrimary) return false
if (textSecondary != other.textSecondary) return false
return actionStringRes == other.actionStringRes
}
override fun hashCode(): Int {
var result = icon
result = 31 * result + textPrimary
result = 31 * result + textSecondary
result = 31 * result + actionStringRes
return result
}
}

@ -3,7 +3,7 @@ package org.koitharu.kotatsu.list.ui.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
open class EmptyState(
class EmptyState(
@DrawableRes val icon: Int,
@StringRes val textPrimary: Int,
@StringRes val textSecondary: Int,
@ -19,9 +19,7 @@ open class EmptyState(
if (icon != other.icon) return false
if (textPrimary != other.textPrimary) return false
if (textSecondary != other.textSecondary) return false
if (actionStringRes != other.actionStringRes) return false
return true
return actionStringRes == other.actionStringRes
}
override fun hashCode(): Int {
@ -31,4 +29,8 @@ open class EmptyState(
result = 31 * result + actionStringRes
return result
}
override fun areItemsTheSame(other: ListModel): Boolean {
return other is EmptyState
}
}

@ -2,7 +2,28 @@ package org.koitharu.kotatsu.list.ui.model
import androidx.annotation.DrawableRes
data class ErrorFooter(
class ErrorFooter(
val exception: Throwable,
@DrawableRes val icon: Int
) : ListModel
) : ListModel {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ErrorFooter
if (exception != other.exception) return false
return icon == other.icon
}
override fun hashCode(): Int {
var result = exception.hashCode()
result = 31 * result + icon
return result
}
override fun areItemsTheSame(other: ListModel): Boolean {
return other is ErrorFooter && exception == other.exception
}
}

@ -3,9 +3,32 @@ package org.koitharu.kotatsu.list.ui.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
data class ErrorState(
class ErrorState(
val exception: Throwable,
@DrawableRes val icon: Int,
val canRetry: Boolean,
@StringRes val buttonText: Int
) : ListModel
) : ListModel {
override fun areItemsTheSame(other: ListModel) = other is ErrorState
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ErrorState
if (exception != other.exception) return false
if (icon != other.icon) return false
if (canRetry != other.canRetry) return false
return buttonText == other.buttonText
}
override fun hashCode(): Int {
var result = exception.hashCode()
result = 31 * result + icon
result = 31 * result + canRetry.hashCode()
result = 31 * result + buttonText
return result
}
}

@ -5,9 +5,9 @@ import androidx.annotation.StringRes
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
class ListHeader private constructor(
val text: CharSequence?,
@StringRes val textRes: Int,
val dateTimeAgo: DateTimeAgo?,
private val text: CharSequence?,
@StringRes private val textRes: Int,
private val dateTimeAgo: DateTimeAgo?,
@StringRes val buttonTextRes: Int,
val payload: Any?,
) : ListModel {
@ -36,6 +36,10 @@ class ListHeader private constructor(
else -> dateTimeAgo?.format(context.resources)
}
override fun areItemsTheSame(other: ListModel): Boolean {
return other is ListHeader && text == other.text && dateTimeAgo == other.dateTimeAgo && textRes == other.textRes
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

@ -3,4 +3,8 @@ package org.koitharu.kotatsu.list.ui.model
interface ListModel {
override fun equals(other: Any?): Boolean
fun areItemsTheSame(other: ListModel): Boolean
fun getChangePayload(previousState: ListModel): Any? = null
}

@ -16,4 +16,8 @@ class LoadingFooter @JvmOverloads constructor(
override fun hashCode(): Int {
return key
}
override fun areItemsTheSame(other: ListModel): Boolean {
return other is LoadingFooter && key == other.key
}
}

@ -3,4 +3,8 @@ package org.koitharu.kotatsu.list.ui.model
object LoadingState : ListModel {
override fun equals(other: Any?): Boolean = other === LoadingState
override fun areItemsTheSame(other: ListModel): Boolean {
return other is LoadingState
}
}

@ -1,12 +1,38 @@
package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.parsers.model.Manga
data class MangaGridModel(
class MangaGridModel(
override val id: Long,
override val title: String,
override val coverUrl: String,
override val manga: Manga,
override val counter: Int,
override val progress: Float,
) : MangaItemModel
) : MangaItemModel() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaGridModel
if (id != other.id) return false
if (title != other.title) return false
if (coverUrl != other.coverUrl) return false
if (manga != other.manga) return false
if (counter != other.counter) return false
return progress == other.progress
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + title.hashCode()
result = 31 * result + coverUrl.hashCode()
result = 31 * result + manga.hashCode()
result = 31 * result + counter
result = 31 * result + progress.hashCode()
return result
}
}

@ -1,17 +1,31 @@
package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
sealed interface MangaItemModel : ListModel {
sealed class MangaItemModel : ListModel {
val id: Long
val manga: Manga
val title: String
val coverUrl: String
val counter: Int
val progress: Float
abstract val id: Long
abstract val manga: Manga
abstract val title: String
abstract val coverUrl: String
abstract val counter: Int
abstract val progress: Float
val source: MangaSource
get() = manga.source
override fun areItemsTheSame(other: ListModel): Boolean {
return other is MangaItemModel && other.javaClass == javaClass && id == other.id
}
override fun getChangePayload(previousState: ListModel): Any? {
return when {
previousState !is MangaItemModel -> super.getChangePayload(previousState)
progress != previousState.progress -> MangaListAdapter.PAYLOAD_PROGRESS
counter != previousState.counter -> Unit
else -> null
}
}
}

@ -3,7 +3,7 @@ package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.parsers.model.Manga
data class MangaListDetailedModel(
class MangaListDetailedModel(
override val id: Long,
override val title: String,
val subtitle: String?,
@ -12,4 +12,33 @@ data class MangaListDetailedModel(
override val counter: Int,
override val progress: Float,
val tags: List<ChipsView.ChipModel>,
) : MangaItemModel
) : MangaItemModel() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaListDetailedModel
if (id != other.id) return false
if (title != other.title) return false
if (subtitle != other.subtitle) return false
if (coverUrl != other.coverUrl) return false
if (manga != other.manga) return false
if (counter != other.counter) return false
if (progress != other.progress) return false
return tags == other.tags
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + title.hashCode()
result = 31 * result + (subtitle?.hashCode() ?: 0)
result = 31 * result + coverUrl.hashCode()
result = 31 * result + manga.hashCode()
result = 31 * result + counter
result = 31 * result + progress.hashCode()
result = 31 * result + tags.hashCode()
return result
}
}

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.parsers.model.Manga
data class MangaListModel(
class MangaListModel(
override val id: Long,
override val title: String,
val subtitle: String,
@ -10,4 +10,31 @@ data class MangaListModel(
override val manga: Manga,
override val counter: Int,
override val progress: Float,
) : MangaItemModel
) : MangaItemModel() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaListModel
if (id != other.id) return false
if (title != other.title) return false
if (subtitle != other.subtitle) return false
if (coverUrl != other.coverUrl) return false
if (manga != other.manga) return false
if (counter != other.counter) return false
return progress == other.progress
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + title.hashCode()
result = 31 * result + subtitle.hashCode()
result = 31 * result + coverUrl.hashCode()
result = 31 * result + manga.hashCode()
result = 31 * result + counter
result = 31 * result + progress.hashCode()
return result
}
}

@ -13,6 +13,10 @@ class PageThumbnail(
val number
get() = page.index + 1
override fun areItemsTheSame(other: ListModel): Boolean {
return other is PageThumbnail && page == other.page
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

@ -2,23 +2,22 @@ package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
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.reader.ui.thumbnails.PageThumbnail
class PageThumbnailAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<PageThumbnail>,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()), FastScroller.SectionIndexer {
) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback), FastScroller.SectionIndexer {
init {
delegatesManager.addDelegate(ITEM_TYPE_THUMBNAIL, pageThumbnailAD(coil, lifecycleOwner, clickListener))
@ -37,33 +36,6 @@ class PageThumbnailAdapter(
return null
}
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return when {
oldItem is PageThumbnail && newItem is PageThumbnail -> {
oldItem.page == newItem.page
}
oldItem is ListHeader && newItem is ListHeader -> {
oldItem.textRes == newItem.textRes &&
oldItem.text == newItem.text &&
oldItem.dateTimeAgo == newItem.dateTimeAgo
}
oldItem is LoadingFooter && newItem is LoadingFooter -> {
oldItem.key == newItem.key
}
else -> oldItem.javaClass == newItem.javaClass
}
}
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return oldItem == newItem
}
}
companion object {
const val ITEM_TYPE_THUMBNAIL = 0

@ -10,6 +10,10 @@ class ScrobblerManga(
val url: String,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is ScrobblerManga && other.id == id
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

@ -16,6 +16,10 @@ class ScrobblingInfo(
val externalUrl: String,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is ScrobblingInfo && other.scrobbler == scrobbler
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

@ -4,10 +4,9 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
enum class ScrobblingStatus : ListModel {
PLANNED,
READING,
RE_READING,
COMPLETED,
ON_HOLD,
DROPPED,
PLANNED, READING, RE_READING, COMPLETED, ON_HOLD, DROPPED;
override fun areItemsTheSame(other: ListModel): Boolean {
return other is ScrobblingStatus && other.ordinal == ordinal
}
}

@ -12,6 +12,10 @@ class ScrobblerHint(
@StringRes val actionStringRes: Int,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is ScrobblerHint && other.textPrimary == textPrimary
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@ -22,9 +26,7 @@ class ScrobblerHint(
if (textPrimary != other.textPrimary) return false
if (textSecondary != other.textSecondary) return false
if (error != other.error) return false
if (actionStringRes != other.actionStringRes) return false
return true
return actionStringRes == other.actionStringRes
}
override fun hashCode(): Int {

@ -1,5 +1,7 @@
package org.koitharu.kotatsu.search.ui.multi
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
import org.koitharu.kotatsu.parsers.model.MangaSource
@ -11,6 +13,18 @@ class MultiSearchListModel(
val error: Throwable?,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is MultiSearchListModel && source == other.source
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is MultiSearchListModel && previousState.list != list) {
ListModelDiffCallback.PAYLOAD_NESTED_LIST_CHANGED
} else {
super.getChangePayload(previousState)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.settings.storage
import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import java.io.File
@ -12,6 +13,18 @@ class DirectoryModel(
val isAvailable: Boolean,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is DirectoryModel && other.file == file && other.title == title && other.titleRes == titleRes
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is DirectoryModel && previousState.isChecked != isChecked) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

@ -1,30 +0,0 @@
package org.koitharu.kotatsu.shelf.ui.adapter
import androidx.recyclerview.widget.DiffUtil
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
import kotlin.jvm.internal.Intrinsics
class MangaItemDiffCallback : DiffUtil.ItemCallback<ListModel>() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
oldItem as MangaItemModel
newItem as MangaItemModel
return oldItem.javaClass == newItem.javaClass && oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
oldItem as MangaItemModel
newItem as MangaItemModel
return when {
oldItem.progress != newItem.progress -> MangaListAdapter.PAYLOAD_PROGRESS
oldItem.counter != newItem.counter -> Unit
else -> super.getChangePayload(oldItem, newItem)
}
}
}

@ -10,6 +10,7 @@ import org.koitharu.kotatsu.core.ui.list.NestedScrollStateHandle
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
@ -27,12 +28,11 @@ class ShelfAdapter(
sizeResolver: ItemSizeResolver,
selectionController: SectionedSelectionController<ShelfSectionModel>,
nestedScrollStateHandle: NestedScrollStateHandle,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()), FastScroller.SectionIndexer {
) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback), FastScroller.SectionIndexer {
init {
val pool = RecyclerView.RecycledViewPool()
delegatesManager
.addDelegate(
delegatesManager.addDelegate(
shelfGroupAD(
sharedPool = pool,
lifecycleOwner = lifecycleOwner,
@ -42,44 +42,13 @@ class ShelfAdapter(
listener = listener,
nestedScrollStateHandle = nestedScrollStateHandle,
),
)
.addDelegate(loadingStateAD())
.addDelegate(loadingFooterAD())
).addDelegate(loadingStateAD()).addDelegate(loadingFooterAD())
.addDelegate(emptyHintAD(coil, lifecycleOwner, listener))
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listener))
.addDelegate(errorStateListAD(listener))
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listener)).addDelegate(errorStateListAD(listener))
}
override fun getSectionText(context: Context, position: Int): CharSequence? {
val item = items.getOrNull(position) as? ShelfSectionModel ?: return null
return item.getTitle(context.resources)
}
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return when {
oldItem is ShelfSectionModel && newItem is ShelfSectionModel -> {
oldItem.key == newItem.key
}
oldItem is LoadingFooter && newItem is LoadingFooter -> {
oldItem.key == newItem.key
}
else -> oldItem.javaClass == newItem.javaClass
}
}
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
return when {
oldItem is ShelfSectionModel && newItem is ShelfSectionModel -> Unit
else -> super.getChangePayload(oldItem, newItem)
}
}
}
}

@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.util.ext.removeItemDecoration
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.databinding.ItemListGroupBinding
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
@ -47,7 +48,7 @@ fun shelfGroupAD(
}
val adapter = AsyncListDifferDelegationAdapter(
MangaItemDiffCallback(),
ListModelDiffCallback,
mangaGridItemAD(coil, lifecycleOwner, listenerAdapter, sizeResolver),
)
adapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY

@ -1,41 +1,15 @@
package org.koitharu.kotatsu.shelf.ui.config
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
class ShelfSettingsAdapter(
listener: ShelfSettingsListener,
) : AsyncListDifferDelegationAdapter<ShelfSettingsItemModel>(DiffCallback()) {
) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
init {
delegatesManager.addDelegate(shelfCategoryAD(listener))
.addDelegate(shelfSectionAD(listener))
}
class DiffCallback : DiffUtil.ItemCallback<ShelfSettingsItemModel>() {
override fun areItemsTheSame(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Boolean {
return when {
oldItem is ShelfSettingsItemModel.Section && newItem is ShelfSettingsItemModel.Section -> {
oldItem.section == newItem.section
}
oldItem is ShelfSettingsItemModel.FavouriteCategory && newItem is ShelfSettingsItemModel.FavouriteCategory -> {
oldItem.id == newItem.id
}
else -> false
}
}
override fun areContentsTheSame(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Any? {
return if (oldItem.isChecked == newItem.isChecked) {
super.getChangePayload(oldItem, newItem)
} else Unit
}
}
}

@ -10,13 +10,14 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.setChecked
import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding
import org.koitharu.kotatsu.databinding.ItemShelfSectionDraggableBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.shelf.domain.model.ShelfSection
@SuppressLint("ClickableViewAccessibility")
fun shelfSectionAD(
listener: ShelfSettingsListener,
) =
adapterDelegateViewBinding<ShelfSettingsItemModel.Section, ShelfSettingsItemModel, ItemShelfSectionDraggableBinding>(
adapterDelegateViewBinding<ShelfSettingsItemModel.Section, ListModel, ItemShelfSectionDraggableBinding>(
{ layoutInflater, parent -> ItemShelfSectionDraggableBinding.inflate(layoutInflater, parent, false) },
) {
@ -50,7 +51,7 @@ fun shelfSectionAD(
fun shelfCategoryAD(
listener: ShelfSettingsListener,
) =
adapterDelegateViewBinding<ShelfSettingsItemModel.FavouriteCategory, ShelfSettingsItemModel, ItemCategoryCheckableMultipleBinding>(
adapterDelegateViewBinding<ShelfSettingsItemModel.FavouriteCategory, ListModel, ItemCategoryCheckableMultipleBinding>(
{ layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
) {
itemView.setOnClickListener {

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.shelf.ui.config
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.shelf.domain.model.ShelfSection
@ -12,6 +13,18 @@ sealed interface ShelfSettingsItemModel : ListModel {
override val isChecked: Boolean,
) : ShelfSettingsItemModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Section && section == other.section
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is Section && previousState.isChecked != isChecked) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@ -35,6 +48,10 @@ sealed interface ShelfSettingsItemModel : ListModel {
override val isChecked: Boolean,
) : ShelfSettingsItemModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is FavouriteCategory && other.id == id
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

@ -19,6 +19,18 @@ sealed interface ShelfSectionModel : ListModel {
override fun toString(): String
override fun areItemsTheSame(other: ListModel): Boolean {
return other is ShelfSectionModel && key == other.key
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is ShelfSectionModel) {
Unit
} else {
null
}
}
class History(
override val items: List<MangaItemModel>,
override val showAllButtonText: Int,

@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.daysDiff
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
@ -75,7 +76,7 @@ class FeedViewModel @Inject constructor(
for (item in this) {
val date = timeAgo(item.createdAt)
if (prevDate != date) {
destination += date
destination += ListHeader(date, 0, null)
}
prevDate = date
destination += item.toFeedItem()

@ -2,38 +2,34 @@ package org.koitharu.kotatsu.tracker.ui.feed.adapter
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorFooterAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
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.adapter.relatedDateItemAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
import kotlin.jvm.internal.Intrinsics
class FeedAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: MangaListListener,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()), FastScroller.SectionIndexer {
) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback), FastScroller.SectionIndexer {
init {
delegatesManager
.addDelegate(ITEM_TYPE_FEED, feedItemAD(coil, lifecycleOwner, listener))
delegatesManager.addDelegate(ITEM_TYPE_FEED, feedItemAD(coil, lifecycleOwner, listener))
.addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD())
.addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD())
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
.addDelegate(ITEM_TYPE_DATE_HEADER, relatedDateItemAD())
}
override fun getSectionText(context: Context, position: Int): CharSequence? {
@ -47,29 +43,6 @@ class FeedAdapter(
return null
}
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when {
oldItem is FeedItem && newItem is FeedItem -> {
oldItem.id == newItem.id
}
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
oldItem == newItem
}
oldItem is LoadingFooter && newItem is LoadingFooter -> {
oldItem.key == newItem.key
}
else -> oldItem.javaClass == newItem.javaClass
}
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
}
companion object {
const val ITEM_TYPE_FEED = 0

@ -3,11 +3,40 @@ package org.koitharu.kotatsu.tracker.ui.feed.model
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
data class FeedItem(
class FeedItem(
val id: Long,
val imageUrl: String,
val title: String,
val manga: Manga,
val count: Int,
val isNew: Boolean,
) : ListModel
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is FeedItem && other.id == id
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FeedItem
if (id != other.id) return false
if (imageUrl != other.imageUrl) return false
if (title != other.title) return false
if (manga != other.manga) return false
if (count != other.count) return false
return isNew == other.isNew
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + imageUrl.hashCode()
result = 31 * result + title.hashCode()
result = 31 * result + manga.hashCode()
result = 31 * result + count
result = 31 * result + isNew.hashCode()
return result
}
}

@ -50,7 +50,8 @@
app:layout_constraintTop_toBottomOf="@+id/textView_title"
tools:text="@tools:sample/lorem/random" />
<com.google.android.material.button.MaterialButton
<Button
android:id="@+id/button_more"
style="@style/Widget.Kotatsu.ExploreButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"

Loading…
Cancel
Save