Multiple selection in library

This commit is contained in:
Koitharu
2022-07-04 18:18:08 +03:00
parent b81aeaebd3
commit f42f244443
13 changed files with 361 additions and 102 deletions

View File

@@ -27,11 +27,14 @@ class ListSelectionController(
) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider { ) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider {
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private val stateEventObserver = StateEventObserver()
val count: Int val count: Int
get() = decoration.checkedItemsCount get() = decoration.checkedItemsCount
init {
registryOwner.lifecycle.addObserver(StateEventObserver())
}
fun snapshot(): Set<Long> { fun snapshot(): Set<Long> {
return peekCheckedIds().toSet() return peekCheckedIds().toSet()
} }
@@ -55,7 +58,6 @@ class ListSelectionController(
fun attachToRecyclerView(recyclerView: RecyclerView) { fun attachToRecyclerView(recyclerView: RecyclerView) {
recyclerView.addItemDecoration(decoration) recyclerView.addItemDecoration(decoration)
registryOwner.lifecycle.addObserver(stateEventObserver)
} }
override fun saveState(): Bundle { override fun saveState(): Bundle {

View File

@@ -0,0 +1,186 @@
package org.koitharu.kotatsu.base.ui.list
import android.app.Activity
import android.os.Bundle
import android.util.ArrayMap
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryOwner
import kotlinx.coroutines.Dispatchers
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
import kotlin.coroutines.EmptyCoroutineContext
private const val PROVIDER_NAME = "selection_decoration_sectioned"
class SectionedSelectionController<T : Any>(
private val activity: Activity,
private val registryOwner: SavedStateRegistryOwner,
private val callback: Callback<T>,
) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider {
private var actionMode: ActionMode? = null
private var pendingData: MutableMap<String, Collection<Long>>? = null
private val decorations = ArrayMap<T, AbstractSelectionItemDecoration>()
val count: Int
get() = decorations.values.sumOf { it.checkedItemsCount }
init {
registryOwner.lifecycle.addObserver(StateEventObserver())
}
fun snapshot(): Map<T, Set<Long>> {
return decorations.mapValues { it.value.checkedItemsIds.toSet() }
}
fun peekCheckedIds(): Map<T, Set<Long>> {
return decorations.mapValues { it.value.checkedItemsIds }
}
fun clear() {
decorations.values.forEach {
it.clearSelection()
}
notifySelectionChanged()
}
fun attachToRecyclerView(section: T, recyclerView: RecyclerView) {
val decoration = getDecoration(section)
val pendingIds = pendingData?.remove(section.toString())
if (!pendingIds.isNullOrEmpty()) {
decoration.checkAll(pendingIds)
startActionMode()
notifySelectionChanged()
}
recyclerView.addItemDecoration(decoration)
if (pendingData?.isEmpty() == true) {
pendingData = null
}
}
override fun saveState(): Bundle {
val bundle = Bundle(decorations.size)
for ((k, v) in decorations) {
bundle.putLongArray(k.toString(), v.checkedItemsIds.toLongArray())
}
return bundle
}
fun onItemClick(section: T, id: Long): Boolean {
val decoration = getDecoration(section)
if (isInSelectionMode()) {
decoration.toggleItemChecked(id)
if (isInSelectionMode()) {
actionMode?.invalidate()
} else {
actionMode?.finish()
}
notifySelectionChanged()
return true
}
return false
}
fun onItemLongClick(section: T, id: Long): Boolean {
val decoration = getDecoration(section)
startActionMode()
return actionMode?.also {
decoration.setItemIsChecked(id, true)
notifySelectionChanged()
} != null
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
return callback.onCreateActionMode(mode, menu)
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
return callback.onPrepareActionMode(mode, menu)
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return callback.onActionItemClicked(mode, item)
}
override fun onDestroyActionMode(mode: ActionMode) {
callback.onDestroyActionMode(mode)
clear()
actionMode = null
}
private fun startActionMode() {
if (actionMode == null) {
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
}
}
private fun isInSelectionMode(): Boolean {
return decorations.values.any { x -> x.checkedItemsCount > 0 }
}
private fun notifySelectionChanged() {
val count = this.count
callback.onSelectionChanged(count)
if (count == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
}
}
private fun restoreState(ids: MutableMap<String, Collection<Long>>) {
if (ids.isEmpty() || isInSelectionMode()) {
return
}
for ((k, v) in decorations) {
val items = ids.remove(k.toString())
if (!items.isNullOrEmpty()) {
v.checkAll(items)
}
}
pendingData = ids
if (isInSelectionMode()) {
startActionMode()
notifySelectionChanged()
}
}
private fun getDecoration(section: T): AbstractSelectionItemDecoration {
return decorations.getOrPut(section) {
callback.onCreateItemDecoration(section)
}
}
interface Callback<T> : ListSelectionController.Callback {
fun onCreateItemDecoration(section: T): AbstractSelectionItemDecoration
}
private inner class StateEventObserver : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_CREATE) {
val registry = registryOwner.savedStateRegistry
registry.registerSavedStateProvider(PROVIDER_NAME, this@SectionedSelectionController)
val state = registry.consumeRestoredStateForKey(PROVIDER_NAME)
if (state != null) {
Dispatchers.Main.dispatch(EmptyCoroutineContext) { // == Handler.post
if (source.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
restoreState(
state.keySet().associateWithTo(HashMap()) { state.getLongArray(it)?.toList().orEmpty() }
)
}
}
}
}
}
}
}

View File

@@ -15,6 +15,8 @@ sealed class DateTimeAgo : ListModel {
override fun format(resources: Resources): String { override fun format(resources: Resources): String {
return resources.getString(R.string.just_now) return resources.getString(R.string.just_now)
} }
override fun toString() = "just_now"
} }
class MinutesAgo(val minutes: Int) : DateTimeAgo() { class MinutesAgo(val minutes: Int) : DateTimeAgo() {
@@ -31,6 +33,8 @@ sealed class DateTimeAgo : ListModel {
} }
override fun hashCode(): Int = minutes override fun hashCode(): Int = minutes
override fun toString() = "minutes_ago_$minutes"
} }
class HoursAgo(val hours: Int) : DateTimeAgo() { class HoursAgo(val hours: Int) : DateTimeAgo() {
@@ -46,18 +50,24 @@ sealed class DateTimeAgo : ListModel {
} }
override fun hashCode(): Int = hours override fun hashCode(): Int = hours
override fun toString() = "hours_ago_$hours"
} }
object Today : DateTimeAgo() { object Today : DateTimeAgo() {
override fun format(resources: Resources): String { override fun format(resources: Resources): String {
return resources.getString(R.string.today) return resources.getString(R.string.today)
} }
override fun toString() = "today"
} }
object Yesterday : DateTimeAgo() { object Yesterday : DateTimeAgo() {
override fun format(resources: Resources): String { override fun format(resources: Resources): String {
return resources.getString(R.string.yesterday) return resources.getString(R.string.yesterday)
} }
override fun toString() = "yesterday"
} }
class DaysAgo(val days: Int) : DateTimeAgo() { class DaysAgo(val days: Int) : DateTimeAgo() {
@@ -73,6 +83,8 @@ sealed class DateTimeAgo : ListModel {
} }
override fun hashCode(): Int = days override fun hashCode(): Int = days
override fun toString() = "days_ago_$days"
} }
class Absolute(private val date: Date) : DateTimeAgo() { class Absolute(private val date: Date) : DateTimeAgo() {
@@ -97,11 +109,15 @@ sealed class DateTimeAgo : ListModel {
override fun hashCode(): Int { override fun hashCode(): Int {
return day return day
} }
override fun toString() = "abs_$day"
} }
object LongAgo : DateTimeAgo() { object LongAgo : DateTimeAgo() {
override fun format(resources: Resources): String { override fun format(resources: Resources): String {
return resources.getString(R.string.long_ago) return resources.getString(R.string.long_ago)
} }
override fun toString() = "long_ago"
} }
} }

View File

@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.library.ui
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
@@ -12,30 +11,31 @@ import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.databinding.FragmentLibraryBinding import org.koitharu.kotatsu.databinding.FragmentLibraryBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
import org.koitharu.kotatsu.history.ui.HistoryActivity import org.koitharu.kotatsu.history.ui.HistoryActivity
import org.koitharu.kotatsu.library.ui.adapter.LibraryAdapter import org.koitharu.kotatsu.library.ui.adapter.LibraryAdapter
import org.koitharu.kotatsu.library.ui.model.LibraryGroupModel import org.koitharu.kotatsu.library.ui.adapter.LibraryListEventListener
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.ItemSizeResolver
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.util.flattenTo
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.findViewsByType import org.koitharu.kotatsu.utils.ext.findViewsByType
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), MangaListListener, ActionMode.Callback { class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), LibraryListEventListener,
SectionedSelectionController.Callback<LibrarySectionModel> {
private val viewModel by viewModel<LibraryViewModel>() private val viewModel by viewModel<LibraryViewModel>()
private var adapter: LibraryAdapter? = null private var adapter: LibraryAdapter? = null
private var selectionDecoration: MangaSelectionDecoration? = null private var selectionController: SectionedSelectionController<LibrarySectionModel>? = null
private var actionMode: ActionMode? = null
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentLibraryBinding { override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentLibraryBinding {
return FragmentLibraryBinding.inflate(inflater, container, false) return FragmentLibraryBinding.inflate(inflater, container, false)
@@ -44,19 +44,17 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), MangaListListene
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val sizeResolver = ItemSizeResolver(resources, get()) val sizeResolver = ItemSizeResolver(resources, get())
val itemCLickListener = object : OnListItemClickListener<LibraryGroupModel> { selectionController = SectionedSelectionController(
override fun onItemClick(item: LibraryGroupModel, view: View) { activity = requireActivity(),
onGroupClick(item, view) registryOwner = this,
} callback = this,
} )
selectionDecoration = MangaSelectionDecoration(view.context)
adapter = LibraryAdapter( adapter = LibraryAdapter(
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
coil = get(), coil = get(),
listener = this, listener = this,
itemClickListener = itemCLickListener,
sizeResolver = sizeResolver, sizeResolver = sizeResolver,
selectionDecoration = checkNotNull(selectionDecoration), selectionController = checkNotNull(selectionController),
) )
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
@@ -68,42 +66,30 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), MangaListListene
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
adapter = null adapter = null
selectionDecoration = null selectionController = null
actionMode = null
} }
override fun onItemClick(item: Manga, view: View) { override fun onItemClick(item: Manga, section: LibrarySectionModel, view: View) {
if (selectionDecoration?.checkedItemsCount != 0) { if (selectionController?.onItemClick(section, item.id) != true) {
selectionDecoration?.toggleItemChecked(item.id) val intent = DetailsActivity.newIntent(view.context, item)
if (selectionDecoration?.checkedItemsCount == 0) { startActivity(intent)
actionMode?.finish() }
} else { }
actionMode?.invalidate()
invalidateItemDecorations() override fun onItemLongClick(item: Manga, section: LibrarySectionModel, view: View): Boolean {
} return selectionController?.onItemLongClick(section, item.id) ?: false
return }
override fun onSectionClick(section: LibrarySectionModel, view: View) {
val intent = when (section) {
is LibrarySectionModel.History -> HistoryActivity.newIntent(view.context)
is LibrarySectionModel.Favourites -> TODO()
} }
val intent = DetailsActivity.newIntent(view.context, item)
startActivity(intent) startActivity(intent)
} }
override fun onItemLongClick(item: Manga, view: View): Boolean {
if (actionMode == null) {
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
}
return actionMode?.also {
selectionDecoration?.setItemIsChecked(item.id, true)
invalidateItemDecorations()
it.invalidate()
} != null
}
override fun onRetryClick(error: Throwable) = Unit override fun onRetryClick(error: Throwable) = Unit
override fun onTagRemoveClick(tag: MangaTag) = Unit
override fun onFilterClick() = Unit
override fun onEmptyActionClick() = Unit override fun onEmptyActionClick() = Unit
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
@@ -121,7 +107,7 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), MangaListListene
} }
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.title = selectionDecoration?.checkedItemsCount?.toString() mode.title = selectionController?.count?.toString()
return true return true
} }
@@ -147,26 +133,28 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), MangaListListene
} }
} }
override fun onDestroyActionMode(mode: ActionMode) { override fun onSelectionChanged(count: Int) {
selectionDecoration?.clearSelection()
invalidateItemDecorations() invalidateItemDecorations()
actionMode = null
} }
private fun onGroupClick(item: LibraryGroupModel, view: View) { override fun onCreateItemDecoration(section: LibrarySectionModel): AbstractSelectionItemDecoration {
val intent = when (item) { return MangaSelectionDecoration(requireContext())
is LibraryGroupModel.History -> HistoryActivity.newIntent(view.context) }
is LibraryGroupModel.Favourites -> TODO()
private fun collectSelectedItemsMap(): Map<LibrarySectionModel, Set<Manga>> {
val snapshot = selectionController?.snapshot()
if (snapshot.isNullOrEmpty()) {
return emptyMap()
} }
startActivity(intent) return snapshot.mapValues { (_, ids) -> viewModel.getManga(ids) }
} }
private fun collectSelectedItems(): Set<Manga> { private fun collectSelectedItems(): Set<Manga> {
val ids = selectionDecoration?.checkedItemsIds val snapshot = selectionController?.snapshot()
if (ids.isNullOrEmpty()) { if (snapshot.isNullOrEmpty()) {
return emptySet() return emptySet()
} }
return emptySet()//viewModel.getItems(ids) return viewModel.getManga(snapshot.values.flattenTo(HashSet()))
} }
private fun invalidateItemDecorations() { private fun invalidateItemDecorations() {

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.library.ui package org.koitharu.kotatsu.library.ui
import androidx.collection.ArraySet
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -16,7 +17,7 @@ import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.MangaWithHistory import org.koitharu.kotatsu.history.domain.MangaWithHistory
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.library.ui.model.LibraryGroupModel import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@@ -57,6 +58,25 @@ class LibraryViewModel(
} }
} }
fun getManga(ids: Set<Long>): Set<Manga> {
val snapshot = content.value ?: return emptySet()
val result = ArraySet<Manga>(ids.size)
for (section in snapshot) {
if (section !is LibrarySectionModel) {
continue
}
for (item in section.items) {
if (item.id in ids) {
result.add(item.manga)
if (result.size == ids.size) {
return result
}
}
}
}
return result
}
private suspend fun mapList( private suspend fun mapList(
history: List<MangaWithHistory>, history: List<MangaWithHistory>,
favourites: Map<FavouriteCategory, List<Manga>>, favourites: Map<FavouriteCategory, List<Manga>>,
@@ -66,12 +86,12 @@ class LibraryViewModel(
result += mapHistory(history) result += mapHistory(history)
} }
for ((category, list) in favourites) { for ((category, list) in favourites) {
result += LibraryGroupModel.Favourites(list.toUi(ListMode.GRID, this), category, R.string.show_all) result += LibrarySectionModel.Favourites(list.toUi(ListMode.GRID, this), category, R.string.show_all)
} }
return result return result
} }
private suspend fun mapHistory(list: List<MangaWithHistory>): List<LibraryGroupModel.History> { private suspend fun mapHistory(list: List<MangaWithHistory>): List<LibrarySectionModel.History> {
val showPercent = settings.isReadingIndicatorsEnabled val showPercent = settings.isReadingIndicatorsEnabled
val groups = ArrayList<DateTimeAgo>() val groups = ArrayList<DateTimeAgo>()
val map = HashMap<DateTimeAgo, ArrayList<MangaItemModel>>() val map = HashMap<DateTimeAgo, ArrayList<MangaItemModel>>()
@@ -84,12 +104,12 @@ class LibraryViewModel(
} }
map.getOrPut(date) { ArrayList() }.add(manga.toGridModel(counter, percent)) map.getOrPut(date) { ArrayList() }.add(manga.toGridModel(counter, percent))
} }
val result = ArrayList<LibraryGroupModel.History>(HISTORY_MAX_SEGMENTS) val result = ArrayList<LibrarySectionModel.History>(HISTORY_MAX_SEGMENTS)
repeat(minOf(HISTORY_MAX_SEGMENTS - 1, groups.size - 1)) { i -> repeat(minOf(HISTORY_MAX_SEGMENTS - 1, groups.size - 1)) { i ->
val key = groups[i] val key = groups[i]
val values = map.remove(key) val values = map.remove(key)
if (!values.isNullOrEmpty()) { if (!values.isNullOrEmpty()) {
result.add(LibraryGroupModel.History(values, key, 0)) result.add(LibrarySectionModel.History(values, key, 0))
} }
} }
val values = map.values.flatten() val values = map.values.flatten()
@@ -99,7 +119,7 @@ class LibraryViewModel(
} else { } else {
map.keys.singleOrNull() ?: DateTimeAgo.LongAgo map.keys.singleOrNull() ?: DateTimeAgo.LongAgo
} }
result.add(LibraryGroupModel.History(values, key, R.string.show_all)) result.add(LibrarySectionModel.History(values, key, R.string.show_all))
} }
return result return result
} }

View File

@@ -5,21 +5,22 @@ 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
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.library.ui.model.LibraryGroupModel import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.ItemSizeResolver
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.* import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import kotlin.jvm.internal.Intrinsics import kotlin.jvm.internal.Intrinsics
class LibraryAdapter( class LibraryAdapter(
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
coil: ImageLoader, coil: ImageLoader,
listener: MangaListListener, listener: LibraryListEventListener,
sizeResolver: ItemSizeResolver, sizeResolver: ItemSizeResolver,
selectionDecoration: MangaSelectionDecoration, selectionController: SectionedSelectionController<LibrarySectionModel>,
itemClickListener: OnListItemClickListener<LibraryGroupModel>,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
init { init {
@@ -31,9 +32,8 @@ class LibraryAdapter(
lifecycleOwner = lifecycleOwner, lifecycleOwner = lifecycleOwner,
coil = coil, coil = coil,
sizeResolver = sizeResolver, sizeResolver = sizeResolver,
selectionDecoration = selectionDecoration, selectionController = selectionController,
listener = listener, listener = listener,
itemClickListener = itemClickListener,
) )
) )
.addDelegate(loadingStateAD()) .addDelegate(loadingStateAD())
@@ -46,7 +46,7 @@ class LibraryAdapter(
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return when { return when {
oldItem is LibraryGroupModel && newItem is LibraryGroupModel -> { oldItem is LibrarySectionModel && newItem is LibrarySectionModel -> {
oldItem.key == newItem.key oldItem.key == newItem.key
} }
else -> oldItem.javaClass == newItem.javaClass else -> oldItem.javaClass == newItem.javaClass
@@ -59,7 +59,7 @@ class LibraryAdapter(
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? { override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
return when { return when {
oldItem is LibraryGroupModel && newItem is LibraryGroupModel -> Unit oldItem is LibrarySectionModel && newItem is LibrarySectionModel -> Unit
else -> super.getChangePayload(oldItem, newItem) else -> super.getChangePayload(oldItem, newItem)
} }
} }

View File

@@ -1,21 +1,22 @@
package org.koitharu.kotatsu.library.ui.adapter package org.koitharu.kotatsu.library.ui.adapter
import android.view.View
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
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
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.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.databinding.ItemListGroupBinding import org.koitharu.kotatsu.databinding.ItemListGroupBinding
import org.koitharu.kotatsu.library.ui.model.LibraryGroupModel import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.ItemSizeResolver
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
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
import org.koitharu.kotatsu.utils.ext.clearItemDecorations
import org.koitharu.kotatsu.utils.ext.setTextAndVisible import org.koitharu.kotatsu.utils.ext.setTextAndVisible
fun libraryGroupAD( fun libraryGroupAD(
@@ -23,26 +24,42 @@ fun libraryGroupAD(
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
coil: ImageLoader, coil: ImageLoader,
sizeResolver: ItemSizeResolver, sizeResolver: ItemSizeResolver,
selectionDecoration: MangaSelectionDecoration, selectionController: SectionedSelectionController<LibrarySectionModel>,
listener: OnListItemClickListener<Manga>, listener: LibraryListEventListener,
itemClickListener: OnListItemClickListener<LibraryGroupModel>, ) = adapterDelegateViewBinding<LibrarySectionModel, ListModel, ItemListGroupBinding>(
) = adapterDelegateViewBinding<LibraryGroupModel, ListModel, ItemListGroupBinding>(
{ layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) } { layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) }
) { ) {
binding.recyclerView.setRecycledViewPool(sharedPool) val listenerAdapter = object : OnListItemClickListener<Manga>, View.OnClickListener {
val adapter = AsyncListDifferDelegationAdapter<ListModel>( override fun onItemClick(item: Manga, view: View) {
MangaItemDiffCallback(), listener.onItemClick(item, this@adapterDelegateViewBinding.item, view)
mangaGridItemAD(coil, lifecycleOwner, listener, sizeResolver) }
)
binding.recyclerView.addItemDecoration(selectionDecoration)
binding.recyclerView.adapter = adapter
val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing)
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
val eventListener = AdapterDelegateClickListenerAdapter(this, itemClickListener)
binding.buttonMore.setOnClickListener(eventListener)
bind { override fun onItemLongClick(item: Manga, view: View): Boolean {
return listener.onItemLongClick(item, this@adapterDelegateViewBinding.item, view)
}
override fun onClick(v: View?) {
listener.onSectionClick(item, itemView)
}
}
val adapter = AsyncListDifferDelegationAdapter(
MangaItemDiffCallback(),
mangaGridItemAD(coil, lifecycleOwner, listenerAdapter, sizeResolver)
)
binding.recyclerView.setRecycledViewPool(sharedPool)
binding.recyclerView.adapter = adapter
val spacingDecoration = SpacingItemDecoration(context.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
binding.recyclerView.addItemDecoration(spacingDecoration)
binding.buttonMore.setOnClickListener(listenerAdapter)
bind { payloads ->
if (payloads.isEmpty()) {
binding.recyclerView.clearItemDecorations()
binding.recyclerView.addItemDecoration(spacingDecoration)
selectionController.attachToRecyclerView(item, binding.recyclerView)
}
binding.textViewTitle.text = item.getTitle(context.resources) binding.textViewTitle.text = item.getTitle(context.resources)
binding.buttonMore.setTextAndVisible(item.showAllButtonText) binding.buttonMore.setTextAndVisible(item.showAllButtonText)
adapter.items = item.items adapter.items = item.items

View File

@@ -0,0 +1,15 @@
package org.koitharu.kotatsu.library.ui.adapter
import android.view.View
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.parsers.model.Manga
interface LibraryListEventListener : ListStateHolderListener {
fun onItemClick(item: Manga, section: LibrarySectionModel, view: View)
fun onItemLongClick(item: Manga, section: LibrarySectionModel, view: View): Boolean
fun onSectionClick(section: LibrarySectionModel, view: View)
}

View File

@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.core.ui.DateTimeAgo
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
sealed class LibraryGroupModel( sealed class LibrarySectionModel(
val items: List<MangaItemModel>, val items: List<MangaItemModel>,
@StringRes val showAllButtonText: Int, @StringRes val showAllButtonText: Int,
) : ListModel { ) : ListModel {
@@ -20,7 +20,7 @@ sealed class LibraryGroupModel(
items: List<MangaItemModel>, items: List<MangaItemModel>,
val timeAgo: DateTimeAgo?, val timeAgo: DateTimeAgo?,
showAllButtonText: Int, showAllButtonText: Int,
) : LibraryGroupModel(items, showAllButtonText) { ) : LibrarySectionModel(items, showAllButtonText) {
override val key: Any override val key: Any
get() = timeAgo?.javaClass ?: this::class.java get() = timeAgo?.javaClass ?: this::class.java
@@ -48,13 +48,17 @@ sealed class LibraryGroupModel(
result = 31 * result + showAllButtonText.hashCode() result = 31 * result + showAllButtonText.hashCode()
return result return result
} }
override fun toString(): String {
return "hist_$timeAgo"
}
} }
class Favourites( class Favourites(
items: List<MangaItemModel>, items: List<MangaItemModel>,
val category: FavouriteCategory, val category: FavouriteCategory,
showAllButtonText: Int, showAllButtonText: Int,
) : LibraryGroupModel(items, showAllButtonText) { ) : LibrarySectionModel(items, showAllButtonText) {
override val key: Any override val key: Any
get() = category.id get() = category.id
@@ -82,5 +86,9 @@ sealed class LibraryGroupModel(
result = 31 * result + showAllButtonText.hashCode() result = 31 * result + showAllButtonText.hashCode()
return result return result
} }
override fun toString(): String {
return "fav_${category.id}"
}
} }
} }

View File

@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.setTextAndVisible import org.koitharu.kotatsu.utils.ext.setTextAndVisible
fun emptyStateListAD( fun emptyStateListAD(
listener: MangaListListener, listener: ListStateHolderListener,
) = adapterDelegateViewBinding<EmptyState, ListModel, ItemEmptyStateBinding>( ) = adapterDelegateViewBinding<EmptyState, ListModel, ItemEmptyStateBinding>(
{ inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) } { inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) }
) { ) {

View File

@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
fun errorStateListAD( fun errorStateListAD(
listener: MangaListListener, listener: ListStateHolderListener,
) = adapterDelegateViewBinding<ErrorState, ListModel, ItemErrorStateBinding>( ) = adapterDelegateViewBinding<ErrorState, ListModel, ItemErrorStateBinding>(
{ inflater, parent -> ItemErrorStateBinding.inflate(inflater, parent, false) } { inflater, parent -> ItemErrorStateBinding.inflate(inflater, parent, false) }
) { ) {

View File

@@ -0,0 +1,8 @@
package org.koitharu.kotatsu.list.ui.adapter
interface ListStateHolderListener {
fun onRetryClick(error: Throwable)
fun onEmptyActionClick()
}

View File

@@ -4,10 +4,9 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
interface MangaListListener : OnListItemClickListener<Manga> { interface MangaListListener : OnListItemClickListener<Manga>, ListStateHolderListener {
fun onRetryClick(error: Throwable)
fun onTagRemoveClick(tag: MangaTag) fun onTagRemoveClick(tag: MangaTag)
fun onFilterClick() fun onFilterClick()
fun onEmptyActionClick()
} }