diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt index 9a026334..e8f969e8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -27,6 +27,7 @@ const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2 const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3 const val DOWNLOAD_ACTION_DOWNLOAD = 4 const val DOWNLOAD_ACTION_LONG_CLICK = 5 +const val DOWNLOAD_ACTION_DELETE_MULTIPLE_FILES = 6 const val DOWNLOAD_ACTION_GO_TO_CHILD = 0 const val DOWNLOAD_ACTION_LOAD_RESULT = 1 @@ -57,6 +58,11 @@ abstract class VisualDownloadCached( } } +abstract class DownloadActionEventBase( + open val action: Int, + open val data: VideoDownloadHelper.DownloadEpisodeCached? +) + data class VisualDownloadChildCached( override val currentBytes: Long, override val totalBytes: Long, @@ -73,9 +79,14 @@ data class VisualDownloadHeaderCached( ): VisualDownloadCached(currentBytes, totalBytes, data) data class DownloadClickEvent( - val action: Int, - val data: VideoDownloadHelper.DownloadEpisodeCached -) + override val action: Int, + override val data: VideoDownloadHelper.DownloadEpisodeCached +): DownloadActionEventBase(action, data) + +data class DownloadDeleteEvent( + override val action: Int, + val items: List +): DownloadActionEventBase(action, null) data class DownloadHeaderClickEvent( val action: Int, @@ -83,8 +94,8 @@ data class DownloadHeaderClickEvent( ) class DownloadAdapter( + private val actionCallback: (DownloadActionEventBase) -> Unit, private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val mediaClickCallback: (DownloadClickEvent) -> Unit, ) : ListAdapter(DiffCallback()) { companion object { @@ -94,8 +105,8 @@ class DownloadAdapter( inner class DownloadViewHolder( private val binding: ViewBinding, + private val actionCallback: (DownloadActionEventBase) -> Unit, private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val mediaClickCallback: (DownloadClickEvent) -> Unit, ) : RecyclerView.ViewHolder(binding.root) { fun bind(card: VisualDownloadCached?) { @@ -145,11 +156,11 @@ class DownloadAdapter( ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) } - downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback) + downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, actionCallback) downloadButton.isVisible = true episodeHolder.setOnClickListener { - mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child)) + actionCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child)) } } else { downloadButton.isVisible = false @@ -214,7 +225,7 @@ class DownloadAdapter( ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) } - downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, mediaClickCallback) + downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, actionCallback) downloadButton.isVisible = true downloadChildEpisodeText.apply { @@ -223,7 +234,7 @@ class DownloadAdapter( } downloadChildEpisodeHolder.setOnClickListener { - mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) + actionCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) } } } @@ -236,7 +247,7 @@ class DownloadAdapter( VIEW_TYPE_CHILD -> DownloadChildEpisodeBinding.inflate(inflater, parent, false) else -> throw IllegalArgumentException("Invalid view type") } - return DownloadViewHolder(binding, clickCallback, mediaClickCallback) + return DownloadViewHolder(binding, actionCallback, clickCallback) } override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) { 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 c8c40e29..cd3888e9 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 @@ -17,19 +17,21 @@ import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager +import kotlinx.coroutines.MainScope object DownloadButtonSetup { - fun handleDownloadClick(click: DownloadClickEvent) { - val id = click.data.id + fun handleDownloadClick(click: DownloadActionEventBase) { when (click.action) { DOWNLOAD_ACTION_DELETE_FILE -> { + if (click !is DownloadDeleteEvent) return + val id = click.items.firstOrNull()?.id ?: return activity?.let { ctx -> val builder: AlertDialog.Builder = AlertDialog.Builder(ctx) val dialogClickListener = DialogInterface.OnClickListener { _, which -> when (which) { DialogInterface.BUTTON_POSITIVE -> { - VideoDownloadManager.deleteFileAndUpdateSettings(ctx, id) + VideoDownloadManager.deleteFilesAndUpdateSettings(ctx, listOf(id), MainScope()) } DialogInterface.BUTTON_NEGATIVE -> { } @@ -41,9 +43,9 @@ object DownloadButtonSetup { .setMessage( ctx.getString(R.string.delete_message).format( ctx.getNameFull( - click.data.name, - click.data.episode, - click.data.season + click.items.firstOrNull()?.name, + click.items.firstOrNull()?.episode, + click.items.firstOrNull()?.season ) ) ) @@ -57,15 +59,17 @@ object DownloadButtonSetup { } } DOWNLOAD_ACTION_PAUSE_DOWNLOAD -> { + val id = click.data?.id ?: return VideoDownloadManager.downloadEvent.invoke( - Pair(click.data.id, VideoDownloadManager.DownloadActionType.Pause) + Pair(id, VideoDownloadManager.DownloadActionType.Pause) ) } DOWNLOAD_ACTION_RESUME_DOWNLOAD -> { + val id = click.data?.id ?: return activity?.let { ctx -> if (VideoDownloadManager.downloadStatus.containsKey(id) && VideoDownloadManager.downloadStatus[id] == VideoDownloadManager.DownloadType.IsPaused) { VideoDownloadManager.downloadEvent.invoke( - Pair(click.data.id, VideoDownloadManager.DownloadActionType.Resume) + Pair(id, VideoDownloadManager.DownloadActionType.Resume) ) } else { val pkg = VideoDownloadManager.getDownloadResumePackage(ctx, id) @@ -73,18 +77,19 @@ object DownloadButtonSetup { VideoDownloadManager.downloadFromResumeUsingWorker(ctx, pkg) } else { VideoDownloadManager.downloadEvent.invoke( - Pair(click.data.id, VideoDownloadManager.DownloadActionType.Resume) + Pair(id, VideoDownloadManager.DownloadActionType.Resume) ) } } } } DOWNLOAD_ACTION_LONG_CLICK -> { + val id = click.data?.id ?: return activity?.let { act -> val length = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( act, - click.data.id + id )?.fileLength ?: 0 if (length > 0) { @@ -95,19 +100,20 @@ object DownloadButtonSetup { } } DOWNLOAD_ACTION_PLAY_FILE -> { + val id = click.data?.id ?: return activity?.let { act -> val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( act, - click.data.id + id ) ?: return val keyInfo = getKey( VideoDownloadManager.KEY_DOWNLOAD_INFO, - click.data.id.toString() + id.toString() ) ?: return val parent = getKey( DOWNLOAD_HEADER_CACHE, - click.data.parentId.toString() + click.data?.parentId.toString() ) ?: return act.navigate( @@ -117,11 +123,11 @@ object DownloadButtonSetup { ExtractorUri( uri = info.path, - id = click.data.id, - parentId = click.data.parentId, + id = id, + parentId = click.data?.parentId, name = act.getString(R.string.downloaded_file), //click.data.name ?: keyInfo.displayName - season = click.data.season, - episode = click.data.episode, + season = click.data?.season, + episode = click.data?.episode, headerName = parent.name, tvType = parent.type, @@ -138,17 +144,47 @@ object DownloadButtonSetup { // keyInfo.basePath, // keyInfo.relativePath, // keyInfo.displayName, - // click.data.parentId, - // click.data.id, + // click.data?.parentId, + // click.data?.id, // headerName ?: "null", // if (click.data.episode <= 0) null else click.data.episode, // click.data.season // ), - // getViewPos(click.data.id)?.position ?: 0 + // getViewPos(click.data?.id)?.position ?: 0 //) ) } } + DOWNLOAD_ACTION_DELETE_MULTIPLE_FILES -> { + activity?.let { ctx -> + if (click !is DownloadDeleteEvent) return + val ids: List = click.items.mapNotNull { it?.id } + .takeIf { it.isNotEmpty() } ?: return@let + val builder: AlertDialog.Builder = AlertDialog.Builder(ctx) + val dialogClickListener = + DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + VideoDownloadManager.deleteFilesAndUpdateSettings(ctx, ids, MainScope()) + } + DialogInterface.BUTTON_NEGATIVE -> { + } + } + } + + try { + builder.setTitle(R.string.delete_files) + .setMessage( + ctx.getString(R.string.delete_multiple_message) + ) + .setPositiveButton(R.string.delete, dialogClickListener) + .setNegativeButton(R.string.cancel, dialogClickListener) + .show().setDefaultFocus() + } catch (e: Exception) { + logError(e) + } + } + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index 03db948c..9d1fbec1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -102,13 +102,17 @@ class DownloadChildFragment : Fragment() { } val adapter = DownloadAdapter( - {}, - { downloadClickEvent -> - handleDownloadClick(downloadClickEvent) - if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { + { actionEvent -> + if (actionEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { + val downloadDeleteEvent = DownloadDeleteEvent( + action = DOWNLOAD_ACTION_DELETE_FILE, + items = listOf(actionEvent.data) + ) + handleDownloadClick(downloadDeleteEvent) setUpDownloadDeleteListener(folder) - } - } + } else handleDownloadClick(actionEvent) + }, + {} ) binding?.downloadChildList?.apply { 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 23d546e1..d464fff3 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 @@ -108,14 +108,18 @@ class DownloadFragment : Fragment() { } val adapter = DownloadAdapter( + { actionEvent -> + if (actionEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { + val downloadDeleteEvent = DownloadDeleteEvent( + action = DOWNLOAD_ACTION_DELETE_FILE, + items = listOf(actionEvent.data) + ) + handleDownloadClick(downloadDeleteEvent) + setUpDownloadDeleteListener() + } else handleDownloadClick(actionEvent) + }, { click -> handleItemClick(click) - }, - { downloadClickEvent -> - handleDownloadClick(downloadClickEvent) - if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { - setUpDownloadDeleteListener() - } } ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index f3cbdaf1..97428b97 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -20,6 +20,7 @@ import androidx.work.WorkManager import com.bumptech.glide.Glide import com.bumptech.glide.load.model.GlideUrl import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.api.Log import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -29,6 +30,7 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -42,6 +44,8 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -1705,7 +1709,28 @@ object VideoDownloadManager { } } - fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { + fun deleteFilesAndUpdateSettings(context: Context, ids: List, scope: CoroutineScope) { + scope.launchSafe(Dispatchers.IO) { + val deleteJobs = ids.map { id -> + async { + id to deleteFileAndUpdateSettings(context, id) + } + } + val results = deleteJobs.awaitAll() + + val failedDeletes = results.filterNot { it.second } + if (failedDeletes.isNotEmpty()) { + failedDeletes.forEach { (id, _) -> + // TODO show a toast if some failed? + Log.e("FileDeletion", "Failed to delete file with ID: $id") + } + } else { + Log.i("FileDeletion", "All files deleted successfully") + } + } + } + + private fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { val success = deleteFile(context, id) if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) return success diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e3f788f..55ab5925 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -300,6 +300,7 @@ E No Episodes found Delete File + Delete Files Delete Cancel Pause @@ -311,6 +312,7 @@ -30 +30 This will permanently delete %s\nAre you sure? + This will permanently delete all of: %s\nAre you sure? %dm\nremaining %s\nremaining Ongoing