Initial support for multi deleting downloads

This is probably a horrible way to do this, I just am not sure of the best way to go about this to be honest
This commit is contained in:
Luna712 2024-07-04 17:43:55 -06:00 committed by GitHub
parent c1b5f5c128
commit 7b1f59fdb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 125 additions and 43 deletions

View file

@ -27,6 +27,7 @@ const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2
const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3 const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3
const val DOWNLOAD_ACTION_DOWNLOAD = 4 const val DOWNLOAD_ACTION_DOWNLOAD = 4
const val DOWNLOAD_ACTION_LONG_CLICK = 5 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_GO_TO_CHILD = 0
const val DOWNLOAD_ACTION_LOAD_RESULT = 1 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( data class VisualDownloadChildCached(
override val currentBytes: Long, override val currentBytes: Long,
override val totalBytes: Long, override val totalBytes: Long,
@ -73,9 +79,14 @@ data class VisualDownloadHeaderCached(
): VisualDownloadCached(currentBytes, totalBytes, data) ): VisualDownloadCached(currentBytes, totalBytes, data)
data class DownloadClickEvent( data class DownloadClickEvent(
val action: Int, override val action: Int,
val data: VideoDownloadHelper.DownloadEpisodeCached override val data: VideoDownloadHelper.DownloadEpisodeCached
) ): DownloadActionEventBase(action, data)
data class DownloadDeleteEvent(
override val action: Int,
val items: List<VideoDownloadHelper.DownloadEpisodeCached?>
): DownloadActionEventBase(action, null)
data class DownloadHeaderClickEvent( data class DownloadHeaderClickEvent(
val action: Int, val action: Int,
@ -83,8 +94,8 @@ data class DownloadHeaderClickEvent(
) )
class DownloadAdapter( class DownloadAdapter(
private val actionCallback: (DownloadActionEventBase) -> Unit,
private val clickCallback: (DownloadHeaderClickEvent) -> Unit, private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
private val mediaClickCallback: (DownloadClickEvent) -> Unit,
) : ListAdapter<VisualDownloadCached, DownloadAdapter.DownloadViewHolder>(DiffCallback()) { ) : ListAdapter<VisualDownloadCached, DownloadAdapter.DownloadViewHolder>(DiffCallback()) {
companion object { companion object {
@ -94,8 +105,8 @@ class DownloadAdapter(
inner class DownloadViewHolder( inner class DownloadViewHolder(
private val binding: ViewBinding, private val binding: ViewBinding,
private val actionCallback: (DownloadActionEventBase) -> Unit,
private val clickCallback: (DownloadHeaderClickEvent) -> Unit, private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
private val mediaClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(card: VisualDownloadCached?) { fun bind(card: VisualDownloadCached?) {
@ -145,11 +156,11 @@ class DownloadAdapter(
ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable)
} }
downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback) downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, actionCallback)
downloadButton.isVisible = true downloadButton.isVisible = true
episodeHolder.setOnClickListener { episodeHolder.setOnClickListener {
mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child)) actionCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child))
} }
} else { } else {
downloadButton.isVisible = false downloadButton.isVisible = false
@ -214,7 +225,7 @@ class DownloadAdapter(
ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable)
} }
downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, mediaClickCallback) downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, actionCallback)
downloadButton.isVisible = true downloadButton.isVisible = true
downloadChildEpisodeText.apply { downloadChildEpisodeText.apply {
@ -223,7 +234,7 @@ class DownloadAdapter(
} }
downloadChildEpisodeHolder.setOnClickListener { 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) VIEW_TYPE_CHILD -> DownloadChildEpisodeBinding.inflate(inflater, parent, false)
else -> throw IllegalArgumentException("Invalid view type") else -> throw IllegalArgumentException("Invalid view type")
} }
return DownloadViewHolder(binding, clickCallback, mediaClickCallback) return DownloadViewHolder(binding, actionCallback, clickCallback)
} }
override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) { override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) {

View file

@ -17,19 +17,21 @@ import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
import kotlinx.coroutines.MainScope
object DownloadButtonSetup { object DownloadButtonSetup {
fun handleDownloadClick(click: DownloadClickEvent) { fun handleDownloadClick(click: DownloadActionEventBase) {
val id = click.data.id
when (click.action) { when (click.action) {
DOWNLOAD_ACTION_DELETE_FILE -> { DOWNLOAD_ACTION_DELETE_FILE -> {
if (click !is DownloadDeleteEvent) return
val id = click.items.firstOrNull()?.id ?: return
activity?.let { ctx -> activity?.let { ctx ->
val builder: AlertDialog.Builder = AlertDialog.Builder(ctx) val builder: AlertDialog.Builder = AlertDialog.Builder(ctx)
val dialogClickListener = val dialogClickListener =
DialogInterface.OnClickListener { _, which -> DialogInterface.OnClickListener { _, which ->
when (which) { when (which) {
DialogInterface.BUTTON_POSITIVE -> { DialogInterface.BUTTON_POSITIVE -> {
VideoDownloadManager.deleteFileAndUpdateSettings(ctx, id) VideoDownloadManager.deleteFilesAndUpdateSettings(ctx, listOf(id), MainScope())
} }
DialogInterface.BUTTON_NEGATIVE -> { DialogInterface.BUTTON_NEGATIVE -> {
} }
@ -41,9 +43,9 @@ object DownloadButtonSetup {
.setMessage( .setMessage(
ctx.getString(R.string.delete_message).format( ctx.getString(R.string.delete_message).format(
ctx.getNameFull( ctx.getNameFull(
click.data.name, click.items.firstOrNull()?.name,
click.data.episode, click.items.firstOrNull()?.episode,
click.data.season click.items.firstOrNull()?.season
) )
) )
) )
@ -57,15 +59,17 @@ object DownloadButtonSetup {
} }
} }
DOWNLOAD_ACTION_PAUSE_DOWNLOAD -> { DOWNLOAD_ACTION_PAUSE_DOWNLOAD -> {
val id = click.data?.id ?: return
VideoDownloadManager.downloadEvent.invoke( VideoDownloadManager.downloadEvent.invoke(
Pair(click.data.id, VideoDownloadManager.DownloadActionType.Pause) Pair(id, VideoDownloadManager.DownloadActionType.Pause)
) )
} }
DOWNLOAD_ACTION_RESUME_DOWNLOAD -> { DOWNLOAD_ACTION_RESUME_DOWNLOAD -> {
val id = click.data?.id ?: return
activity?.let { ctx -> activity?.let { ctx ->
if (VideoDownloadManager.downloadStatus.containsKey(id) && VideoDownloadManager.downloadStatus[id] == VideoDownloadManager.DownloadType.IsPaused) { if (VideoDownloadManager.downloadStatus.containsKey(id) && VideoDownloadManager.downloadStatus[id] == VideoDownloadManager.DownloadType.IsPaused) {
VideoDownloadManager.downloadEvent.invoke( VideoDownloadManager.downloadEvent.invoke(
Pair(click.data.id, VideoDownloadManager.DownloadActionType.Resume) Pair(id, VideoDownloadManager.DownloadActionType.Resume)
) )
} else { } else {
val pkg = VideoDownloadManager.getDownloadResumePackage(ctx, id) val pkg = VideoDownloadManager.getDownloadResumePackage(ctx, id)
@ -73,18 +77,19 @@ object DownloadButtonSetup {
VideoDownloadManager.downloadFromResumeUsingWorker(ctx, pkg) VideoDownloadManager.downloadFromResumeUsingWorker(ctx, pkg)
} else { } else {
VideoDownloadManager.downloadEvent.invoke( VideoDownloadManager.downloadEvent.invoke(
Pair(click.data.id, VideoDownloadManager.DownloadActionType.Resume) Pair(id, VideoDownloadManager.DownloadActionType.Resume)
) )
} }
} }
} }
} }
DOWNLOAD_ACTION_LONG_CLICK -> { DOWNLOAD_ACTION_LONG_CLICK -> {
val id = click.data?.id ?: return
activity?.let { act -> activity?.let { act ->
val length = val length =
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
act, act,
click.data.id id
)?.fileLength )?.fileLength
?: 0 ?: 0
if (length > 0) { if (length > 0) {
@ -95,19 +100,20 @@ object DownloadButtonSetup {
} }
} }
DOWNLOAD_ACTION_PLAY_FILE -> { DOWNLOAD_ACTION_PLAY_FILE -> {
val id = click.data?.id ?: return
activity?.let { act -> activity?.let { act ->
val info = val info =
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
act, act,
click.data.id id
) ?: return ) ?: return
val keyInfo = getKey<VideoDownloadManager.DownloadedFileInfo>( val keyInfo = getKey<VideoDownloadManager.DownloadedFileInfo>(
VideoDownloadManager.KEY_DOWNLOAD_INFO, VideoDownloadManager.KEY_DOWNLOAD_INFO,
click.data.id.toString() id.toString()
) ?: return ) ?: return
val parent = getKey<VideoDownloadHelper.DownloadHeaderCached>( val parent = getKey<VideoDownloadHelper.DownloadHeaderCached>(
DOWNLOAD_HEADER_CACHE, DOWNLOAD_HEADER_CACHE,
click.data.parentId.toString() click.data?.parentId.toString()
) ?: return ) ?: return
act.navigate( act.navigate(
@ -117,11 +123,11 @@ object DownloadButtonSetup {
ExtractorUri( ExtractorUri(
uri = info.path, uri = info.path,
id = click.data.id, id = id,
parentId = click.data.parentId, parentId = click.data?.parentId,
name = act.getString(R.string.downloaded_file), //click.data.name ?: keyInfo.displayName name = act.getString(R.string.downloaded_file), //click.data.name ?: keyInfo.displayName
season = click.data.season, season = click.data?.season,
episode = click.data.episode, episode = click.data?.episode,
headerName = parent.name, headerName = parent.name,
tvType = parent.type, tvType = parent.type,
@ -138,17 +144,47 @@ object DownloadButtonSetup {
// keyInfo.basePath, // keyInfo.basePath,
// keyInfo.relativePath, // keyInfo.relativePath,
// keyInfo.displayName, // keyInfo.displayName,
// click.data.parentId, // click.data?.parentId,
// click.data.id, // click.data?.id,
// headerName ?: "null", // headerName ?: "null",
// if (click.data.episode <= 0) null else click.data.episode, // if (click.data.episode <= 0) null else click.data.episode,
// click.data.season // 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<Int> = 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)
}
}
}
} }
} }
} }

View file

@ -102,13 +102,17 @@ class DownloadChildFragment : Fragment() {
} }
val adapter = DownloadAdapter( val adapter = DownloadAdapter(
{}, { actionEvent ->
{ downloadClickEvent -> if (actionEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
handleDownloadClick(downloadClickEvent) val downloadDeleteEvent = DownloadDeleteEvent(
if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { action = DOWNLOAD_ACTION_DELETE_FILE,
items = listOf(actionEvent.data)
)
handleDownloadClick(downloadDeleteEvent)
setUpDownloadDeleteListener(folder) setUpDownloadDeleteListener(folder)
} } else handleDownloadClick(actionEvent)
} },
{}
) )
binding?.downloadChildList?.apply { binding?.downloadChildList?.apply {

View file

@ -108,14 +108,18 @@ class DownloadFragment : Fragment() {
} }
val adapter = DownloadAdapter( 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 -> { click ->
handleItemClick(click) handleItemClick(click)
},
{ downloadClickEvent ->
handleDownloadClick(downloadClickEvent)
if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
setUpDownloadDeleteListener()
}
} }
) )

View file

@ -20,6 +20,7 @@ import androidx.work.WorkManager
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.api.Log
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey 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.R
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.services.VideoDownloadService
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@ -42,6 +44,8 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
@ -1705,7 +1709,28 @@ object VideoDownloadManager {
} }
} }
fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { fun deleteFilesAndUpdateSettings(context: Context, ids: List<Int>, 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) val success = deleteFile(context, id)
if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString())
return success return success

View file

@ -300,6 +300,7 @@
<string name="episode_short">E</string> <string name="episode_short">E</string>
<string name="no_episodes_found">No Episodes found</string> <string name="no_episodes_found">No Episodes found</string>
<string name="delete_file">Delete File</string> <string name="delete_file">Delete File</string>
<string name="delete_files">Delete Files</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="pause">Pause</string> <string name="pause">Pause</string>
@ -311,6 +312,7 @@
<string name="go_back_30">-30</string> <string name="go_back_30">-30</string>
<string name="go_forward_30">+30</string> <string name="go_forward_30">+30</string>
<string name="delete_message" formatted="true">This will permanently delete %s\nAre you sure?</string> <string name="delete_message" formatted="true">This will permanently delete %s\nAre you sure?</string>
<string name="delete_multiple_message" formatted="true">This will permanently delete all of: %s\nAre you sure?</string>
<string name="resume_time_left" formatted="true">%dm\nremaining</string> <string name="resume_time_left" formatted="true">%dm\nremaining</string>
<string name="resume_remaining" formatted="true">%s\nremaining</string> <string name="resume_remaining" formatted="true">%s\nremaining</string>
<string name="status_ongoing">Ongoing</string> <string name="status_ongoing">Ongoing</string>