From baaf9320e915dce516bb948702389e68eb78c2f5 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Fri, 14 Jun 2024 20:15:36 -0600 Subject: [PATCH] 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 --- .../ui/download/DownloadButtonSetup.kt | 4 +- .../ui/download/DownloadFragment.kt | 32 +++---- .../ui/download/DownloadHeaderAdapter.kt | 12 +-- ...back.kt => DownloadSwipeDeleteCallback.kt} | 95 +++++++++++++++++-- 4 files changed, 106 insertions(+), 37 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/ui/download/{DownloadSwipeToDeleteCallback.kt => DownloadSwipeDeleteCallback.kt} (61%) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 63a268ed..10ce67a7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.download +import android.app.Activity import android.content.DialogInterface import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -19,7 +20,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager object DownloadButtonSetup { - fun handleDownloadClick(click: DownloadClickEvent, deleteCallback: () -> Unit = {}) { + fun handleDownloadClick(click: DownloadClickEvent) { val id = click.data.id if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return when (click.action) { @@ -31,7 +32,6 @@ object DownloadButtonSetup { when (which) { DialogInterface.BUTTON_POSITIVE -> { VideoDownloadManager.deleteFileAndUpdateSettings(ctx, id) - deleteCallback.invoke() } DialogInterface.BUTTON_NEGATIVE -> { } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 89979dfd..f0036351 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -66,7 +66,7 @@ class DownloadFragment : Fragment() { @SuppressLint("NotifyDataSetChanged") private fun setList(list: List) { main { - (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list + (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list.toMutableList() binding?.downloadList?.adapter?.notifyDataSetChanged() } } @@ -147,7 +147,7 @@ class DownloadFragment : Fragment() { when (click.action) { 0 -> { if (click.data.type.isMovieType()) { - //wont be called + // Won't be called } else { val folder = DataStore.getFolderName( DOWNLOAD_EPISODE_CACHE, @@ -173,26 +173,21 @@ class DownloadFragment : Fragment() { if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter handleDownloadClick(downloadClickEvent) if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { - context?.let { ctx -> - downloadsViewModel.updateList(ctx) + 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 } } } ) - 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 { setHasFixedSize(true) setItemViewCacheSize(20) @@ -203,10 +198,9 @@ class DownloadFragment : Fragment() { nextUp = FOCUS_SELF, nextDown = FOCUS_SELF ) - //layoutManager = GridLayoutManager(context, 1) val itemTouchHelper = ItemTouchHelper( - DownloadSwipeToDeleteCallback( + DownloadSwipeDeleteCallback( this.adapter as DownloadHeaderAdapter, context ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt index 65a6441f..749f0da0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt @@ -29,7 +29,7 @@ data class DownloadHeaderClickEvent( ) class DownloadHeaderAdapter( - var cardList: List, + var cardList: MutableList, private val clickCallback: (DownloadHeaderClickEvent) -> Unit, private val movieClickCallback: (DownloadClickEvent) -> Unit, ) : RecyclerView.Adapter() { @@ -55,7 +55,7 @@ class DownloadHeaderAdapter( } override fun getItemCount(): Int { - return cardList.size + return cardList.count() } class DownloadHeaderViewHolder @@ -94,7 +94,7 @@ class DownloadHeaderAdapter( if (card.child != null) { //downloadHeaderProgressDownloaded.visibility = View.VISIBLE - // downloadHeaderEpisodeDownload.visibility = View.VISIBLE + // downloadHeaderEpisodeDownload.visibility = View.VISIBLE binding.downloadHeaderGotoChild.visibility = View.GONE downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, movieClickCallback) @@ -119,8 +119,8 @@ class DownloadHeaderAdapter( } } else { downloadButton.isVisible = false - // downloadHeaderProgressDownloaded.visibility = View.GONE - // downloadHeaderEpisodeDownload.visibility = View.GONE + // downloadHeaderProgressDownloaded.visibility = View.GONE + // downloadHeaderEpisodeDownload.visibility = View.GONE binding.downloadHeaderGotoChild.visibility = View.VISIBLE try { @@ -146,4 +146,4 @@ class DownloadHeaderAdapter( } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadSwipeToDeleteCallback.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadSwipeDeleteCallback.kt similarity index 61% rename from app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadSwipeToDeleteCallback.kt rename to app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadSwipeDeleteCallback.kt index 14652d25..ef6b5ea6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadSwipeToDeleteCallback.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadSwipeDeleteCallback.kt @@ -16,12 +16,15 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.isEpisodeBased import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread +import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadDeleteEvent -class DownloadSwipeToDeleteCallback( +class DownloadSwipeDeleteCallback( private val adapter: DownloadHeaderAdapter, private val context: Context ) : ItemTouchHelper.Callback() { + private var downloadDeleteEventListener: ((Int) -> Unit)? = null + private val swipeOpenItems: MutableSet = mutableSetOf() private val deleteIcon: Drawable? by lazy { ContextCompat.getDrawable(context, R.drawable.ic_baseline_delete_outline_24) @@ -39,7 +42,8 @@ class DownloadSwipeToDeleteCallback( val position = viewHolder.bindingAdapterPosition val item = adapter.cardList[position] 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.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 ) } @@ -127,6 +136,14 @@ class DownloadSwipeToDeleteCallback( } } + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ) { + clearDownloadDeleteEvent() + super.clearView(recyclerView, viewHolder) + } + @SuppressLint("ClickableViewAccessibility") private fun setRecyclerViewTouchListener( recyclerView: RecyclerView, @@ -136,14 +153,17 @@ class DownloadSwipeToDeleteCallback( if (event.action == MotionEvent.ACTION_UP) { val x = event.x.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) vh?.itemView?.let { swipeItemView -> val backgroundLeft: Int = swipeItemView.right - swipeDistance.toInt() val backgroundXRange: IntRange = backgroundLeft..swipeItemView.right val backgroundYRange: IntRange = swipeItemView.top..swipeItemView.bottom if (x in backgroundXRange && y in backgroundYRange) { - handleDelete(pos) + handleDeleteAction(pos) + addDownloadDeleteEvent(pos, recyclerView) return@setOnTouchListener true } } @@ -154,11 +174,19 @@ class DownloadSwipeToDeleteCallback( } @SuppressLint("ClickableViewAccessibility") - private fun removeRecyclerViewTouchListener( - recyclerView: RecyclerView - ): Unit = recyclerView.setOnTouchListener(null) + private fun removeRecyclerViewTouchListener(recyclerView: RecyclerView) { + // We don't want to unnecessarily listen to unused touch events + 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] runOnMainThread { item.child?.let { clickEvent -> @@ -167,8 +195,55 @@ class DownloadSwipeToDeleteCallback( DOWNLOAD_ACTION_DELETE_FILE, 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 + } + } } \ No newline at end of file