Swipe delete without reloading all of the cards and add some comments and other cleanup

We can use adapter.notifyItemRemoved and remove it from the cardList in adapater directly since we do understand the position in this case. It works better because it does not require reloading everything and causes less bugs with the view model.

I am not sure if this was the proper way to do this. If not I can revert the change to this method

It also removes the need for a callback in DownloadButtonSetup
This commit is contained in:
Luna712 2024-06-14 20:15:36 -06:00 committed by GitHub
parent 312c2a738b
commit baaf9320e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 106 additions and 37 deletions

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.download package com.lagradost.cloudstream3.ui.download
import android.app.Activity
import android.content.DialogInterface import android.content.DialogInterface
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@ -19,7 +20,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
object DownloadButtonSetup { object DownloadButtonSetup {
fun handleDownloadClick(click: DownloadClickEvent, deleteCallback: () -> Unit = {}) { fun handleDownloadClick(click: DownloadClickEvent) {
val id = click.data.id val id = click.data.id
if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return
when (click.action) { when (click.action) {
@ -31,7 +32,6 @@ object DownloadButtonSetup {
when (which) { when (which) {
DialogInterface.BUTTON_POSITIVE -> { DialogInterface.BUTTON_POSITIVE -> {
VideoDownloadManager.deleteFileAndUpdateSettings(ctx, id) VideoDownloadManager.deleteFileAndUpdateSettings(ctx, id)
deleteCallback.invoke()
} }
DialogInterface.BUTTON_NEGATIVE -> { DialogInterface.BUTTON_NEGATIVE -> {
} }

View file

@ -66,7 +66,7 @@ class DownloadFragment : Fragment() {
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun setList(list: List<VisualDownloadHeaderCached>) { private fun setList(list: List<VisualDownloadHeaderCached>) {
main { main {
(binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list.toMutableList()
binding?.downloadList?.adapter?.notifyDataSetChanged() binding?.downloadList?.adapter?.notifyDataSetChanged()
} }
} }
@ -147,7 +147,7 @@ class DownloadFragment : Fragment() {
when (click.action) { when (click.action) {
0 -> { 0 -> {
if (click.data.type.isMovieType()) { if (click.data.type.isMovieType()) {
//wont be called // Won't be called
} else { } else {
val folder = DataStore.getFolderName( val folder = DataStore.getFolderName(
DOWNLOAD_EPISODE_CACHE, DOWNLOAD_EPISODE_CACHE,
@ -173,26 +173,21 @@ class DownloadFragment : Fragment() {
if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter
handleDownloadClick(downloadClickEvent) handleDownloadClick(downloadClickEvent)
if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
context?.let { ctx -> downloadDeleteEventListener = { id ->
downloadsViewModel.updateList(ctx) val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList
if (list != null) {
if (list.any { it.data.id == id }) {
context?.let { ctx ->
downloadsViewModel.updateList(ctx)
}
}
}
} }
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
} }
} }
) )
downloadDeleteEventListener = { id ->
val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList
if (list != null) {
if (list.any { it.data.id == id }) {
context?.let { ctx ->
downloadsViewModel.updateList(ctx)
}
}
}
}
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
binding?.downloadList?.apply { binding?.downloadList?.apply {
setHasFixedSize(true) setHasFixedSize(true)
setItemViewCacheSize(20) setItemViewCacheSize(20)
@ -203,10 +198,9 @@ class DownloadFragment : Fragment() {
nextUp = FOCUS_SELF, nextUp = FOCUS_SELF,
nextDown = FOCUS_SELF nextDown = FOCUS_SELF
) )
//layoutManager = GridLayoutManager(context, 1)
val itemTouchHelper = ItemTouchHelper( val itemTouchHelper = ItemTouchHelper(
DownloadSwipeToDeleteCallback( DownloadSwipeDeleteCallback(
this.adapter as DownloadHeaderAdapter, this.adapter as DownloadHeaderAdapter,
context context
) )

View file

@ -29,7 +29,7 @@ data class DownloadHeaderClickEvent(
) )
class DownloadHeaderAdapter( class DownloadHeaderAdapter(
var cardList: List<VisualDownloadHeaderCached>, var cardList: MutableList<VisualDownloadHeaderCached>,
private val clickCallback: (DownloadHeaderClickEvent) -> Unit, private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
private val movieClickCallback: (DownloadClickEvent) -> Unit, private val movieClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
@ -55,7 +55,7 @@ class DownloadHeaderAdapter(
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
return cardList.size return cardList.count()
} }
class DownloadHeaderViewHolder class DownloadHeaderViewHolder
@ -94,7 +94,7 @@ class DownloadHeaderAdapter(
if (card.child != null) { if (card.child != null) {
//downloadHeaderProgressDownloaded.visibility = View.VISIBLE //downloadHeaderProgressDownloaded.visibility = View.VISIBLE
// downloadHeaderEpisodeDownload.visibility = View.VISIBLE // downloadHeaderEpisodeDownload.visibility = View.VISIBLE
binding.downloadHeaderGotoChild.visibility = View.GONE binding.downloadHeaderGotoChild.visibility = View.GONE
downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, movieClickCallback) downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, movieClickCallback)
@ -119,8 +119,8 @@ class DownloadHeaderAdapter(
} }
} else { } else {
downloadButton.isVisible = false downloadButton.isVisible = false
// downloadHeaderProgressDownloaded.visibility = View.GONE // downloadHeaderProgressDownloaded.visibility = View.GONE
// downloadHeaderEpisodeDownload.visibility = View.GONE // downloadHeaderEpisodeDownload.visibility = View.GONE
binding.downloadHeaderGotoChild.visibility = View.VISIBLE binding.downloadHeaderGotoChild.visibility = View.VISIBLE
try { try {
@ -146,4 +146,4 @@ class DownloadHeaderAdapter(
} }
} }
} }
} }

View file

@ -16,12 +16,15 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.isEpisodeBased import com.lagradost.cloudstream3.isEpisodeBased
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadDeleteEvent
class DownloadSwipeToDeleteCallback( class DownloadSwipeDeleteCallback(
private val adapter: DownloadHeaderAdapter, private val adapter: DownloadHeaderAdapter,
private val context: Context private val context: Context
) : ItemTouchHelper.Callback() { ) : ItemTouchHelper.Callback() {
private var downloadDeleteEventListener: ((Int) -> Unit)? = null
private val swipeOpenItems: MutableSet<Int> = mutableSetOf() private val swipeOpenItems: MutableSet<Int> = mutableSetOf()
private val deleteIcon: Drawable? by lazy { private val deleteIcon: Drawable? by lazy {
ContextCompat.getDrawable(context, R.drawable.ic_baseline_delete_outline_24) ContextCompat.getDrawable(context, R.drawable.ic_baseline_delete_outline_24)
@ -39,7 +42,8 @@ class DownloadSwipeToDeleteCallback(
val position = viewHolder.bindingAdapterPosition val position = viewHolder.bindingAdapterPosition
val item = adapter.cardList[position] val item = adapter.cardList[position]
return if (item.data.type.isEpisodeBased()) 0 else { return if (item.data.type.isEpisodeBased()) 0 else {
makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) val swipeFlags = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
makeMovementFlags(0, swipeFlags)
} }
} }
@ -102,7 +106,12 @@ class DownloadSwipeToDeleteCallback(
itemView.right.toFloat(), itemView.right.toFloat(),
itemView.bottom.toFloat() itemView.bottom.toFloat()
), ),
floatArrayOf(0f, 0f, 20f, 20f, 20f, 20f, 0f, 0f), floatArrayOf(
0f, 0f, // Top-left corner
20f, 20f, // Top-right corner
20f, 20f, // Bottom-right corner
0f, 0f // Bottom-left corner
),
Path.Direction.CW Path.Direction.CW
) )
} }
@ -127,6 +136,14 @@ class DownloadSwipeToDeleteCallback(
} }
} }
override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
clearDownloadDeleteEvent()
super.clearView(recyclerView, viewHolder)
}
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun setRecyclerViewTouchListener( private fun setRecyclerViewTouchListener(
recyclerView: RecyclerView, recyclerView: RecyclerView,
@ -136,14 +153,17 @@ class DownloadSwipeToDeleteCallback(
if (event.action == MotionEvent.ACTION_UP) { if (event.action == MotionEvent.ACTION_UP) {
val x = event.x.toInt() val x = event.x.toInt()
val y = event.y.toInt() val y = event.y.toInt()
swipeOpenItems.forEach { pos -> // We use toList() to avoid a very rare edge case
// where it gives concurrent modification errors
swipeOpenItems.toList().forEach { pos ->
val vh = recyclerView.findViewHolderForAdapterPosition(pos) val vh = recyclerView.findViewHolderForAdapterPosition(pos)
vh?.itemView?.let { swipeItemView -> vh?.itemView?.let { swipeItemView ->
val backgroundLeft: Int = swipeItemView.right - swipeDistance.toInt() val backgroundLeft: Int = swipeItemView.right - swipeDistance.toInt()
val backgroundXRange: IntRange = backgroundLeft..swipeItemView.right val backgroundXRange: IntRange = backgroundLeft..swipeItemView.right
val backgroundYRange: IntRange = swipeItemView.top..swipeItemView.bottom val backgroundYRange: IntRange = swipeItemView.top..swipeItemView.bottom
if (x in backgroundXRange && y in backgroundYRange) { if (x in backgroundXRange && y in backgroundYRange) {
handleDelete(pos) handleDeleteAction(pos)
addDownloadDeleteEvent(pos, recyclerView)
return@setOnTouchListener true return@setOnTouchListener true
} }
} }
@ -154,11 +174,19 @@ class DownloadSwipeToDeleteCallback(
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun removeRecyclerViewTouchListener( private fun removeRecyclerViewTouchListener(recyclerView: RecyclerView) {
recyclerView: RecyclerView // We don't want to unnecessarily listen to unused touch events
): Unit = recyclerView.setOnTouchListener(null) recyclerView.setOnTouchListener(null)
private fun handleDelete(position: Int) { /**
* If we are not listening to touch events, then
* we should clear the delete event as it will
* not be used at the moment.
*/
clearDownloadDeleteEvent()
}
private fun handleDeleteAction(position: Int) {
val item = adapter.cardList[position] val item = adapter.cardList[position]
runOnMainThread { runOnMainThread {
item.child?.let { clickEvent -> item.child?.let { clickEvent ->
@ -167,8 +195,55 @@ class DownloadSwipeToDeleteCallback(
DOWNLOAD_ACTION_DELETE_FILE, DOWNLOAD_ACTION_DELETE_FILE,
clickEvent clickEvent
) )
) { adapter.notifyItemRemoved(position) } )
} }
} }
} }
private fun addDownloadDeleteEvent(
position: Int,
recyclerView: RecyclerView
) {
// Clear any old events as we don't want to get
// concurrent modification errors
clearDownloadDeleteEvent()
downloadDeleteEventListener = { id: Int ->
val list = adapter.cardList
if (list.any { it.data.id == id }) {
/**
* Seamlessly remove now-deleted item from adapter.
* We don't need to reload from the viewModel,
* that just causes more unnecessary actions and
* unreliable data to be returned oftentimes,
* as it would cause it to reload the entire
* view model (which is all items) we only want
* to target one item and this provides a more seamless
* and performant solution to it since we do have access to
* the position we need to target here.
*/
adapter.cardList.removeAt(position)
adapter.notifyItemRemoved(position)
adapter.notifyItemRangeChanged(
position,
adapter.cardList.size
) // rebind to new positions
/**
* we need to clear the listener now since nothing should be open
* and it was closed outside of on onChildDraw so we don't
* want to have random unexpected touch events.
*/
removeRecyclerViewTouchListener(recyclerView)
}
}
downloadDeleteEventListener?.let { downloadDeleteEvent += it }
}
private fun clearDownloadDeleteEvent() {
if (downloadDeleteEventListener != null) {
downloadDeleteEvent -= downloadDeleteEventListener!!
downloadDeleteEventListener = null
}
}
} }