From e42fcd628c2f056fc74ae8a5a358d772c2987979 Mon Sep 17 00:00:00 2001 From: LandWarderer2772 Date: Tue, 14 Jul 2026 20:10:27 +0530 Subject: [PATCH] feat(downloads): Add download deletion controls Users can now delete specific downloaded manga from the downloads menu. --- .../futon/download/ui/list/DownloadItemAD.kt | 7 +++- .../download/ui/list/DownloadItemListener.kt | 3 ++ .../download/ui/list/DownloadsActivity.kt | 28 ++++++++++++- .../download/ui/list/DownloadsMenuProvider.kt | 7 +++- .../download/ui/list/DownloadsViewModel.kt | 39 ++++++++++++++++++- .../ui/list/chapters/DownloadChapter.kt | 5 ++- .../ui/list/chapters/DownloadChapterAD.kt | 10 ++++- .../main/res/layout/item_chapter_download.xml | 17 +++++++- app/src/main/res/values/strings.xml | 4 ++ 9 files changed, 109 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadItemAD.kt b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadItemAD.kt index 1fe5514afe..2892e985e0 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadItemAD.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadItemAD.kt @@ -52,7 +52,12 @@ fun downloadItemAD( } } val chaptersAdapter = BaseListAdapter() - .addDelegate(ListItemType.CHAPTER_LIST, downloadChapterAD()) + .addDelegate( + ListItemType.CHAPTER_LIST, + downloadChapterAD { chapter -> + listener.onDeleteChapterClick(item, chapter) + }, + ) binding.recyclerViewChapters.adapter = chaptersAdapter binding.buttonCancel.setOnClickListener(clickListener) diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadItemListener.kt b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadItemListener.kt index 1392e954ea..5364c781fd 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadItemListener.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadItemListener.kt @@ -1,6 +1,7 @@ package io.github.landwarderer.futon.download.ui.list import io.github.landwarderer.futon.core.ui.list.OnListItemClickListener +import io.github.landwarderer.futon.download.ui.list.chapters.DownloadChapter interface DownloadItemListener : OnListItemClickListener { @@ -15,4 +16,6 @@ interface DownloadItemListener : OnListItemClickListener { fun onSkipAllClick(item: DownloadItemModel) fun onExpandClick(item: DownloadItemModel) + + fun onDeleteChapterClick(item: DownloadItemModel, chapter: DownloadChapter) } diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadsActivity.kt b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadsActivity.kt index 440ce18b65..3ff5e6f6a7 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadsActivity.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadsActivity.kt @@ -18,9 +18,12 @@ import io.github.landwarderer.futon.core.nav.router import io.github.landwarderer.futon.core.ui.BaseActivity import io.github.landwarderer.futon.core.ui.list.ListSelectionController import io.github.landwarderer.futon.core.ui.list.RecyclerScrollKeeper +import io.github.landwarderer.futon.core.ui.dialog.buildAlertDialog +import io.github.landwarderer.futon.core.ui.dialog.setCheckbox import io.github.landwarderer.futon.core.ui.util.MenuInvalidator import io.github.landwarderer.futon.core.ui.util.ReversibleActionObserver import io.github.landwarderer.futon.core.util.FileSize +import io.github.landwarderer.futon.download.ui.list.chapters.DownloadChapter import io.github.landwarderer.futon.core.util.ext.observe import io.github.landwarderer.futon.core.util.ext.observeEvent import io.github.landwarderer.futon.databinding.ActivityDownloadsBinding @@ -194,8 +197,18 @@ class DownloadsActivity : BaseActivity(), } R.id.action_remove -> { - viewModel.remove(controller.snapshot()) - mode?.finish() + var deleteFiles = false + buildAlertDialog(this) { + setTitle(R.string.remove_downloads_confirm) + setCheckbox(R.string.delete_downloaded_files, false) { _, isChecked -> + deleteFiles = isChecked + } + setPositiveButton(R.string.delete) { _, _ -> + viewModel.remove(controller.snapshot(), deleteFiles) + mode?.finish() + } + setNegativeButton(android.R.string.cancel, null) + }.show() true } @@ -226,4 +239,15 @@ class DownloadsActivity : BaseActivity(), menu.findItem(R.id.action_remove)?.isVisible = canRemove return super.onPrepareActionMode(controller, mode, menu) } + + override fun onDeleteChapterClick(item: DownloadItemModel, chapter: DownloadChapter) { + buildAlertDialog(this) { + setTitle(R.string.delete_chapter) + setMessage(getString(R.string.delete_chapter_confirm, chapter.name)) + setPositiveButton(R.string.delete) { _, _ -> + viewModel.deleteChapter(item, chapter) + } + setNegativeButton(android.R.string.cancel, null) + }.show() + } } diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadsMenuProvider.kt b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadsMenuProvider.kt index 1bf8f06fad..8f55ce285f 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadsMenuProvider.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadsMenuProvider.kt @@ -8,6 +8,7 @@ import androidx.fragment.app.FragmentActivity import io.github.landwarderer.futon.R import io.github.landwarderer.futon.core.nav.router import io.github.landwarderer.futon.core.ui.dialog.buildAlertDialog +import io.github.landwarderer.futon.core.ui.dialog.setCheckbox class DownloadsMenuProvider( private val activity: FragmentActivity, @@ -49,12 +50,16 @@ class DownloadsMenuProvider( } private fun confirmRemoveCompleted() { + var deleteFiles = false buildAlertDialog(activity, isCentered = true) { setTitle(R.string.remove_completed) setMessage(R.string.remove_completed_downloads_confirm) setIcon(R.drawable.ic_clear_all) + setCheckbox(R.string.delete_downloaded_files, false) { _, isChecked -> + deleteFiles = isChecked + } setNegativeButton(android.R.string.cancel, null) - setPositiveButton(R.string.clear) { _, _ -> viewModel.removeCompleted() } + setPositiveButton(R.string.clear) { _, _ -> viewModel.removeCompleted(deleteFiles) } }.show() } } diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadsViewModel.kt b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadsViewModel.kt index f558a5bb9f..1ce9471ee8 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadsViewModel.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/DownloadsViewModel.kt @@ -18,6 +18,7 @@ import io.github.landwarderer.futon.core.util.ext.MutableEventFlow import io.github.landwarderer.futon.core.util.ext.calculateTimeAgo import io.github.landwarderer.futon.core.util.ext.call import io.github.landwarderer.futon.core.util.ext.isEmpty +import io.github.landwarderer.futon.core.util.ext.printStackTraceDebug import io.github.landwarderer.futon.download.domain.DownloadState import io.github.landwarderer.futon.download.ui.list.chapters.DownloadChapter import io.github.landwarderer.futon.download.ui.worker.DownloadWorker @@ -27,6 +28,7 @@ import io.github.landwarderer.futon.list.ui.model.ListModel import io.github.landwarderer.futon.list.ui.model.LoadingState import io.github.landwarderer.futon.local.data.LocalMangaRepository import io.github.landwarderer.futon.local.data.LocalStorageChanges +import io.github.landwarderer.futon.local.domain.DeleteLocalMangaUseCase import io.github.landwarderer.futon.local.domain.EnforceStorageQuotaUseCase import io.github.landwarderer.futon.local.domain.model.LocalManga import kotlinx.coroutines.Dispatchers @@ -57,6 +59,7 @@ class DownloadsViewModel @Inject constructor( @LocalStorageChanges private val localStorageChanges: MutableSharedFlow, private val localMangaRepository: LocalMangaRepository, private val enforceStorageQuotaUseCase: EnforceStorageQuotaUseCase, + private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase, ) : BaseViewModel() { val storageUsage = MutableStateFlow(null) @@ -165,24 +168,55 @@ class DownloadsViewModel @Inject constructor( onActionDone.call(ReversibleAction(R.string.downloads_resumed, null)) } - fun remove(ids: Set) { + fun remove(ids: Set, deleteFiles: Boolean) { launchJob(Dispatchers.IO) { val snapshot = works.value ?: return@launchJob val uuids = HashSet(ids.size) for (work in snapshot) { if (work.id.mostSignificantBits in ids) { uuids.add(work.id) + if (deleteFiles) { + val manga = work.manga ?: continue + runCatchingCancellable { + deleteLocalMangaUseCase(manga) + }.onFailure { + it.printStackTraceDebug("DownloadsViewModel::remove") + } + } } } workScheduler.delete(uuids) onActionDone.call(ReversibleAction(R.string.downloads_removed, null)) + refreshStorageUsage() } } - fun removeCompleted() { + fun removeCompleted(deleteFiles: Boolean) { launchJob(Dispatchers.IO) { + if (deleteFiles) { + val snapshot = works.value ?: return@launchJob + for (work in snapshot) { + if (work.workState.isFinished) { + val manga = work.manga ?: continue + runCatchingCancellable { + deleteLocalMangaUseCase(manga) + }.onFailure { + it.printStackTraceDebug("DownloadsViewModel::removeCompleted") + } + } + } + } workScheduler.removeCompleted() onActionDone.call(ReversibleAction(R.string.downloads_removed, null)) + refreshStorageUsage() + } + } + + fun deleteChapter(item: DownloadItemModel, chapter: DownloadChapter) { + val manga = item.manga ?: return + launchJob(Dispatchers.IO) { + localMangaRepository.deleteChapters(manga, setOf(chapter.id)) + refreshStorageUsage() } } @@ -322,6 +356,7 @@ class DownloadsViewModel @Inject constructor( return chapters.mapNotNullTo(ArrayList(size)) { if (chapterIds == null || it.id in chapterIds) { DownloadChapter( + id = it.id, number = it.numberString(), name = it.name, isDownloaded = it.id in localChapters, diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/chapters/DownloadChapter.kt b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/chapters/DownloadChapter.kt index c64c6c6bbe..8e7484716d 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/chapters/DownloadChapter.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/chapters/DownloadChapter.kt @@ -4,17 +4,18 @@ import io.github.landwarderer.futon.list.ui.ListModelDiffCallback import io.github.landwarderer.futon.list.ui.model.ListModel data class DownloadChapter( + val id: Long, val number: String?, val name: String, val isDownloaded: Boolean, ) : ListModel { override fun areItemsTheSame(other: ListModel): Boolean { - return other is DownloadChapter && other.name == name + return other is DownloadChapter && other.id == id } override fun getChangePayload(previousState: ListModel): Any? { - return if (previousState is DownloadChapter && previousState.name == name && previousState.number == number) { + return if (previousState is DownloadChapter && previousState.id == id && previousState.number == number) { ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED } else { super.getChangePayload(previousState) diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/chapters/DownloadChapterAD.kt b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/chapters/DownloadChapterAD.kt index 1ff775f247..6845567a82 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/chapters/DownloadChapterAD.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/list/chapters/DownloadChapterAD.kt @@ -2,19 +2,27 @@ package io.github.landwarderer.futon.download.ui.list.chapters import androidx.core.content.ContextCompat import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import androidx.core.view.isVisible import io.github.landwarderer.futon.R import io.github.landwarderer.futon.core.util.ext.drawableEnd import io.github.landwarderer.futon.databinding.ItemChapterDownloadBinding -fun downloadChapterAD() = adapterDelegateViewBinding( +fun downloadChapterAD( + onDeleteClick: (DownloadChapter) -> Unit, +) = adapterDelegateViewBinding( { layoutInflater, parent -> ItemChapterDownloadBinding.inflate(layoutInflater, parent, false) }, ) { val iconDone = ContextCompat.getDrawable(context, R.drawable.ic_check) + binding.buttonDelete.setOnClickListener { + onDeleteClick(item) + } + bind { binding.textViewNumber.text = item.number binding.textViewTitle.text = item.name binding.textViewTitle.drawableEnd = if (item.isDownloaded) iconDone else null + binding.buttonDelete.isVisible = item.isDownloaded } } diff --git a/app/src/main/res/layout/item_chapter_download.xml b/app/src/main/res/layout/item_chapter_download.xml index 35c341cdfe..5f0551010b 100644 --- a/app/src/main/res/layout/item_chapter_download.xml +++ b/app/src/main/res/layout/item_chapter_download.xml @@ -26,10 +26,11 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 94b206d2c4..8f50829191 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -412,6 +412,10 @@ No thanks All active downloads will be cancelled, partially downloaded data will be lost Your downloads history will be permanently deleted. No downloaded files will be affected + Remove selected downloads? + Also delete downloaded files from storage + Delete chapter + Delete chapter \"%1$s\" from local storage? You don\'t have any downloads Downloads have been resumed Downloads have been paused