add track deleted files on disk (#304)

This commit is contained in:
AmirHossein Abdolmotallebi 2024-12-15 18:30:20 +03:30 committed by GitHub
parent fbcb8478e4
commit ec64772ce8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 283 additions and 24 deletions

View File

@ -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

View File

@ -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

View File

@ -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(),
)
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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())
}

View File

@ -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),
)
}
}

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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(

View File

@ -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

View File

@ -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" }

View File

@ -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)

View File

@ -1,4 +1,4 @@
package com.abdownloadmanager.desktop.utils
package com.abdownloadmanager.utils
import java.io.File

View 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

View File

@ -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

View File

@ -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

View File

@ -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)
}
}