mirror of
https://github.com/amir1376/ab-download-manager.git
synced 2025-02-20 11:43:24 +08:00
add track deleted files on disk (#304)
This commit is contained in:
parent
fbcb8478e4
commit
ec64772ce8
@ -8,6 +8,7 @@ import com.abdownloadmanager.desktop.ui.Ui
|
||||
import com.abdownloadmanager.desktop.utils.*
|
||||
import com.abdownloadmanager.desktop.utils.singleInstance.*
|
||||
import com.abdownloadmanager.integration.Integration
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import ir.amirab.util.platform.Platform
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okio.Path.Companion.toOkioPath
|
||||
|
@ -39,6 +39,7 @@ import ir.amirab.downloader.utils.OnDuplicateStrategy
|
||||
import com.abdownloadmanager.integration.Integration
|
||||
import com.abdownloadmanager.integration.IntegrationResult
|
||||
import com.abdownloadmanager.resources.*
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import com.abdownloadmanager.utils.category.CategoryManager
|
||||
import com.abdownloadmanager.utils.category.CategorySelectionMode
|
||||
import ir.amirab.downloader.exception.TooManyErrorException
|
||||
|
@ -35,8 +35,11 @@ import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import com.abdownloadmanager.updatechecker.DummyUpdateChecker
|
||||
import com.abdownloadmanager.updatechecker.UpdateChecker
|
||||
import com.abdownloadmanager.utils.DownloadFoldersRegistry
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import com.abdownloadmanager.utils.FileIconProvider
|
||||
import com.abdownloadmanager.utils.FileIconProviderUsingCategoryIcons
|
||||
import com.abdownloadmanager.utils.autoremove.RemovedDownloadsFromDiskTracker
|
||||
import com.abdownloadmanager.utils.category.*
|
||||
import com.abdownloadmanager.utils.compose.IMyIcons
|
||||
import com.abdownloadmanager.utils.proxy.IProxyStorage
|
||||
@ -264,6 +267,11 @@ val appModule = module {
|
||||
}
|
||||
}
|
||||
}
|
||||
single {
|
||||
RemovedDownloadsFromDiskTracker(
|
||||
get(), get(), get(),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.abdownloadmanager.desktop.pages.addDownload
|
||||
|
||||
import com.abdownloadmanager.desktop.utils.*
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import ir.amirab.downloader.connection.DownloaderClient
|
||||
import ir.amirab.downloader.downloaditem.DownloadCredentials
|
||||
import ir.amirab.util.flow.onEachLatest
|
||||
|
@ -4,7 +4,7 @@ import com.abdownloadmanager.desktop.pages.addDownload.AddDownloadComponent
|
||||
import com.abdownloadmanager.desktop.pages.addDownload.DownloadUiChecker
|
||||
import com.abdownloadmanager.desktop.repository.AppRepository
|
||||
import com.abdownloadmanager.desktop.ui.widget.customtable.TableState
|
||||
import com.abdownloadmanager.desktop.utils.DownloadSystem
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.*
|
||||
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
||||
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
||||
import com.abdownloadmanager.resources.Res
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import ir.amirab.downloader.connection.DownloaderClient
|
||||
|
@ -1,11 +1,9 @@
|
||||
package com.abdownloadmanager.desktop.pages.editdownload
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.abdownloadmanager.desktop.utils.*
|
||||
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
||||
import com.abdownloadmanager.desktop.utils.mvi.ContainsScreenState
|
||||
import com.abdownloadmanager.desktop.utils.mvi.SupportsScreenState
|
||||
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import com.abdownloadmanager.utils.FileIconProvider
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import ir.amirab.downloader.connection.DownloaderClient
|
||||
|
@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.abdownloadmanager.desktop.pages.category.CategoryDialogManager
|
||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
||||
import com.abdownloadmanager.resources.Res
|
||||
import com.abdownloadmanager.resources.*
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import com.abdownloadmanager.utils.FileIconProvider
|
||||
import com.abdownloadmanager.utils.category.Category
|
||||
import com.abdownloadmanager.utils.category.CategoryItemWithId
|
||||
@ -39,6 +39,8 @@ import ir.amirab.util.flow.combineStateFlows
|
||||
import ir.amirab.util.flow.mapStateFlow
|
||||
import ir.amirab.util.flow.mapTwoWayStateFlow
|
||||
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
|
||||
import ir.amirab.downloader.downloaditem.contexts.RemovedBy
|
||||
import ir.amirab.downloader.downloaditem.contexts.User
|
||||
import ir.amirab.util.compose.asStringSource
|
||||
import ir.amirab.util.compose.asStringSourceWithARgs
|
||||
import ir.amirab.util.osfileutil.FileUtils
|
||||
@ -495,7 +497,11 @@ class HomeComponent(
|
||||
scope.launch {
|
||||
val selectionList = promptState.downloadList
|
||||
for (id in selectionList) {
|
||||
downloadSystem.removeDownload(id, promptState.alsoDeleteFile)
|
||||
downloadSystem.removeDownload(
|
||||
id = id,
|
||||
alsoRemoveFile = promptState.alsoDeleteFile,
|
||||
context = RemovedBy(User),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -558,6 +564,9 @@ class HomeComponent(
|
||||
title = Res.string.delete.asStringSource(),
|
||||
icon = MyIcons.remove
|
||||
) {
|
||||
item(Res.string.all_missing_files.asStringSource()) {
|
||||
requestDelete(downloadSystem.getListOfDownloadThatMissingFileOrHaveNotProgress().map { it.id })
|
||||
}
|
||||
item(Res.string.all_finished.asStringSource()) {
|
||||
requestDelete(downloadSystem.getFinishedDownloadIds())
|
||||
}
|
||||
|
@ -106,6 +106,21 @@ fun useSparseFileAllocation(appRepository: AppRepository): BooleanConfigurable {
|
||||
)
|
||||
}
|
||||
|
||||
fun trackDeletedFilesOnDisk(appRepository: AppRepository): BooleanConfigurable {
|
||||
return BooleanConfigurable(
|
||||
title = Res.string.settings_track_deleted_files_on_disk.asStringSource(),
|
||||
description = Res.string.settings_track_deleted_files_on_disk_description.asStringSource(),
|
||||
backedBy = appRepository.trackDeletedFilesOnDisk,
|
||||
describe = {
|
||||
if (it) {
|
||||
Res.string.enabled.asStringSource()
|
||||
} else {
|
||||
Res.string.disabled.asStringSource()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun showDownloadFinishWindow(settingsStorage: AppSettingsStorage): BooleanConfigurable {
|
||||
return BooleanConfigurable(
|
||||
title = Res.string.settings_show_completion_dialog.asStringSource(),
|
||||
@ -403,6 +418,7 @@ class SettingsComponent(
|
||||
showDownloadFinishWindow(appSettings),
|
||||
useServerLastModified(appRepository),
|
||||
useSparseFileAllocation(appRepository),
|
||||
trackDeletedFilesOnDisk(appRepository),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import com.abdownloadmanager.desktop.pages.settings.configurable.BooleanConfigur
|
||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
||||
import com.abdownloadmanager.desktop.storage.PageStatesStorage
|
||||
import com.abdownloadmanager.resources.Res
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import com.abdownloadmanager.utils.FileIconProvider
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import ir.amirab.downloader.DownloadManagerEvents
|
||||
@ -22,7 +23,6 @@ import ir.amirab.util.compose.StringSource
|
||||
import ir.amirab.util.compose.asStringSource
|
||||
import ir.amirab.util.compose.asStringSourceWithARgs
|
||||
import ir.amirab.util.flow.combineStateFlows
|
||||
import ir.amirab.util.flow.mapStateFlow
|
||||
import ir.amirab.util.flow.mapTwoWayStateFlow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
@ -79,7 +79,7 @@ class SingleDownloadComponent(
|
||||
init {
|
||||
downloadMonitor
|
||||
.downloadListFlow
|
||||
.conflate()
|
||||
// .conflate()
|
||||
.onEach {
|
||||
val item = it.firstOrNull { it.id == downloadId }
|
||||
val previous = itemStateFlow.value
|
||||
|
@ -3,7 +3,7 @@ package com.abdownloadmanager.desktop.pages.updater
|
||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
||||
import com.abdownloadmanager.desktop.utils.AppVersion
|
||||
import com.abdownloadmanager.desktop.utils.BaseComponent
|
||||
import com.abdownloadmanager.desktop.utils.DownloadSystem
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
|
@ -2,10 +2,11 @@ package com.abdownloadmanager.desktop.repository
|
||||
|
||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
||||
import com.abdownloadmanager.desktop.utils.AutoStartManager
|
||||
import com.abdownloadmanager.desktop.utils.DownloadSystem
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import ir.amirab.downloader.DownloadSettings
|
||||
import com.abdownloadmanager.integration.Integration
|
||||
import com.abdownloadmanager.integration.IntegrationResult
|
||||
import com.abdownloadmanager.utils.autoremove.RemovedDownloadsFromDiskTracker
|
||||
import com.abdownloadmanager.utils.proxy.ProxyManager
|
||||
import ir.amirab.downloader.DownloadManager
|
||||
import ir.amirab.downloader.monitor.IDownloadMonitor
|
||||
@ -24,11 +25,12 @@ class AppRepository : KoinComponent {
|
||||
val theme = appSettings.theme
|
||||
|
||||
val uiScale = appSettings.uiScale
|
||||
private val downloadSystem : DownloadSystem by inject()
|
||||
private val downloadSystem: DownloadSystem by inject()
|
||||
private val downloadSettings: DownloadSettings by inject()
|
||||
private val downloadManager: DownloadManager = downloadSystem.downloadManager
|
||||
private val downloadMonitor: IDownloadMonitor = downloadSystem.downloadMonitor
|
||||
private val integration: Integration by inject()
|
||||
private val removedDownloadsFromDiskTracker: RemovedDownloadsFromDiskTracker by inject()
|
||||
|
||||
val speedLimiter = appSettings.speedLimit
|
||||
val threadCount = appSettings.threadCount
|
||||
@ -39,7 +41,7 @@ class AppRepository : KoinComponent {
|
||||
val saveLocation = appSettings.defaultDownloadFolder
|
||||
val integrationEnabled = appSettings.browserIntegrationEnabled
|
||||
val integrationPort = appSettings.browserIntegrationPort
|
||||
|
||||
val trackDeletedFilesOnDisk = appSettings.trackDeletedFilesOnDisk
|
||||
|
||||
init {
|
||||
//maybe its better to move this to another place
|
||||
@ -105,6 +107,16 @@ class AppRepository : KoinComponent {
|
||||
integrationEnabled.update { false }
|
||||
}
|
||||
}.launchIn(scope)
|
||||
trackDeletedFilesOnDisk
|
||||
.debounce(500)
|
||||
.onEach { enabled ->
|
||||
if (enabled) {
|
||||
removedDownloadsFromDiskTracker.removeDownloadsThatFilesAreMissing()
|
||||
removedDownloadsFromDiskTracker.start()
|
||||
} else {
|
||||
removedDownloadsFromDiskTracker.stop()
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
}
|
@ -33,6 +33,7 @@ data class AppSettingsModel(
|
||||
.canonicalFile.absolutePath,
|
||||
val browserIntegrationEnabled: Boolean = true,
|
||||
val browserIntegrationPort: Int = 15151,
|
||||
val trackDeletedFilesOnDisk: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
val default: AppSettingsModel get() = AppSettingsModel()
|
||||
@ -57,6 +58,7 @@ data class AppSettingsModel(
|
||||
val defaultDownloadFolder = stringKeyOf("defaultDownloadFolder")
|
||||
val browserIntegrationEnabled = booleanKeyOf("browserIntegrationEnabled")
|
||||
val browserIntegrationPort = intKeyOf("browserIntegrationPort")
|
||||
val trackDeletedFilesOnDisk = booleanKeyOf("trackDeletedFilesOnDisk")
|
||||
}
|
||||
|
||||
|
||||
@ -84,6 +86,7 @@ data class AppSettingsModel(
|
||||
browserIntegrationEnabled = source.get(Keys.browserIntegrationEnabled)
|
||||
?: default.browserIntegrationEnabled,
|
||||
browserIntegrationPort = source.get(Keys.browserIntegrationPort) ?: default.browserIntegrationPort,
|
||||
trackDeletedFilesOnDisk = source.get(Keys.trackDeletedFilesOnDisk) ?: default.trackDeletedFilesOnDisk,
|
||||
)
|
||||
}
|
||||
|
||||
@ -106,6 +109,7 @@ data class AppSettingsModel(
|
||||
put(Keys.defaultDownloadFolder, focus.defaultDownloadFolder)
|
||||
put(Keys.browserIntegrationEnabled, focus.browserIntegrationEnabled)
|
||||
put(Keys.browserIntegrationPort, focus.browserIntegrationPort)
|
||||
put(Keys.trackDeletedFilesOnDisk, focus.trackDeletedFilesOnDisk)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -143,4 +147,5 @@ class AppSettingsStorage(
|
||||
val defaultDownloadFolder = from(AppSettingsModel.defaultDownloadFolder)
|
||||
val browserIntegrationEnabled = from(AppSettingsModel.browserIntegrationEnabled)
|
||||
val browserIntegrationPort = from(AppSettingsModel.browserIntegrationPort)
|
||||
val trackDeletedFilesOnDisk = from(AppSettingsModel.trackDeletedFilesOnDisk)
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package com.abdownloadmanager.desktop.utils
|
||||
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import ir.amirab.util.osfileutil.FileUtils
|
||||
import ir.amirab.util.flow.mapStateFlow
|
||||
import com.abdownloadmanager.utils.isValidUrl
|
||||
|
@ -6,6 +6,7 @@ import ir.amirab.downloader.downloaditem.DownloadStatus
|
||||
import ir.amirab.downloader.utils.intervalFlow
|
||||
import ir.amirab.util.flow.saved
|
||||
import ir.amirab.downloader.DownloadManager
|
||||
import ir.amirab.util.flow.combineStateFlows
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@ -37,8 +38,8 @@ class DownloadMonitor(
|
||||
}
|
||||
override val activeDownloadListFlow = MutableStateFlow<List<ProcessingDownloadItemState>>(emptyList())
|
||||
override val completedDownloadListFlow = MutableStateFlow<List<CompletedDownloadItemState>>(emptyList())
|
||||
override val downloadListFlow: Flow<List<IDownloadItemState>> =
|
||||
combine(activeDownloadListFlow, completedDownloadListFlow) { a, b -> a + b }
|
||||
override val downloadListFlow: StateFlow<List<IDownloadItemState>> =
|
||||
combineStateFlows(activeDownloadListFlow, completedDownloadListFlow) { a, b -> a + b }
|
||||
|
||||
init {
|
||||
activeDownloadListFlow
|
||||
|
@ -1,6 +1,7 @@
|
||||
package ir.amirab.downloader.monitor
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import java.io.File
|
||||
|
||||
@Immutable
|
||||
sealed interface IDownloadItemState {
|
||||
@ -13,4 +14,6 @@ sealed interface IDownloadItemState {
|
||||
val startTime: Long
|
||||
val completeTime: Long
|
||||
val downloadLink:String
|
||||
|
||||
fun getFullPath() = File(folder, name)
|
||||
}
|
@ -8,9 +8,9 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface IDownloadMonitor {
|
||||
var useAverageSpeed: Boolean
|
||||
val activeDownloadListFlow: MutableStateFlow<List<ProcessingDownloadItemState>>
|
||||
val completedDownloadListFlow: MutableStateFlow<List<CompletedDownloadItemState>>
|
||||
val downloadListFlow: Flow<List<IDownloadItemState>>
|
||||
val activeDownloadListFlow: StateFlow<List<ProcessingDownloadItemState>>
|
||||
val completedDownloadListFlow: StateFlow<List<CompletedDownloadItemState>>
|
||||
val downloadListFlow: StateFlow<List<IDownloadItemState>>
|
||||
val activeDownloadCount: StateFlow<Int>
|
||||
|
||||
suspend fun waitForDownloadToFinishOrCancel(
|
||||
|
@ -25,6 +25,7 @@ data class ProcessingDownloadItemState(
|
||||
val progress = parts.sumOf {
|
||||
it.howMuchProceed
|
||||
}
|
||||
val hasProgress get() = progress > 0
|
||||
val gotAnyProgress= progress > 0L
|
||||
val percent: Int? = if (contentLength == DownloadItem.LENGTH_UNKNOWN) {
|
||||
null
|
||||
|
@ -25,6 +25,7 @@ composeReorderable = "0.9.6"
|
||||
semver = "2.0.0"
|
||||
jgit = "6.9.0.202403050737-r"
|
||||
osThemeDetector = "3.9.1"
|
||||
kotlinFileWatcher = "1.3.0"
|
||||
|
||||
[libraries]
|
||||
|
||||
@ -119,6 +120,8 @@ osThemeDetector = { module = "com.github.Dansoftowner:jSystemThemeDetector", ver
|
||||
|
||||
handlebarsJava = "com.github.jknack:handlebars:4.4.0"
|
||||
|
||||
kotlinFileWatcher = { module = "io.github.irgaly.kfswatch:kfswatch", version.ref = "kotlinFileWatcher" }
|
||||
|
||||
[plugins]
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
compose = { id = "org.jetbrains.compose", version.ref = "compose" }
|
||||
|
@ -12,6 +12,7 @@ dependencies {
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.kotlin.coroutines.core)
|
||||
implementation(libs.kotlin.serialization.json)
|
||||
implementation(libs.kotlinFileWatcher)
|
||||
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.abdownloadmanager.desktop.utils
|
||||
package com.abdownloadmanager.utils
|
||||
|
||||
import java.io.File
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.abdownloadmanager.desktop.utils
|
||||
package com.abdownloadmanager.utils
|
||||
|
||||
import com.abdownloadmanager.utils.category.CategoryItemWithId
|
||||
import com.abdownloadmanager.utils.category.CategoryManager
|
||||
@ -6,11 +6,12 @@ import com.abdownloadmanager.utils.category.CategorySelectionMode
|
||||
import ir.amirab.downloader.DownloadManager
|
||||
import ir.amirab.downloader.db.IDownloadListDb
|
||||
import ir.amirab.downloader.downloaditem.*
|
||||
import ir.amirab.downloader.downloaditem.contexts.RemovedBy
|
||||
import ir.amirab.downloader.downloaditem.contexts.ResumedBy
|
||||
import ir.amirab.downloader.downloaditem.contexts.StoppedBy
|
||||
import ir.amirab.downloader.downloaditem.contexts.User
|
||||
import ir.amirab.downloader.monitor.IDownloadItemState
|
||||
import ir.amirab.downloader.monitor.IDownloadMonitor
|
||||
import ir.amirab.downloader.monitor.ProcessingDownloadItemState
|
||||
import ir.amirab.downloader.monitor.isDownloadActiveFlow
|
||||
import ir.amirab.downloader.queue.QueueManager
|
||||
import ir.amirab.downloader.utils.OnDuplicateStrategy
|
||||
@ -108,7 +109,11 @@ class DownloadSystem(
|
||||
return downloadId
|
||||
}
|
||||
|
||||
suspend fun removeDownload(id: Long, alsoRemoveFile: Boolean) {
|
||||
suspend fun removeDownload(
|
||||
id: Long,
|
||||
alsoRemoveFile: Boolean,
|
||||
context: DownloadItemContext,
|
||||
) {
|
||||
downloadManager.deleteDownload(id, {
|
||||
if (it.status == DownloadStatus.Completed) {
|
||||
alsoRemoveFile
|
||||
@ -116,7 +121,7 @@ class DownloadSystem(
|
||||
// always remove file if download is not finished!
|
||||
true
|
||||
}
|
||||
}, RemovedBy(User))
|
||||
}, context)
|
||||
categoryManager.removeItemInCategories(listOf(id))
|
||||
}
|
||||
|
||||
@ -201,6 +206,12 @@ class DownloadSystem(
|
||||
return downloadManager.calculateOutputFile(downloadItem)
|
||||
}
|
||||
|
||||
fun getDownloadItemByPath(path: String): IDownloadItemState? {
|
||||
return downloadMonitor.downloadListFlow.value.find {
|
||||
it.getFullPath().path == path
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun getFilePathById(id: Long): File? {
|
||||
val item = getDownloadItemById(id) ?: return null
|
||||
@ -229,6 +240,24 @@ class DownloadSystem(
|
||||
}
|
||||
}
|
||||
|
||||
fun isDownloadMissingFileOrHaveNotProgress(downloadItem: IDownloadItemState): Boolean {
|
||||
val missingFileBypass = if (downloadItem is ProcessingDownloadItemState) {
|
||||
// some downloads not started yet so there is no file belong to them, so we shouldn't remove them
|
||||
downloadItem.hasProgress
|
||||
} else {
|
||||
// finished downloads can be removed
|
||||
true
|
||||
}
|
||||
return missingFileBypass && !downloadItem.getFullPath().exists()
|
||||
}
|
||||
|
||||
fun getListOfDownloadThatMissingFileOrHaveNotProgress(): List<IDownloadItemState> {
|
||||
val downloads = downloadMonitor.downloadListFlow.value
|
||||
return downloads.filter {
|
||||
isDownloadMissingFileOrHaveNotProgress(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllRegisteredDownloadFiles(): List<File> {
|
||||
return downloadMonitor.run {
|
||||
activeDownloadListFlow.value + completedDownloadListFlow.value
|
@ -0,0 +1,153 @@
|
||||
package com.abdownloadmanager.utils.autoremove
|
||||
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import io.github.irgaly.kfswatch.KfsDirectoryWatcher
|
||||
import io.github.irgaly.kfswatch.KfsEvent
|
||||
import ir.amirab.downloader.downloaditem.contexts.CanPerformRemove
|
||||
import ir.amirab.downloader.downloaditem.contexts.RemovedBy
|
||||
import ir.amirab.downloader.monitor.*
|
||||
import ir.amirab.util.flow.withPrevious
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.io.File
|
||||
|
||||
|
||||
class RemovedDownloadsFromDiskTracker(
|
||||
private val downloadMonitor: IDownloadMonitor,
|
||||
private val scope: CoroutineScope,
|
||||
private val downloadSystem: DownloadSystem,
|
||||
) {
|
||||
private fun createWatcher(
|
||||
scope: CoroutineScope,
|
||||
): KfsDirectoryWatcher {
|
||||
return KfsDirectoryWatcher(
|
||||
scope = scope,
|
||||
)
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var stopped = true
|
||||
|
||||
//download item ids that should be checked for existence after a delay
|
||||
private var itemsToCheck = MutableStateFlow(emptySet<Long>())
|
||||
private var activeJob: Job? = null
|
||||
|
||||
fun start() {
|
||||
stopped = false
|
||||
activeJob = scope.launch {
|
||||
val watcher = createWatcher(this)
|
||||
watcher
|
||||
.onEventFlow
|
||||
.filter { it.event == KfsEvent.Delete }
|
||||
.onEach {
|
||||
val fullPath = File(it.targetDirectory, it.path).path
|
||||
onPathRemoved(fullPath)
|
||||
}
|
||||
.launchIn(this)
|
||||
downloadMonitor.downloadListFlow
|
||||
.map { it.map { it.folder }.distinct() }
|
||||
.distinctUntilChanged()
|
||||
.changes()
|
||||
.onEach { changes ->
|
||||
val groups = changes
|
||||
.groupBy { it.second }
|
||||
groups[Change.Removed]
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.map { it.first }?.toTypedArray()?.let {
|
||||
watcher.remove(*it)
|
||||
}
|
||||
groups[Change.Added]
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.map { it.first }?.toTypedArray()?.let {
|
||||
watcher.add(*it)
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
itemsToCheck
|
||||
.debounce(500)
|
||||
.filter { it.isNotEmpty() }
|
||||
.onEach { downloadItems ->
|
||||
checkAndRemoveThisItems(downloadItems)
|
||||
itemsToCheck.update { it.subtract(downloadItems) }
|
||||
}.launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stop() {
|
||||
activeJob?.cancelAndJoin()
|
||||
activeJob = null
|
||||
itemsToCheck.update { emptySet() }
|
||||
stopped = true
|
||||
}
|
||||
|
||||
suspend fun removeDownloadsThatFilesAreMissing() {
|
||||
checkAndRemoveThisItems(
|
||||
downloadSystem.getListOfDownloadThatMissingFileOrHaveNotProgress()
|
||||
.map { it.id }
|
||||
.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun checkAndRemoveThisItems(ids: Set<Long>) {
|
||||
for (id in ids) {
|
||||
val downloadItem = downloadSystem.getDownloadItemById(id) ?: continue
|
||||
val file = downloadSystem.getDownloadFile(downloadItem)
|
||||
if (!file.exists()) {
|
||||
downloadSystem.removeDownload(
|
||||
id = downloadItem.id,
|
||||
alsoRemoveFile = false, // it is already deleted!
|
||||
context = RemovedBy(AutoRemoveOption)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* find the corespounding download and schedule for remove that
|
||||
* I will add a delay for that (maybe it's a temporary file remove for example when renaming download item)
|
||||
*/
|
||||
private fun onPathRemoved(path: String) {
|
||||
if (stopped) return
|
||||
val item = downloadSystem.getDownloadItemByPath(path) ?: return
|
||||
itemsToCheck.update { it.plus(item.id) }
|
||||
}
|
||||
}
|
||||
|
||||
private sealed interface Change {
|
||||
data object Added : Change
|
||||
data object Removed : Change
|
||||
data object NotChange : Change
|
||||
}
|
||||
|
||||
|
||||
private fun <T> Flow<List<T>>.changes(): Flow<List<Pair<T, Change>>> {
|
||||
return withPrevious { previous, current ->
|
||||
if (previous == null) {
|
||||
current.map { it to Change.Added }
|
||||
} else {
|
||||
diffOf(previous, current)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> diffOf(
|
||||
a: Collection<T>, b: Collection<T>,
|
||||
): List<Pair<T, Change>> {
|
||||
val output = ArrayList<Pair<T, Change>>(maxOf(a.size, b.size))
|
||||
val aSet = a.toSet()
|
||||
val remainingBItems = b.toMutableSet()
|
||||
// find removed items in b
|
||||
for (i in aSet) {
|
||||
if (i in remainingBItems) {
|
||||
output.add(i to Change.NotChange)
|
||||
remainingBItems.remove(i)
|
||||
} else {
|
||||
output.add(i to Change.Removed)
|
||||
}
|
||||
}
|
||||
// remaining b's are added!
|
||||
output.addAll(remainingBItems.map { it to Change.Added })
|
||||
return output
|
||||
}
|
||||
|
||||
data object AutoRemoveOption : CanPerformRemove
|
@ -57,6 +57,7 @@ tasks=Tasks
|
||||
tools=Tools
|
||||
help=Help
|
||||
system=System
|
||||
all_missing_files=All Missing Files
|
||||
all_finished=All Finished
|
||||
all_unfinished=All Unfinished
|
||||
entire_list=Entire List
|
||||
@ -193,6 +194,8 @@ settings_use_proxy_description=Use proxy for downloading files
|
||||
settings_use_proxy_describe_no_proxy=No Proxy will be used
|
||||
settings_use_proxy_describe_system_proxy=System Proxy will be used
|
||||
settings_use_proxy_describe_manual_proxy="{{value}}" will be used
|
||||
settings_track_deleted_files_on_disk=Track Deleted Files On Disk
|
||||
settings_track_deleted_files_on_disk_description=Automatically remove files from the list when they are deleted or moved from the download directory.
|
||||
settings_theme=Theme
|
||||
settings_theme_description=Select a theme for the App
|
||||
settings_ui_scale=UI Scale
|
||||
|
@ -175,7 +175,7 @@ fun <T> Flow<T>.saved(count: Int): Flow<List<T>> {
|
||||
require(count >= 0)
|
||||
return when (count) {
|
||||
0 -> emptyFlow()
|
||||
else -> scan(
|
||||
else -> scan<T, List<T>>(
|
||||
listOf()
|
||||
) { l, v ->
|
||||
if (l.size < count) {
|
||||
@ -183,7 +183,7 @@ fun <T> Flow<T>.saved(count: Int): Flow<List<T>> {
|
||||
} else {
|
||||
l.drop(1).plus(v)
|
||||
}
|
||||
}
|
||||
}.drop(1) // scan emits an initial value (emptyList)
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,4 +225,16 @@ fun <T> Flow<T>.chunked(count: Int): Flow<List<T>> = flow {
|
||||
fun <T> Flow<T>.onEachLatest(block:suspend (T)->Unit) = transformLatest {
|
||||
block(it)
|
||||
emit(it)
|
||||
}
|
||||
|
||||
fun <T, R> Flow<T>.withPrevious(
|
||||
transform: (previous: T?, current: T) -> R,
|
||||
): Flow<R> {
|
||||
return saved(2)
|
||||
.pad(2, false)
|
||||
.map {
|
||||
val previous = it[0]
|
||||
val current = it[1] as T
|
||||
transform(previous, current)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user