Refactor ListModel

This commit is contained in:
Koitharu
2023-07-03 13:57:51 +03:00
parent 80db817ff2
commit 942d4fe5ab
67 changed files with 671 additions and 715 deletions

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter package org.koitharu.kotatsu.bookmarks.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter 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.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController 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.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import kotlin.jvm.internal.Intrinsics
class BookmarksGroupAdapter( class BookmarksGroupAdapter(
coil: ImageLoader, coil: ImageLoader,
@@ -26,7 +24,7 @@ class BookmarksGroupAdapter(
listener: ListStateHolderListener, listener: ListStateHolderListener,
bookmarkClickListener: OnListItemClickListener<Bookmark>, bookmarkClickListener: OnListItemClickListener<Bookmark>,
groupClickListener: OnListItemClickListener<BookmarksGroup>, groupClickListener: OnListItemClickListener<BookmarksGroup>,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
init { init {
val pool = RecyclerView.RecycledViewPool() val pool = RecyclerView.RecycledViewPool()
@@ -46,32 +44,4 @@ class BookmarksGroupAdapter(
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listener)) .addDelegate(emptyStateListAD(coil, lifecycleOwner, listener))
.addDelegate(errorStateListAD(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)
}
}
}
} }

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.bookmarks.ui.model package org.koitharu.kotatsu.bookmarks.ui.model
import org.koitharu.kotatsu.bookmarks.domain.Bookmark 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.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.areItemsEquals import org.koitharu.kotatsu.parsers.util.areItemsEquals
@@ -10,6 +11,18 @@ class BookmarksGroup(
val bookmarks: List<Bookmark>, val bookmarks: List<Bookmark>,
) : ListModel { ) : 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
@@ -28,4 +41,4 @@ class BookmarksGroup(
result = 31 * result + bookmarks.sumOf { it.imageUrl.hashCode() } result = 31 * result + bookmarks.sumOf { it.imageUrl.hashCode() }
return result return result
} }
} }

View File

@@ -15,7 +15,7 @@ class TrimTransformation(
private val tolerance: Int = 20, private val tolerance: Int = 20,
) : Transformation { ) : Transformation {
override val cacheKey: String = javaClass.name override val cacheKey: String = "${javaClass.name}-$tolerance"
override suspend fun transform(input: Bitmap, size: Size): Bitmap { override suspend fun transform(input: Bitmap, size: Size): Bitmap {
var left = 0 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 { private fun isColorTheSame(@ColorInt a: Int, @ColorInt b: Int): Boolean {
return abs(a.red - b.red) <= tolerance && return abs(a.red - b.red) <= tolerance &&
abs(a.green - b.green) <= tolerance && abs(a.green - b.green) <= tolerance &&
abs(a.blue - b.blue) <= tolerance && abs(a.blue - b.blue) <= tolerance &&
abs(a.alpha - b.alpha) <= 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
}
} }

View File

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

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.details.ui.model 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 import org.koitharu.kotatsu.list.ui.model.ListModel
class MangaBranch( class MangaBranch(
@@ -8,6 +10,18 @@ class MangaBranch(
val isSelected: Boolean, val isSelected: Boolean,
) : ListModel { ) : 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false

View File

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

View File

@@ -2,33 +2,18 @@ package org.koitharu.kotatsu.details.ui.scrobbling
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter 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( class ScrollingInfoAdapter(
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
coil: ImageLoader, coil: ImageLoader,
fragmentManager: FragmentManager, fragmentManager: FragmentManager,
) : AsyncListDifferDelegationAdapter<ScrobblingInfo>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
init { init {
delegatesManager.addDelegate(scrobblingInfoAD(lifecycleOwner, coil, fragmentManager)) 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
}
}
} }

View File

@@ -47,6 +47,24 @@ class DownloadItemModel(
return timestamp.compareTo(other.timestamp) 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false

View File

@@ -1,62 +1,25 @@
package org.koitharu.kotatsu.download.ui.list package org.koitharu.kotatsu.download.ui.list
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter 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.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD 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.ListModel
import kotlin.jvm.internal.Intrinsics
class DownloadsAdapter( class DownloadsAdapter(
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
coil: ImageLoader, coil: ImageLoader,
listener: DownloadItemListener, listener: DownloadItemListener,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
init { init {
delegatesManager.addDelegate(ITEM_TYPE_DOWNLOAD, downloadItemAD(lifecycleOwner, coil, listener)) delegatesManager.addDelegate(ITEM_TYPE_DOWNLOAD, downloadItemAD(lifecycleOwner, coil, listener))
.addDelegate(loadingStateAD()) .addDelegate(loadingStateAD())
.addDelegate(emptyStateListAD(coil, lifecycleOwner, null)) .addDelegate(emptyStateListAD(coil, lifecycleOwner, null))
.addDelegate(relatedDateItemAD()) .addDelegate(listHeaderAD(null))
}
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)
}
}
} }
companion object { companion object {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,8 @@
package org.koitharu.kotatsu.explore.ui.adapter package org.koitharu.kotatsu.explore.ui.adapter
import android.view.View import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.favicon.faviconUri 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.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest 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.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
import org.koitharu.kotatsu.databinding.ItemExploreSourceGridBinding import org.koitharu.kotatsu.databinding.ItemExploreSourceGridBinding
import org.koitharu.kotatsu.databinding.ItemExploreSourceListBinding import org.koitharu.kotatsu.databinding.ItemExploreSourceListBinding
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
import org.koitharu.kotatsu.databinding.ItemRecommendationBinding import org.koitharu.kotatsu.databinding.ItemRecommendationBinding
import org.koitharu.kotatsu.explore.ui.model.ExploreItem import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener 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( fun exploreButtonsAD(
clickListener: View.OnClickListener, clickListener: View.OnClickListener,
) = adapterDelegateViewBinding<ExploreItem.Buttons, ExploreItem, ItemExploreButtonsBinding>( ) = adapterDelegateViewBinding<ExploreButtons, ListModel, ItemExploreButtonsBinding>(
{ layoutInflater, parent -> ItemExploreButtonsBinding.inflate(layoutInflater, parent, false) }, { 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( fun exploreRecommendationItemAD(
coil: ImageLoader, coil: ImageLoader,
clickListener: View.OnClickListener, clickListener: View.OnClickListener,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<ExploreItem.Recommendation, ExploreItem, ItemRecommendationBinding>( ) = adapterDelegateViewBinding<RecommendationsItem, ListModel, ItemRecommendationBinding>(
{ layoutInflater, parent -> ItemRecommendationBinding.inflate(layoutInflater, parent, false) } { 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( fun exploreSourceListItemAD(
coil: ImageLoader, coil: ImageLoader,
listener: OnListItemClickListener<ExploreItem.Source>, listener: OnListItemClickListener<MangaSourceItem>,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<ExploreItem.Source, ExploreItem, ItemExploreSourceListBinding>( ) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceListBinding>(
{ layoutInflater, parent -> ItemExploreSourceListBinding.inflate(layoutInflater, parent, false) }, { 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) val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
@@ -128,11 +97,11 @@ fun exploreSourceListItemAD(
fun exploreSourceGridItemAD( fun exploreSourceGridItemAD(
coil: ImageLoader, coil: ImageLoader,
listener: OnListItemClickListener<ExploreItem.Source>, listener: OnListItemClickListener<MangaSourceItem>,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<ExploreItem.Source, ExploreItem, ItemExploreSourceGridBinding>( ) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceGridBinding>(
{ layoutInflater, parent -> ItemExploreSourceGridBinding.inflate(layoutInflater, parent, false) }, { 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) val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
@@ -156,21 +125,3 @@ fun exploreSourceGridItemAD(
binding.imageViewIcon.disposeImageRequest() 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) {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,61 +1,25 @@
package org.koitharu.kotatsu.favourites.ui.categories.adapter package org.koitharu.kotatsu.favourites.ui.categories.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener 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.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import kotlin.jvm.internal.Intrinsics
class CategoriesAdapter( class CategoriesAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
onItemClickListener: FavouriteCategoriesListListener, onItemClickListener: FavouriteCategoriesListListener,
listListener: ListStateHolderListener, listListener: ListStateHolderListener,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
init { init {
delegatesManager.addDelegate(categoryAD(coil, lifecycleOwner, onItemClickListener)) delegatesManager.addDelegate(categoryAD(coil, lifecycleOwner, onItemClickListener))
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listListener)) .addDelegate(emptyStateListAD(coil, lifecycleOwner, listListener))
.addDelegate(loadingStateAD()) .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)
}
}
}
} }

View File

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

View File

@@ -4,5 +4,16 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
class CategoriesHeaderItem : 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()
}
} }

View File

@@ -1,9 +1,41 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.model package org.koitharu.kotatsu.favourites.ui.categories.select.model
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
data class MangaCategoryItem( class MangaCategoryItem(
val id: Long, val id: Long,
val name: String, val name: String,
val isChecked: Boolean, 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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.list.ui.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding 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.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -23,12 +22,3 @@ fun listHeaderAD(
binding.buttonMore.setTextAndVisible(item.buttonTextRes) 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)
}
}

View File

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

View File

@@ -1,25 +1,16 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel 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( open class MangaListAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
listener: MangaListListener, listener: MangaListListener,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
init { init {
delegatesManager delegatesManager
@@ -28,64 +19,10 @@ open class MangaListAdapter(
.addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, listener, null)) .addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, listener, null))
.addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD()) .addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD())
.addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD()) .addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD())
.addDelegate(ITEM_TYPE_DATE, relatedDateItemAD())
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener)) .addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener)) .addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener)) .addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD(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 { companion object {
@@ -95,12 +32,10 @@ open class MangaListAdapter(
const val ITEM_TYPE_MANGA_GRID = 2 const val ITEM_TYPE_MANGA_GRID = 2
const val ITEM_TYPE_LOADING_FOOTER = 3 const val ITEM_TYPE_LOADING_FOOTER = 3
const val ITEM_TYPE_LOADING_STATE = 4 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_STATE = 6
const val ITEM_TYPE_ERROR_FOOTER = 7 const val ITEM_TYPE_ERROR_FOOTER = 7
const val ITEM_TYPE_EMPTY = 8 const val ITEM_TYPE_EMPTY = 8
const val ITEM_TYPE_HEADER = 9 const val ITEM_TYPE_HEADER = 9
const val ITEM_TYPE_HEADER_2 = 10
val PAYLOAD_PROGRESS = Any() val PAYLOAD_PROGRESS = Any()
} }

View File

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

View File

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

View File

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

View File

@@ -4,11 +4,35 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
class EmptyHint( class EmptyHint(
@DrawableRes icon: Int, @DrawableRes val icon: Int,
@StringRes textPrimary: Int, @StringRes val textPrimary: Int,
@StringRes textSecondary: Int, @StringRes val textSecondary: Int,
@StringRes actionStringRes: Int, @StringRes val actionStringRes: Int,
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes) { ) : ListModel {
fun toState() = EmptyState(icon, textPrimary, textSecondary, actionStringRes) 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
}
} }

View File

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

View File

@@ -2,7 +2,28 @@ package org.koitharu.kotatsu.list.ui.model
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
data class ErrorFooter( class ErrorFooter(
val exception: Throwable, val exception: Throwable,
@DrawableRes val icon: Int @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
}
}

View File

@@ -3,9 +3,32 @@ package org.koitharu.kotatsu.list.ui.model
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
data class ErrorState( class ErrorState(
val exception: Throwable, val exception: Throwable,
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
val canRetry: Boolean, val canRetry: Boolean,
@StringRes val buttonText: Int @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
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,38 @@
package org.koitharu.kotatsu.list.ui.model 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.Manga
data class MangaGridModel( class MangaGridModel(
override val id: Long, override val id: Long,
override val title: String, override val title: String,
override val coverUrl: String, override val coverUrl: String,
override val manga: Manga, override val manga: Manga,
override val counter: Int, override val counter: Int,
override val progress: Float, 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
}
}

View File

@@ -1,17 +1,31 @@
package org.koitharu.kotatsu.list.ui.model 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
sealed interface MangaItemModel : ListModel { sealed class MangaItemModel : ListModel {
val id: Long abstract val id: Long
val manga: Manga abstract val manga: Manga
val title: String abstract val title: String
val coverUrl: String abstract val coverUrl: String
val counter: Int abstract val counter: Int
val progress: Float abstract val progress: Float
val source: MangaSource val source: MangaSource
get() = manga.source 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
}
}
} }

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
data class MangaListDetailedModel( class MangaListDetailedModel(
override val id: Long, override val id: Long,
override val title: String, override val title: String,
val subtitle: String?, val subtitle: String?,
@@ -12,4 +12,33 @@ data class MangaListDetailedModel(
override val counter: Int, override val counter: Int,
override val progress: Float, override val progress: Float,
val tags: List<ChipsView.ChipModel>, 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
}
}

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
data class MangaListModel( class MangaListModel(
override val id: Long, override val id: Long,
override val title: String, override val title: String,
val subtitle: String, val subtitle: String,
@@ -10,4 +10,31 @@ data class MangaListModel(
override val manga: Manga, override val manga: Manga,
override val counter: Int, override val counter: Int,
override val progress: Float, 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
}
}

View File

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

View File

@@ -2,23 +2,22 @@ package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
import android.content.Context import android.content.Context
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller 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.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
class PageThumbnailAdapter( class PageThumbnailAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<PageThumbnail>, clickListener: OnListItemClickListener<PageThumbnail>,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()), FastScroller.SectionIndexer { ) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback), FastScroller.SectionIndexer {
init { init {
delegatesManager.addDelegate(ITEM_TYPE_THUMBNAIL, pageThumbnailAD(coil, lifecycleOwner, clickListener)) delegatesManager.addDelegate(ITEM_TYPE_THUMBNAIL, pageThumbnailAD(coil, lifecycleOwner, clickListener))
@@ -37,33 +36,6 @@ class PageThumbnailAdapter(
return null 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 { companion object {
const val ITEM_TYPE_THUMBNAIL = 0 const val ITEM_TYPE_THUMBNAIL = 0

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.search.ui.multi 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.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel import org.koitharu.kotatsu.list.ui.model.MangaItemModel
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -11,6 +13,18 @@ class MultiSearchListModel(
val error: Throwable?, val error: Throwable?,
) : ListModel { ) : 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.settings.storage package org.koitharu.kotatsu.settings.storage
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import java.io.File import java.io.File
@@ -12,6 +13,18 @@ class DirectoryModel(
val isAvailable: Boolean, val isAvailable: Boolean,
) : ListModel { ) : 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false

View File

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

View File

@@ -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.SectionedSelectionController
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.ItemSizeResolver 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.emptyHintAD
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
@@ -27,12 +28,11 @@ class ShelfAdapter(
sizeResolver: ItemSizeResolver, sizeResolver: ItemSizeResolver,
selectionController: SectionedSelectionController<ShelfSectionModel>, selectionController: SectionedSelectionController<ShelfSectionModel>,
nestedScrollStateHandle: NestedScrollStateHandle, nestedScrollStateHandle: NestedScrollStateHandle,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()), FastScroller.SectionIndexer { ) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback), FastScroller.SectionIndexer {
init { init {
val pool = RecyclerView.RecycledViewPool() val pool = RecyclerView.RecycledViewPool()
delegatesManager delegatesManager.addDelegate(
.addDelegate(
shelfGroupAD( shelfGroupAD(
sharedPool = pool, sharedPool = pool,
lifecycleOwner = lifecycleOwner, lifecycleOwner = lifecycleOwner,
@@ -42,44 +42,13 @@ class ShelfAdapter(
listener = listener, listener = listener,
nestedScrollStateHandle = nestedScrollStateHandle, nestedScrollStateHandle = nestedScrollStateHandle,
), ),
) ).addDelegate(loadingStateAD()).addDelegate(loadingFooterAD())
.addDelegate(loadingStateAD())
.addDelegate(loadingFooterAD())
.addDelegate(emptyHintAD(coil, lifecycleOwner, listener)) .addDelegate(emptyHintAD(coil, lifecycleOwner, listener))
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listener)) .addDelegate(emptyStateListAD(coil, lifecycleOwner, listener)).addDelegate(errorStateListAD(listener))
.addDelegate(errorStateListAD(listener))
} }
override fun getSectionText(context: Context, position: Int): CharSequence? { override fun getSectionText(context: Context, position: Int): CharSequence? {
val item = items.getOrNull(position) as? ShelfSectionModel ?: return null val item = items.getOrNull(position) as? ShelfSectionModel ?: return null
return item.getTitle(context.resources) 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)
}
}
}
} }

View File

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

View File

@@ -1,41 +1,15 @@
package org.koitharu.kotatsu.shelf.ui.config package org.koitharu.kotatsu.shelf.ui.config
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
class ShelfSettingsAdapter( class ShelfSettingsAdapter(
listener: ShelfSettingsListener, listener: ShelfSettingsListener,
) : AsyncListDifferDelegationAdapter<ShelfSettingsItemModel>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
init { init {
delegatesManager.addDelegate(shelfCategoryAD(listener)) delegatesManager.addDelegate(shelfCategoryAD(listener))
.addDelegate(shelfSectionAD(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
}
}
} }

View File

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

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.shelf.ui.config 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.list.ui.model.ListModel
import org.koitharu.kotatsu.shelf.domain.model.ShelfSection import org.koitharu.kotatsu.shelf.domain.model.ShelfSection
@@ -12,6 +13,18 @@ sealed interface ShelfSettingsItemModel : ListModel {
override val isChecked: Boolean, override val isChecked: Boolean,
) : ShelfSettingsItemModel { ) : 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
@@ -35,6 +48,10 @@ sealed interface ShelfSettingsItemModel : ListModel {
override val isChecked: Boolean, override val isChecked: Boolean,
) : ShelfSettingsItemModel { ) : ShelfSettingsItemModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is FavouriteCategory && other.id == id
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false

View File

@@ -19,6 +19,18 @@ sealed interface ShelfSectionModel : ListModel {
override fun toString(): String 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( class History(
override val items: List<MangaItemModel>, override val items: List<MangaItemModel>,
override val showAllButtonText: Int, override val showAllButtonText: Int,

View File

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

View File

@@ -2,38 +2,34 @@ package org.koitharu.kotatsu.tracker.ui.feed.adapter
import android.content.Context import android.content.Context
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo 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.MangaListListener
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorFooterAD import org.koitharu.kotatsu.list.ui.adapter.errorFooterAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD 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.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD 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.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( class FeedAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
listener: MangaListListener, listener: MangaListListener,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()), FastScroller.SectionIndexer { ) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback), FastScroller.SectionIndexer {
init { init {
delegatesManager delegatesManager.addDelegate(ITEM_TYPE_FEED, feedItemAD(coil, lifecycleOwner, listener))
.addDelegate(ITEM_TYPE_FEED, feedItemAD(coil, lifecycleOwner, listener))
.addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD()) .addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD())
.addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD()) .addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD())
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener)) .addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(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_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
.addDelegate(ITEM_TYPE_DATE_HEADER, relatedDateItemAD())
} }
override fun getSectionText(context: Context, position: Int): CharSequence? { override fun getSectionText(context: Context, position: Int): CharSequence? {
@@ -47,29 +43,6 @@ class FeedAdapter(
return null 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 { companion object {
const val ITEM_TYPE_FEED = 0 const val ITEM_TYPE_FEED = 0

View File

@@ -3,11 +3,40 @@ package org.koitharu.kotatsu.tracker.ui.feed.model
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
data class FeedItem( class FeedItem(
val id: Long, val id: Long,
val imageUrl: String, val imageUrl: String,
val title: String, val title: String,
val manga: Manga, val manga: Manga,
val count: Int, val count: Int,
val isNew: Boolean, 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
}
}

View File

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