Fix list items reorder

This commit is contained in:
Koitharu
2023-12-05 09:59:07 +02:00
parent 21639ddcbc
commit 357669d8b2
9 changed files with 184 additions and 127 deletions

View File

@@ -0,0 +1,75 @@
package org.koitharu.kotatsu.core.ui
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AdapterDelegate
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.model.ListModel
import java.util.Collections
import java.util.LinkedList
open class ReorderableListAdapter<T : ListModel> : ListDelegationAdapter<List<T>>(), FlowCollector<List<T>?> {
private val listListeners = LinkedList<ListListener<T>>()
override suspend fun emit(value: List<T>?) {
val oldList = items.orEmpty()
val newList = value.orEmpty()
val diffResult = withContext(Dispatchers.Default) {
val diffCallback = DiffCallback(oldList, newList)
DiffUtil.calculateDiff(diffCallback)
}
super.setItems(newList)
diffResult.dispatchUpdatesTo(this)
listListeners.forEach { it.onCurrentListChanged(oldList, newList) }
}
@Deprecated("Use emit() to dispatch list updates", level = DeprecationLevel.ERROR)
override fun setItems(items: List<T>?) {
super.setItems(items)
}
fun reorderItems(oldPos: Int, newPos: Int) {
Collections.swap(items ?: return, oldPos, newPos)
notifyItemMoved(oldPos, newPos)
}
fun addDelegate(type: ListItemType, delegate: AdapterDelegate<List<T>>): ReorderableListAdapter<T> {
delegatesManager.addDelegate(type.ordinal, delegate)
return this
}
fun addListListener(listListener: ListListener<T>) {
listListeners.add(listListener)
}
fun removeListListener(listListener: ListListener<T>) {
listListeners.remove(listListener)
}
protected class DiffCallback<T : ListModel>(
val oldList: List<T>,
val newList: List<T>,
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size
override fun getNewListSize(): Int = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList[oldItemPosition]
val newItem = newList[newItemPosition]
return newItem.areItemsTheSame(oldItem)
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList[oldItemPosition]
val newItem = newList[newItemPosition]
return newItem == oldItem
}
}
}

View File

@@ -142,7 +142,11 @@ class FavouriteCategoriesActivity :
}
val fromPos = viewHolder.bindingAdapterPosition
val toPos = target.bindingAdapterPosition
return fromPos != toPos && fromPos != RecyclerView.NO_POSITION && toPos != RecyclerView.NO_POSITION
if (fromPos == toPos || fromPos == RecyclerView.NO_POSITION || toPos == RecyclerView.NO_POSITION) {
return false
}
adapter.reorderItems(fromPos, toPos)
return true
}
override fun canDropOver(
@@ -151,25 +155,16 @@ class FavouriteCategoriesActivity :
target: RecyclerView.ViewHolder,
): Boolean = current.itemViewType == target.itemViewType
override fun onMoved(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
fromPos: Int,
target: RecyclerView.ViewHolder,
toPos: Int,
x: Int,
y: Int,
) {
super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y)
viewModel.reorderCategories(fromPos, toPos)
}
override fun isLongPressDragEnabled(): Boolean = false
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
viewBinding.recyclerView.isNestedScrollingEnabled =
actionState == ItemTouchHelper.ACTION_STATE_IDLE
viewBinding.recyclerView.isNestedScrollingEnabled = actionState == ItemTouchHelper.ACTION_STATE_IDLE
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewModel.saveOrder(adapter.items ?: return)
}
}

View File

@@ -1,13 +1,14 @@
package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.yield
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -19,7 +20,6 @@ import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.parsers.util.move
import javax.inject.Inject
@HiltViewModel
@@ -30,17 +30,9 @@ class FavouritesCategoriesViewModel @Inject constructor(
private var commitJob: Job? = null
val content = MutableStateFlow<List<ListModel>>(listOf(LoadingState))
init {
launchJob(Dispatchers.Default) {
repository.observeCategoriesWithCovers()
.collectLatest {
commitJob?.join()
updateContent(it)
}
}
}
val content = repository.observeCategoriesWithCovers()
.map { it.toUiList() }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
fun deleteCategories(ids: Set<Long>) {
launchJob(Dispatchers.Default) {
@@ -54,11 +46,17 @@ class FavouritesCategoriesViewModel @Inject constructor(
fun isEmpty(): Boolean = content.value.none { it is CategoryListModel }
fun reorderCategories(oldPos: Int, newPos: Int) {
val snapshot = content.requireValue().toMutableList()
snapshot.move(oldPos, newPos)
content.value = snapshot
commit(snapshot)
fun saveOrder(snapshot: List<ListModel>) {
val prevJob = commitJob
commitJob = launchJob {
prevJob?.cancelAndJoin()
val ids = snapshot.mapNotNullTo(ArrayList(snapshot.size)) {
(it as? CategoryListModel)?.category?.id
}
if (ids.isNotEmpty()) {
repository.reorderCategories(ids)
}
}
}
fun setIsVisible(ids: Set<Long>, isVisible: Boolean) {
@@ -76,36 +74,21 @@ class FavouritesCategoriesViewModel @Inject constructor(
}
}
private fun commit(snapshot: List<ListModel>) {
val prevJob = commitJob
commitJob = launchJob {
prevJob?.cancelAndJoin()
delay(500)
val ids = snapshot.mapNotNullTo(ArrayList(snapshot.size)) {
(it as? CategoryListModel)?.category?.id
}
repository.reorderCategories(ids)
yield()
}
}
private fun updateContent(categories: Map<FavouriteCategory, List<Cover>>) {
content.value = categories.map { (category, covers) ->
CategoryListModel(
mangaCount = covers.size,
covers = covers.take(3),
category = category,
isTrackerEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources,
)
}.ifEmpty {
listOf(
EmptyState(
icon = R.drawable.ic_empty_favourites,
textPrimary = R.string.text_empty_holder_primary,
textSecondary = R.string.empty_favourite_categories,
actionStringRes = 0,
),
)
}
private fun Map<FavouriteCategory, List<Cover>>.toUiList(): List<ListModel> = map { (category, covers) ->
CategoryListModel(
mangaCount = covers.size,
covers = covers.take(3),
category = category,
isTrackerEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources,
)
}.ifEmpty {
listOf(
EmptyState(
icon = R.drawable.ic_empty_favourites,
textPrimary = R.string.text_empty_holder_primary,
textSecondary = R.string.empty_favourite_categories,
actionStringRes = 0,
),
)
}
}

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.favourites.ui.categories.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.ReorderableListAdapter
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
@@ -15,7 +15,7 @@ class CategoriesAdapter(
lifecycleOwner: LifecycleOwner,
onItemClickListener: FavouriteCategoriesListListener,
listListener: ListStateHolderListener,
) : BaseListAdapter<ListModel>() {
) : ReorderableListAdapter<ListModel>() {
init {
addDelegate(ListItemType.CATEGORY_LARGE, categoryAD(coil, lifecycleOwner, onItemClickListener))

View File

@@ -2,14 +2,14 @@ package org.koitharu.kotatsu.settings.sources.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.ReorderableListAdapter
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
class SourceConfigAdapter(
listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) : BaseListAdapter<SourceConfigItem>() {
) : ReorderableListAdapter<SourceConfigItem>() {
init {
with(delegatesManager) {

View File

@@ -54,6 +54,7 @@ class SourcesManageFragment :
lateinit var shortcutManager: AppShortcutManager
private var reorderHelper: ItemTouchHelper? = null
private var sourcesAdapter: SourceConfigAdapter? = null
private val viewModel by viewModels<SourcesManageViewModel>()
override val recyclerView: RecyclerView
@@ -69,7 +70,7 @@ class SourcesManageFragment :
savedInstanceState: Bundle?,
) {
super.onViewBindingCreated(binding, savedInstanceState)
val sourcesAdapter = SourceConfigAdapter(this, coil, viewLifecycleOwner)
sourcesAdapter = SourceConfigAdapter(this, coil, viewLifecycleOwner)
with(binding.recyclerView) {
setHasFixedSize(true)
adapter = sourcesAdapter
@@ -77,7 +78,7 @@ class SourcesManageFragment :
it.attachToRecyclerView(this)
}
}
viewModel.content.observe(viewLifecycleOwner, sourcesAdapter)
viewModel.content.observe(viewLifecycleOwner, checkNotNull(sourcesAdapter))
viewModel.onActionDone.observeEvent(
viewLifecycleOwner,
ReversibleActionObserver(binding.recyclerView),
@@ -91,6 +92,7 @@ class SourcesManageFragment :
}
override fun onDestroyView() {
sourcesAdapter = null
reorderHelper = null
super.onDestroyView()
}
@@ -204,7 +206,7 @@ class SourcesManageFragment :
y: Int,
) {
super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y)
viewModel.reorderSources(fromPos, toPos)
sourcesAdapter?.reorderItems(fromPos, toPos)
}
override fun canDropOver(
@@ -248,5 +250,10 @@ class SourcesManageFragment :
}
override fun isLongPressDragEnabled() = true
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewModel.saveSourcesOrder(sourcesAdapter?.items ?: return)
}
}
}

View File

@@ -4,8 +4,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.removeObserverAsync
@@ -43,17 +41,19 @@ class SourcesManageViewModel @Inject constructor(
database.invalidationTracker.removeObserverAsync(listProducer)
}
fun reorderSources(oldPos: Int, newPos: Int) {
val snapshot = content.value.toMutableList()
if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) {
return
fun saveSourcesOrder(snapshot: List<SourceConfigItem>) {
val prevJob = commitJob
commitJob = launchJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
val newSourcesList = snapshot.mapNotNull { x ->
if (x is SourceConfigItem.SourceItem && x.isDraggable) {
x.source
} else {
null
}
}
repository.setPositions(newSourcesList)
}
if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) {
return
}
snapshot.move(oldPos, newPos)
content.value = snapshot
commit(snapshot)
}
fun canReorder(oldPos: Int, newPos: Int): Boolean {
@@ -72,28 +72,31 @@ class SourcesManageViewModel @Inject constructor(
}
fun bringToTop(source: MangaSource) {
var oldPos = -1
var newPos = -1
val snapshot = content.value
for ((i, x) in snapshot.withIndex()) {
if (x !is SourceConfigItem.SourceItem) {
continue
launchJob(Dispatchers.Default) {
var oldPos = -1
var newPos = -1
for ((i, x) in snapshot.withIndex()) {
if (x !is SourceConfigItem.SourceItem) {
continue
}
if (newPos == -1) {
newPos = i
}
if (x.source == source) {
oldPos = i
break
}
}
if (newPos == -1) {
newPos = i
@Suppress("KotlinConstantConditions")
if (oldPos != -1 && newPos != -1) {
reorderSources(oldPos, newPos)
val revert = ReversibleAction(R.string.moved_to_top) {
reorderSources(newPos, oldPos)
}
commitJob?.join()
onActionDone.call(revert)
}
if (x.source == source) {
oldPos = i
break
}
}
@Suppress("KotlinConstantConditions")
if (oldPos != -1 && newPos != -1) {
reorderSources(oldPos, newPos)
val revert = ReversibleAction(R.string.moved_to_top) {
reorderSources(newPos, oldPos)
}
onActionDone.call(revert)
}
}
@@ -113,20 +116,15 @@ class SourcesManageViewModel @Inject constructor(
}
}
private fun commit(snapshot: List<SourceConfigItem>) {
val prevJob = commitJob
commitJob = launchJob {
prevJob?.cancelAndJoin()
delay(500)
val newSourcesList = snapshot.mapNotNull { x ->
if (x is SourceConfigItem.SourceItem && x.isDraggable) {
x.source
} else {
null
}
}
repository.setPositions(newSourcesList)
yield()
private fun reorderSources(oldPos: Int, newPos: Int) {
val snapshot = content.value.toMutableList()
if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) {
return
}
if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) {
return
}
snapshot.move(oldPos, newPos)
saveSourcesOrder(snapshot)
}
}