diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/ListSelectionController.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/ListSelectionController.kt index b3171a9df..cc479e34a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/ListSelectionController.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/ListSelectionController.kt @@ -27,11 +27,14 @@ class ListSelectionController( ) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider { private var actionMode: ActionMode? = null - private val stateEventObserver = StateEventObserver() val count: Int get() = decoration.checkedItemsCount + init { + registryOwner.lifecycle.addObserver(StateEventObserver()) + } + fun snapshot(): Set { return peekCheckedIds().toSet() } @@ -55,7 +58,6 @@ class ListSelectionController( fun attachToRecyclerView(recyclerView: RecyclerView) { recyclerView.addItemDecoration(decoration) - registryOwner.lifecycle.addObserver(stateEventObserver) } override fun saveState(): Bundle { diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/SectionedSelectionController.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/SectionedSelectionController.kt new file mode 100644 index 000000000..ba9404ac4 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/SectionedSelectionController.kt @@ -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( + private val activity: Activity, + private val registryOwner: SavedStateRegistryOwner, + private val callback: Callback, +) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider { + + private var actionMode: ActionMode? = null + + private var pendingData: MutableMap>? = null + private val decorations = ArrayMap() + + val count: Int + get() = decorations.values.sumOf { it.checkedItemsCount } + + init { + registryOwner.lifecycle.addObserver(StateEventObserver()) + } + + fun snapshot(): Map> { + return decorations.mapValues { it.value.checkedItemsIds.toSet() } + } + + fun peekCheckedIds(): Map> { + 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>) { + 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 : 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() } + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt index 03bafa077..614c9d3b7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt @@ -15,6 +15,8 @@ sealed class DateTimeAgo : ListModel { override fun format(resources: Resources): String { return resources.getString(R.string.just_now) } + + override fun toString() = "just_now" } class MinutesAgo(val minutes: Int) : DateTimeAgo() { @@ -31,6 +33,8 @@ sealed class DateTimeAgo : ListModel { } override fun hashCode(): Int = minutes + + override fun toString() = "minutes_ago_$minutes" } class HoursAgo(val hours: Int) : DateTimeAgo() { @@ -46,18 +50,24 @@ sealed class DateTimeAgo : ListModel { } override fun hashCode(): Int = hours + + override fun toString() = "hours_ago_$hours" } object Today : DateTimeAgo() { override fun format(resources: Resources): String { return resources.getString(R.string.today) } + + override fun toString() = "today" } object Yesterday : DateTimeAgo() { override fun format(resources: Resources): String { return resources.getString(R.string.yesterday) } + + override fun toString() = "yesterday" } class DaysAgo(val days: Int) : DateTimeAgo() { @@ -73,6 +83,8 @@ sealed class DateTimeAgo : ListModel { } override fun hashCode(): Int = days + + override fun toString() = "days_ago_$days" } class Absolute(private val date: Date) : DateTimeAgo() { @@ -97,11 +109,15 @@ sealed class DateTimeAgo : ListModel { override fun hashCode(): Int { return day } + + override fun toString() = "abs_$day" } object LongAgo : DateTimeAgo() { override fun format(resources: Resources): String { return resources.getString(R.string.long_ago) } + + override fun toString() = "long_ago" } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryFragment.kt b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryFragment.kt index 2d131e85f..bdb6e1569 100644 --- a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryFragment.kt @@ -2,7 +2,6 @@ package org.koitharu.kotatsu.library.ui import android.os.Bundle import android.view.* -import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.core.graphics.Insets 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.koitharu.kotatsu.R 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.details.ui.DetailsActivity import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet import org.koitharu.kotatsu.history.ui.HistoryActivity 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.MangaSelectionDecoration -import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.model.ListModel 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.ext.findViewsByType import org.koitharu.kotatsu.utils.ext.getDisplayMessage -class LibraryFragment : BaseFragment(), MangaListListener, ActionMode.Callback { +class LibraryFragment : BaseFragment(), LibraryListEventListener, + SectionedSelectionController.Callback { private val viewModel by viewModel() private var adapter: LibraryAdapter? = null - private var selectionDecoration: MangaSelectionDecoration? = null - private var actionMode: ActionMode? = null + private var selectionController: SectionedSelectionController? = null override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentLibraryBinding { return FragmentLibraryBinding.inflate(inflater, container, false) @@ -44,19 +44,17 @@ class LibraryFragment : BaseFragment(), MangaListListene override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val sizeResolver = ItemSizeResolver(resources, get()) - val itemCLickListener = object : OnListItemClickListener { - override fun onItemClick(item: LibraryGroupModel, view: View) { - onGroupClick(item, view) - } - } - selectionDecoration = MangaSelectionDecoration(view.context) + selectionController = SectionedSelectionController( + activity = requireActivity(), + registryOwner = this, + callback = this, + ) adapter = LibraryAdapter( lifecycleOwner = viewLifecycleOwner, coil = get(), listener = this, - itemClickListener = itemCLickListener, sizeResolver = sizeResolver, - selectionDecoration = checkNotNull(selectionDecoration), + selectionController = checkNotNull(selectionController), ) binding.recyclerView.adapter = adapter binding.recyclerView.setHasFixedSize(true) @@ -68,41 +66,29 @@ class LibraryFragment : BaseFragment(), MangaListListene override fun onDestroyView() { super.onDestroyView() adapter = null - selectionDecoration = null - actionMode = null - } - - override fun onItemClick(item: Manga, view: View) { - if (selectionDecoration?.checkedItemsCount != 0) { - selectionDecoration?.toggleItemChecked(item.id) - if (selectionDecoration?.checkedItemsCount == 0) { - actionMode?.finish() - } else { - actionMode?.invalidate() - invalidateItemDecorations() - } - return - } - val intent = DetailsActivity.newIntent(view.context, item) - startActivity(intent) + selectionController = null } - override fun onItemLongClick(item: Manga, view: View): Boolean { - if (actionMode == null) { - actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) + override fun onItemClick(item: Manga, section: LibrarySectionModel, view: View) { + if (selectionController?.onItemClick(section, item.id) != true) { + val intent = DetailsActivity.newIntent(view.context, item) + startActivity(intent) } - return actionMode?.also { - selectionDecoration?.setItemIsChecked(item.id, true) - invalidateItemDecorations() - it.invalidate() - } != null } - override fun onRetryClick(error: Throwable) = Unit + override fun onItemLongClick(item: Manga, section: LibrarySectionModel, view: View): Boolean { + return selectionController?.onItemLongClick(section, item.id) ?: false + } - override fun onTagRemoveClick(tag: MangaTag) = Unit + override fun onSectionClick(section: LibrarySectionModel, view: View) { + val intent = when (section) { + is LibrarySectionModel.History -> HistoryActivity.newIntent(view.context) + is LibrarySectionModel.Favourites -> TODO() + } + startActivity(intent) + } - override fun onFilterClick() = Unit + override fun onRetryClick(error: Throwable) = Unit override fun onEmptyActionClick() = Unit @@ -121,7 +107,7 @@ class LibraryFragment : BaseFragment(), MangaListListene } override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.title = selectionDecoration?.checkedItemsCount?.toString() + mode.title = selectionController?.count?.toString() return true } @@ -147,26 +133,28 @@ class LibraryFragment : BaseFragment(), MangaListListene } } - override fun onDestroyActionMode(mode: ActionMode) { - selectionDecoration?.clearSelection() + override fun onSelectionChanged(count: Int) { invalidateItemDecorations() - actionMode = null } - private fun onGroupClick(item: LibraryGroupModel, view: View) { - val intent = when (item) { - is LibraryGroupModel.History -> HistoryActivity.newIntent(view.context) - is LibraryGroupModel.Favourites -> TODO() + override fun onCreateItemDecoration(section: LibrarySectionModel): AbstractSelectionItemDecoration { + return MangaSelectionDecoration(requireContext()) + } + + private fun collectSelectedItemsMap(): Map> { + val snapshot = selectionController?.snapshot() + if (snapshot.isNullOrEmpty()) { + return emptyMap() } - startActivity(intent) + return snapshot.mapValues { (_, ids) -> viewModel.getManga(ids) } } private fun collectSelectedItems(): Set { - val ids = selectionDecoration?.checkedItemsIds - if (ids.isNullOrEmpty()) { + val snapshot = selectionController?.snapshot() + if (snapshot.isNullOrEmpty()) { return emptySet() } - return emptySet()//viewModel.getItems(ids) + return viewModel.getManga(snapshot.values.flattenTo(HashSet())) } private fun invalidateItemDecorations() { diff --git a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt index 47b38d652..548f4d804 100644 --- a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.library.ui +import androidx.collection.ArraySet import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope 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.MangaWithHistory 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.ui.model.* import org.koitharu.kotatsu.parsers.model.Manga @@ -57,6 +58,25 @@ class LibraryViewModel( } } + fun getManga(ids: Set): Set { + val snapshot = content.value ?: return emptySet() + val result = ArraySet(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( history: List, favourites: Map>, @@ -66,12 +86,12 @@ class LibraryViewModel( result += mapHistory(history) } 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 } - private suspend fun mapHistory(list: List): List { + private suspend fun mapHistory(list: List): List { val showPercent = settings.isReadingIndicatorsEnabled val groups = ArrayList() val map = HashMap>() @@ -84,12 +104,12 @@ class LibraryViewModel( } map.getOrPut(date) { ArrayList() }.add(manga.toGridModel(counter, percent)) } - val result = ArrayList(HISTORY_MAX_SEGMENTS) + val result = ArrayList(HISTORY_MAX_SEGMENTS) repeat(minOf(HISTORY_MAX_SEGMENTS - 1, groups.size - 1)) { i -> val key = groups[i] val values = map.remove(key) if (!values.isNullOrEmpty()) { - result.add(LibraryGroupModel.History(values, key, 0)) + result.add(LibrarySectionModel.History(values, key, 0)) } } val values = map.values.flatten() @@ -99,7 +119,7 @@ class LibraryViewModel( } else { 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 } diff --git a/app/src/main/java/org/koitharu/kotatsu/library/ui/adapter/LibraryAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/library/ui/adapter/LibraryAdapter.kt index 99e892c25..72b0d132f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/library/ui/adapter/LibraryAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/library/ui/adapter/LibraryAdapter.kt @@ -5,21 +5,22 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.library.ui.model.LibraryGroupModel +import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController +import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel import org.koitharu.kotatsu.list.ui.ItemSizeResolver -import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration -import org.koitharu.kotatsu.list.ui.adapter.* +import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD +import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD +import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD +import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.model.ListModel import kotlin.jvm.internal.Intrinsics class LibraryAdapter( lifecycleOwner: LifecycleOwner, coil: ImageLoader, - listener: MangaListListener, + listener: LibraryListEventListener, sizeResolver: ItemSizeResolver, - selectionDecoration: MangaSelectionDecoration, - itemClickListener: OnListItemClickListener, + selectionController: SectionedSelectionController, ) : AsyncListDifferDelegationAdapter(DiffCallback()) { init { @@ -31,9 +32,8 @@ class LibraryAdapter( lifecycleOwner = lifecycleOwner, coil = coil, sizeResolver = sizeResolver, - selectionDecoration = selectionDecoration, + selectionController = selectionController, listener = listener, - itemClickListener = itemClickListener, ) ) .addDelegate(loadingStateAD()) @@ -46,7 +46,7 @@ class LibraryAdapter( override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { return when { - oldItem is LibraryGroupModel && newItem is LibraryGroupModel -> { + oldItem is LibrarySectionModel && newItem is LibrarySectionModel -> { oldItem.key == newItem.key } else -> oldItem.javaClass == newItem.javaClass @@ -59,7 +59,7 @@ class LibraryAdapter( override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? { return when { - oldItem is LibraryGroupModel && newItem is LibraryGroupModel -> Unit + oldItem is LibrarySectionModel && newItem is LibrarySectionModel -> Unit else -> super.getChangePayload(oldItem, newItem) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/library/ui/adapter/LibraryGroupAD.kt b/app/src/main/java/org/koitharu/kotatsu/library/ui/adapter/LibraryGroupAD.kt index dca6757c4..3d08a43b7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/library/ui/adapter/LibraryGroupAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/library/ui/adapter/LibraryGroupAD.kt @@ -1,21 +1,22 @@ package org.koitharu.kotatsu.library.ui.adapter +import android.view.View import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding 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.SectionedSelectionController import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration 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.MangaSelectionDecoration import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.utils.ext.clearItemDecorations import org.koitharu.kotatsu.utils.ext.setTextAndVisible fun libraryGroupAD( @@ -23,26 +24,42 @@ fun libraryGroupAD( lifecycleOwner: LifecycleOwner, coil: ImageLoader, sizeResolver: ItemSizeResolver, - selectionDecoration: MangaSelectionDecoration, - listener: OnListItemClickListener, - itemClickListener: OnListItemClickListener, -) = adapterDelegateViewBinding( + selectionController: SectionedSelectionController, + listener: LibraryListEventListener, +) = adapterDelegateViewBinding( { layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) } ) { - binding.recyclerView.setRecycledViewPool(sharedPool) - val adapter = AsyncListDifferDelegationAdapter( + val listenerAdapter = object : OnListItemClickListener, View.OnClickListener { + override fun onItemClick(item: Manga, view: View) { + listener.onItemClick(item, this@adapterDelegateViewBinding.item, view) + } + + 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, listener, sizeResolver) + mangaGridItemAD(coil, lifecycleOwner, listenerAdapter, sizeResolver) ) - binding.recyclerView.addItemDecoration(selectionDecoration) + binding.recyclerView.setRecycledViewPool(sharedPool) 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) + val spacingDecoration = SpacingItemDecoration(context.resources.getDimensionPixelOffset(R.dimen.grid_spacing)) + binding.recyclerView.addItemDecoration(spacingDecoration) + binding.buttonMore.setOnClickListener(listenerAdapter) - bind { + 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.buttonMore.setTextAndVisible(item.showAllButtonText) adapter.items = item.items diff --git a/app/src/main/java/org/koitharu/kotatsu/library/ui/adapter/LibraryListEventListener.kt b/app/src/main/java/org/koitharu/kotatsu/library/ui/adapter/LibraryListEventListener.kt new file mode 100644 index 000000000..8416c458b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/library/ui/adapter/LibraryListEventListener.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/library/ui/model/LibraryGroupModel.kt b/app/src/main/java/org/koitharu/kotatsu/library/ui/model/LibrarySectionModel.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/library/ui/model/LibraryGroupModel.kt rename to app/src/main/java/org/koitharu/kotatsu/library/ui/model/LibrarySectionModel.kt index 20dc18e2f..a374f463b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/library/ui/model/LibraryGroupModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/library/ui/model/LibrarySectionModel.kt @@ -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.MangaItemModel -sealed class LibraryGroupModel( +sealed class LibrarySectionModel( val items: List, @StringRes val showAllButtonText: Int, ) : ListModel { @@ -20,7 +20,7 @@ sealed class LibraryGroupModel( items: List, val timeAgo: DateTimeAgo?, showAllButtonText: Int, - ) : LibraryGroupModel(items, showAllButtonText) { + ) : LibrarySectionModel(items, showAllButtonText) { override val key: Any get() = timeAgo?.javaClass ?: this::class.java @@ -48,13 +48,17 @@ sealed class LibraryGroupModel( result = 31 * result + showAllButtonText.hashCode() return result } + + override fun toString(): String { + return "hist_$timeAgo" + } } class Favourites( items: List, val category: FavouriteCategory, showAllButtonText: Int, - ) : LibraryGroupModel(items, showAllButtonText) { + ) : LibrarySectionModel(items, showAllButtonText) { override val key: Any get() = category.id @@ -82,5 +86,9 @@ sealed class LibraryGroupModel( result = 31 * result + showAllButtonText.hashCode() return result } + + override fun toString(): String { + return "fav_${category.id}" + } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyStateListAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyStateListAD.kt index 8385090ab..811b22fd5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyStateListAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyStateListAD.kt @@ -7,7 +7,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.utils.ext.setTextAndVisible fun emptyStateListAD( - listener: MangaListListener, + listener: ListStateHolderListener, ) = adapterDelegateViewBinding( { inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) } ) { diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ErrorStateListAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ErrorStateListAD.kt index 084c0c28f..a31b55d3f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ErrorStateListAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ErrorStateListAD.kt @@ -8,7 +8,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.utils.ext.getDisplayMessage fun errorStateListAD( - listener: MangaListListener, + listener: ListStateHolderListener, ) = adapterDelegateViewBinding( { inflater, parent -> ItemErrorStateBinding.inflate(inflater, parent, false) } ) { diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListStateHolderListener.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListStateHolderListener.kt new file mode 100644 index 000000000..5040db801 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListStateHolderListener.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.list.ui.adapter + +interface ListStateHolderListener { + + fun onRetryClick(error: Throwable) + + fun onEmptyActionClick() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListListener.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListListener.kt index d9cc8d8e4..c515ef7ec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListListener.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListListener.kt @@ -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.MangaTag -interface MangaListListener : OnListItemClickListener { +interface MangaListListener : OnListItemClickListener, ListStateHolderListener { - fun onRetryClick(error: Throwable) fun onTagRemoveClick(tag: MangaTag) + fun onFilterClick() - fun onEmptyActionClick() } \ No newline at end of file