mirror of
https://github.com/amir1376/ab-download-manager.git
synced 2025-02-20 11:43:24 +08:00
add file checksum (#362)
This commit is contained in:
parent
6063d8f420
commit
d031450bf4
@ -8,6 +8,8 @@ import com.abdownloadmanager.desktop.pages.batchdownload.BatchDownloadComponent
|
|||||||
import com.abdownloadmanager.desktop.pages.category.CategoryComponent
|
import com.abdownloadmanager.desktop.pages.category.CategoryComponent
|
||||||
import com.abdownloadmanager.desktop.pages.category.CategoryDialogManager
|
import com.abdownloadmanager.desktop.pages.category.CategoryDialogManager
|
||||||
import com.abdownloadmanager.desktop.pages.editdownload.EditDownloadComponent
|
import com.abdownloadmanager.desktop.pages.editdownload.EditDownloadComponent
|
||||||
|
import com.abdownloadmanager.desktop.pages.filehash.FileChecksumComponent
|
||||||
|
import com.abdownloadmanager.desktop.pages.filehash.FileChecksumComponentConfig
|
||||||
import com.abdownloadmanager.desktop.pages.home.HomeComponent
|
import com.abdownloadmanager.desktop.pages.home.HomeComponent
|
||||||
import com.abdownloadmanager.desktop.pages.queue.QueuesComponent
|
import com.abdownloadmanager.desktop.pages.queue.QueuesComponent
|
||||||
import com.abdownloadmanager.desktop.pages.settings.SettingsComponent
|
import com.abdownloadmanager.desktop.pages.settings.SettingsComponent
|
||||||
@ -80,6 +82,7 @@ class AppComponent(
|
|||||||
AddDownloadDialogManager,
|
AddDownloadDialogManager,
|
||||||
CategoryDialogManager,
|
CategoryDialogManager,
|
||||||
EditDownloadDialogManager,
|
EditDownloadDialogManager,
|
||||||
|
FileChecksumDialogManager,
|
||||||
NotificationSender,
|
NotificationSender,
|
||||||
DownloadItemOpener,
|
DownloadItemOpener,
|
||||||
ContainsEffects<AppEffects> by supportEffects(),
|
ContainsEffects<AppEffects> by supportEffects(),
|
||||||
@ -120,6 +123,7 @@ class AppComponent(
|
|||||||
downloadItemOpener = this,
|
downloadItemOpener = this,
|
||||||
downloadDialogManager = this,
|
downloadDialogManager = this,
|
||||||
addDownloadDialogManager = this,
|
addDownloadDialogManager = this,
|
||||||
|
fileChecksumDialogManager = this,
|
||||||
categoryDialogManager = this,
|
categoryDialogManager = this,
|
||||||
notificationSender = this,
|
notificationSender = this,
|
||||||
editDownloadDialogManager = this,
|
editDownloadDialogManager = this,
|
||||||
@ -680,6 +684,42 @@ class AppComponent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val fileChecksumPagesControl = SlotNavigation<FileChecksumComponentConfig>()
|
||||||
|
val openedFileChecksumDialog = childSlot(
|
||||||
|
key = "openedFileChecksumPage",
|
||||||
|
source = fileChecksumPagesControl,
|
||||||
|
serializer = null,
|
||||||
|
childFactory = { config, ctx ->
|
||||||
|
FileChecksumComponent(
|
||||||
|
ctx = ctx,
|
||||||
|
id = config.id,
|
||||||
|
itemIds = config.itemIds,
|
||||||
|
closeComponent = {
|
||||||
|
closeFileChecksumPage(config.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
).subscribeAsStateFlow()
|
||||||
|
|
||||||
|
override fun openFileChecksumPage(ids: List<Long>) {
|
||||||
|
scope.launch {
|
||||||
|
val instance = openedFileChecksumDialog.value.child?.instance
|
||||||
|
if (instance?.itemIds == ids) {
|
||||||
|
instance.bringToFront()
|
||||||
|
} else {
|
||||||
|
fileChecksumPagesControl.navigate {
|
||||||
|
FileChecksumComponentConfig(itemIds = ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun closeFileChecksumPage(dialogId: String) {
|
||||||
|
scope.launch {
|
||||||
|
fileChecksumPagesControl.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun addDownload(
|
fun addDownload(
|
||||||
items: List<DownloadItem>,
|
items: List<DownloadItem>,
|
||||||
onDuplicateStrategy: (DownloadItem) -> OnDuplicateStrategy,
|
onDuplicateStrategy: (DownloadItem) -> OnDuplicateStrategy,
|
||||||
@ -881,4 +921,10 @@ interface AddDownloadDialogManager {
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun closeAddDownloadDialog(dialogId: String)
|
fun closeAddDownloadDialog(dialogId: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FileChecksumDialogManager {
|
||||||
|
fun openFileChecksumPage(ids: List<Long>)
|
||||||
|
|
||||||
|
fun closeFileChecksumPage(dialogId: String)
|
||||||
|
}
|
||||||
|
@ -8,6 +8,8 @@ import com.abdownloadmanager.desktop.pages.settings.configurable.StringConfigura
|
|||||||
import com.abdownloadmanager.desktop.repository.AppRepository
|
import com.abdownloadmanager.desktop.repository.AppRepository
|
||||||
import com.abdownloadmanager.desktop.utils.*
|
import com.abdownloadmanager.desktop.utils.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import com.abdownloadmanager.desktop.pages.settings.configurable.FileChecksumConfigurable
|
||||||
|
import com.abdownloadmanager.desktop.pages.settings.configurable.widgets.RenderFileChecksumConfig
|
||||||
import com.abdownloadmanager.shared.utils.mvi.ContainsEffects
|
import com.abdownloadmanager.shared.utils.mvi.ContainsEffects
|
||||||
import com.abdownloadmanager.shared.utils.mvi.supportEffects
|
import com.abdownloadmanager.shared.utils.mvi.supportEffects
|
||||||
import com.abdownloadmanager.resources.Res
|
import com.abdownloadmanager.resources.Res
|
||||||
@ -214,6 +216,7 @@ class AddSingleDownloadComponent(
|
|||||||
//extra settings
|
//extra settings
|
||||||
private var threadCount = MutableStateFlow(null as Int?)
|
private var threadCount = MutableStateFlow(null as Int?)
|
||||||
private var speedLimit = MutableStateFlow(0L)
|
private var speedLimit = MutableStateFlow(0L)
|
||||||
|
private var fileChecksum = MutableStateFlow(null as FileChecksum?)
|
||||||
|
|
||||||
|
|
||||||
val downloadItem = combineStateFlows(
|
val downloadItem = combineStateFlows(
|
||||||
@ -222,7 +225,8 @@ class AddSingleDownloadComponent(
|
|||||||
this.name,
|
this.name,
|
||||||
this.length,
|
this.length,
|
||||||
this.speedLimit,
|
this.speedLimit,
|
||||||
this.threadCount
|
this.threadCount,
|
||||||
|
this.fileChecksum,
|
||||||
) {
|
) {
|
||||||
credentials,
|
credentials,
|
||||||
folder,
|
folder,
|
||||||
@ -230,6 +234,7 @@ class AddSingleDownloadComponent(
|
|||||||
length,
|
length,
|
||||||
speedLimit,
|
speedLimit,
|
||||||
threadCount,
|
threadCount,
|
||||||
|
fileChecksum,
|
||||||
->
|
->
|
||||||
DownloadItem(
|
DownloadItem(
|
||||||
id = -1,
|
id = -1,
|
||||||
@ -242,7 +247,8 @@ class AddSingleDownloadComponent(
|
|||||||
completeTime = null,
|
completeTime = null,
|
||||||
status = DownloadStatus.Added,
|
status = DownloadStatus.Added,
|
||||||
preferredConnectionCount = threadCount,
|
preferredConnectionCount = threadCount,
|
||||||
speedLimit = speedLimit
|
speedLimit = speedLimit,
|
||||||
|
fileChecksum = fileChecksum?.toString()
|
||||||
).withCredentials(credentials)
|
).withCredentials(credentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,6 +268,12 @@ class AddSingleDownloadComponent(
|
|||||||
).asStringSource()
|
).asStringSource()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
FileChecksumConfigurable(
|
||||||
|
Res.string.download_item_settings_file_checksum.asStringSource(),
|
||||||
|
Res.string.download_item_settings_file_checksum_description.asStringSource(),
|
||||||
|
backedBy = fileChecksum,
|
||||||
|
describe = { "".asStringSource() }
|
||||||
|
),
|
||||||
IntConfigurable(
|
IntConfigurable(
|
||||||
Res.string.settings_download_thread_count.asStringSource(),
|
Res.string.settings_download_thread_count.asStringSource(),
|
||||||
Res.string.settings_download_thread_count_description.asStringSource(),
|
Res.string.settings_download_thread_count_description.asStringSource(),
|
||||||
@ -425,4 +437,4 @@ fun interface OnRequestDownloadSingleItem {
|
|||||||
onDuplicateStrategy: OnDuplicateStrategy,
|
onDuplicateStrategy: OnDuplicateStrategy,
|
||||||
categoryId: Long?,
|
categoryId: Long?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -98,4 +98,4 @@ class EditDownloadComponent(
|
|||||||
fun bringToFront() {
|
fun bringToFront() {
|
||||||
sendEffect(EditDownloadPageEffects.BringToFront)
|
sendEffect(EditDownloadPageEffects.BringToFront)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
package com.abdownloadmanager.desktop.pages.editdownload
|
package com.abdownloadmanager.desktop.pages.editdownload
|
||||||
|
|
||||||
|
import com.abdownloadmanager.desktop.pages.settings.configurable.FileChecksumConfigurable
|
||||||
import com.abdownloadmanager.desktop.pages.settings.configurable.IntConfigurable
|
import com.abdownloadmanager.desktop.pages.settings.configurable.IntConfigurable
|
||||||
import com.abdownloadmanager.desktop.pages.settings.configurable.SpeedLimitConfigurable
|
import com.abdownloadmanager.desktop.pages.settings.configurable.SpeedLimitConfigurable
|
||||||
import com.abdownloadmanager.desktop.pages.settings.configurable.StringConfigurable
|
import com.abdownloadmanager.desktop.pages.settings.configurable.StringConfigurable
|
||||||
import com.abdownloadmanager.desktop.repository.AppRepository
|
import com.abdownloadmanager.desktop.repository.AppRepository
|
||||||
import com.abdownloadmanager.shared.utils.FileNameValidator
|
|
||||||
import com.abdownloadmanager.shared.utils.LinkChecker
|
|
||||||
import com.abdownloadmanager.shared.utils.convertPositiveSpeedToHumanReadable
|
|
||||||
import com.abdownloadmanager.resources.Res
|
import com.abdownloadmanager.resources.Res
|
||||||
import com.abdownloadmanager.shared.utils.isValidUrl
|
import com.abdownloadmanager.shared.utils.*
|
||||||
import ir.amirab.downloader.connection.DownloaderClient
|
import ir.amirab.downloader.connection.DownloaderClient
|
||||||
import ir.amirab.downloader.downloaditem.DownloadCredentials
|
import ir.amirab.downloader.downloaditem.DownloadCredentials
|
||||||
import ir.amirab.downloader.downloaditem.DownloadItem
|
import ir.amirab.downloader.downloaditem.DownloadItem
|
||||||
@ -169,6 +167,25 @@ class EditDownloadState(
|
|||||||
else convertPositiveSpeedToHumanReadable(it, appRepository.speedUnit.value).asStringSource()
|
else convertPositiveSpeedToHumanReadable(it, appRepository.speedUnit.value).asStringSource()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
FileChecksumConfigurable(
|
||||||
|
Res.string.download_item_settings_file_checksum.asStringSource(),
|
||||||
|
Res.string.download_item_settings_file_checksum_description.asStringSource(),
|
||||||
|
backedBy = editedDownloadItem.mapTwoWayStateFlow(
|
||||||
|
map = {
|
||||||
|
it.fileChecksum?.let {
|
||||||
|
runCatching {
|
||||||
|
FileChecksum.fromString(it)
|
||||||
|
}.onFailure {
|
||||||
|
println(it.printStackTrace())
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unMap = {
|
||||||
|
copy(fileChecksum = it?.toString())
|
||||||
|
}
|
||||||
|
),
|
||||||
|
describe = { "".asStringSource() }
|
||||||
|
),
|
||||||
IntConfigurable(
|
IntConfigurable(
|
||||||
Res.string.settings_download_thread_count.asStringSource(),
|
Res.string.settings_download_thread_count.asStringSource(),
|
||||||
Res.string.settings_download_thread_count_description.asStringSource(),
|
Res.string.settings_download_thread_count_description.asStringSource(),
|
||||||
@ -335,4 +352,4 @@ class EditDownloadState(
|
|||||||
scheduleRefresh(alsoRecheckLink = false)
|
scheduleRefresh(alsoRecheckLink = false)
|
||||||
}.launchIn(scope)
|
}.launchIn(scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,268 @@
|
|||||||
|
package com.abdownloadmanager.desktop.pages.filehash
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.abdownloadmanager.shared.utils.*
|
||||||
|
import com.abdownloadmanager.shared.utils.mvi.ContainsEffects
|
||||||
|
import com.abdownloadmanager.shared.utils.mvi.ContainsScreenState
|
||||||
|
import com.abdownloadmanager.shared.utils.mvi.SupportsScreenState
|
||||||
|
import com.abdownloadmanager.shared.utils.mvi.supportEffects
|
||||||
|
import com.arkivanov.decompose.ComponentContext
|
||||||
|
import ir.amirab.downloader.downloaditem.DownloadItem
|
||||||
|
import ir.amirab.downloader.downloaditem.DownloadStatus
|
||||||
|
import ir.amirab.util.ifThen
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
data class FileChecksumComponentConfig(
|
||||||
|
val id: String = UUID.randomUUID().toString(),
|
||||||
|
val itemIds: List<Long>,
|
||||||
|
)
|
||||||
|
|
||||||
|
class FileChecksumComponent(
|
||||||
|
ctx: ComponentContext,
|
||||||
|
val id: String,
|
||||||
|
val itemIds: List<Long>,
|
||||||
|
private val closeComponent: () -> Unit,
|
||||||
|
) : BaseComponent(ctx),
|
||||||
|
KoinComponent,
|
||||||
|
ContainsScreenState<FileChecksumUiState> by SupportsScreenState(FileChecksumUiState.default()),
|
||||||
|
ContainsEffects<FileChecksumUiEffects> by supportEffects() {
|
||||||
|
val downloadSystem: DownloadSystem by inject()
|
||||||
|
|
||||||
|
private var downloadItems: List<DownloadItem> by Delegates.notNull()
|
||||||
|
|
||||||
|
private val isChecking = MutableStateFlow(false)
|
||||||
|
private val selectedDefaultAlgorithm: MutableStateFlow<FileChecksumAlgorithm> =
|
||||||
|
MutableStateFlow(FileChecksumAlgorithm.default())
|
||||||
|
|
||||||
|
fun onAlgorithmChange(algorithm: FileChecksumAlgorithm) {
|
||||||
|
this.selectedDefaultAlgorithm.update { algorithm }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isDefaultAlgorithmNeeded(): Boolean {
|
||||||
|
return state.value.items.any {
|
||||||
|
it.savedChecksum == null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launch {
|
||||||
|
load(itemIds)
|
||||||
|
setup()
|
||||||
|
|
||||||
|
if (!isDefaultAlgorithmNeeded()) {
|
||||||
|
// user don't need to manually set checksum algorithm
|
||||||
|
// start checking immediately
|
||||||
|
startCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isChecking.onEach { isChecking ->
|
||||||
|
setState { fileChecksumUiState ->
|
||||||
|
fileChecksumUiState.copy(isChecking = isChecking)
|
||||||
|
}
|
||||||
|
}.launchIn(scope)
|
||||||
|
selectedDefaultAlgorithm.onEach { algorithm ->
|
||||||
|
setState { fileChecksumUiState ->
|
||||||
|
fileChecksumUiState.copy(
|
||||||
|
// reset checksum algorithm
|
||||||
|
items = fileChecksumUiState.items.map { itemWithChecksum ->
|
||||||
|
itemWithChecksum.copy(
|
||||||
|
algorithm = getChecksumAlgorithmForItem(itemWithChecksum.downloadItem)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
defaultAlgorithm = algorithm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setup() {
|
||||||
|
setState {
|
||||||
|
it.copy(
|
||||||
|
items = downloadItems.map { downloadItem ->
|
||||||
|
val savedChecksum = downloadItem.fileChecksum?.let { fc ->
|
||||||
|
FileChecksum.fromString(fc)
|
||||||
|
}
|
||||||
|
DownloadItemWithChecksum(
|
||||||
|
downloadItem = downloadItem,
|
||||||
|
checksumStatus = ChecksumStatus.Waiting,
|
||||||
|
algorithm = savedChecksum?.algorithm ?: selectedDefaultAlgorithm.value.algorithm,
|
||||||
|
savedChecksum = savedChecksum?.value,
|
||||||
|
calculatedChecksum = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun load(items: List<Long>) {
|
||||||
|
downloadItems = items.mapNotNull {
|
||||||
|
downloadSystem.getDownloadItemById(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun startCheck() {
|
||||||
|
// clean old statuses
|
||||||
|
setup()
|
||||||
|
|
||||||
|
isChecking.update { true }
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
for (item in downloadItems) {
|
||||||
|
processItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isChecking.update { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processItem(item: DownloadItem) {
|
||||||
|
val file = downloadSystem.getDownloadFile(item)
|
||||||
|
if (item.status != DownloadStatus.Completed) {
|
||||||
|
scope.launch {
|
||||||
|
updateItemStatus(item.id, ChecksumStatus.Error.DownloadNotFinished)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!file.isFile) {
|
||||||
|
scope.launch {
|
||||||
|
updateItemStatus(item.id, ChecksumStatus.Error.FileNotFound)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val algorithm = getChecksumAlgorithmForItem(item)
|
||||||
|
val hash = HashUtil.fileHash(
|
||||||
|
algorithm = algorithm,
|
||||||
|
file = file,
|
||||||
|
onNewPercent = { percent ->
|
||||||
|
scope.launch {
|
||||||
|
updateItemStatus(item.id, ChecksumStatus.Checking(percent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val savedChecksum = FileChecksum.fromNullableString(item.fileChecksum)
|
||||||
|
val calculatedChecksum = FileChecksum(algorithm, hash)
|
||||||
|
val newStatus = if (savedChecksum == null) {
|
||||||
|
ChecksumStatus.Finished.Done
|
||||||
|
} else {
|
||||||
|
if (savedChecksum == calculatedChecksum) {
|
||||||
|
ChecksumStatus.Finished.Matches
|
||||||
|
} else {
|
||||||
|
ChecksumStatus.Finished.NotMatches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
updateItem(item.id) {
|
||||||
|
it.copy(
|
||||||
|
checksumStatus = newStatus,
|
||||||
|
calculatedChecksum = hash,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
scope.launch {
|
||||||
|
updateItemStatus(item.id, ChecksumStatus.Error.Exception(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getChecksumAlgorithmForItem(downloadItem: DownloadItem): String {
|
||||||
|
return downloadItem.fileChecksum?.let {
|
||||||
|
FileChecksum.fromString(it).algorithm
|
||||||
|
} ?: selectedDefaultAlgorithm.value.algorithm
|
||||||
|
}
|
||||||
|
private fun updateItem(id: Long, updater: (DownloadItemWithChecksum) -> DownloadItemWithChecksum) {
|
||||||
|
setState {
|
||||||
|
it.copy(
|
||||||
|
items = it.items.map { itemWithChecksum ->
|
||||||
|
itemWithChecksum.ifThen(itemWithChecksum.downloadItem.id == id) {
|
||||||
|
updater(itemWithChecksum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun updateItemStatus(id: Long, status: ChecksumStatus) {
|
||||||
|
updateItem(id) {
|
||||||
|
it.copy(checksumStatus = status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRequestClose() {
|
||||||
|
closeComponent()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRequestStartCheck() {
|
||||||
|
scope.launch {
|
||||||
|
startCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bringToFront() {
|
||||||
|
sendEffect(FileChecksumUiEffects.BringToFront)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface ChecksumStatus {
|
||||||
|
sealed interface Finished : ChecksumStatus {
|
||||||
|
data object Matches : Finished
|
||||||
|
data object NotMatches : Finished
|
||||||
|
|
||||||
|
// just finished there is no saved checksum to compare it
|
||||||
|
data object Done : Finished
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Checking(val percent: Int) : ChecksumStatus
|
||||||
|
sealed interface Error : ChecksumStatus {
|
||||||
|
data object FileNotFound : Error
|
||||||
|
data object DownloadNotFinished : Error
|
||||||
|
data class Exception(val t: Throwable) : Error
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Waiting : ChecksumStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class DownloadItemWithChecksum(
|
||||||
|
val downloadItem: DownloadItem,
|
||||||
|
val checksumStatus: ChecksumStatus,
|
||||||
|
val algorithm: String,
|
||||||
|
val savedChecksum: String?,
|
||||||
|
val calculatedChecksum: String?,
|
||||||
|
) {
|
||||||
|
val isProcessing = checksumStatus is ChecksumStatus.Checking
|
||||||
|
val isError = checksumStatus is ChecksumStatus.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class FileChecksumUiState(
|
||||||
|
val items: List<DownloadItemWithChecksum>,
|
||||||
|
val isChecking: Boolean,
|
||||||
|
val defaultAlgorithm: FileChecksumAlgorithm,
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun default() = FileChecksumUiState(
|
||||||
|
items = emptyList(),
|
||||||
|
isChecking = false,
|
||||||
|
defaultAlgorithm = FileChecksumAlgorithm.default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface FileChecksumUiEffects {
|
||||||
|
data object BringToFront : FileChecksumUiEffects
|
||||||
|
}
|
@ -0,0 +1,423 @@
|
|||||||
|
package com.abdownloadmanager.desktop.pages.filehash
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.desktop.pages.settings.configurable.widgets.RenderSpinner
|
||||||
|
import com.abdownloadmanager.desktop.utils.ClipboardUtil
|
||||||
|
import com.abdownloadmanager.desktop.window.custom.WindowTitle
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.shared.ui.widget.ActionButton
|
||||||
|
import com.abdownloadmanager.shared.ui.widget.Help
|
||||||
|
import com.abdownloadmanager.shared.ui.widget.Text
|
||||||
|
import com.abdownloadmanager.shared.ui.widget.Tooltip
|
||||||
|
import com.abdownloadmanager.shared.ui.widget.customtable.*
|
||||||
|
import com.abdownloadmanager.shared.ui.widget.customtable.styled.MyStyledTableHeader
|
||||||
|
import com.abdownloadmanager.shared.utils.FileChecksumAlgorithm
|
||||||
|
import com.abdownloadmanager.shared.utils.div
|
||||||
|
import com.abdownloadmanager.shared.utils.rememberDotLoading
|
||||||
|
import com.abdownloadmanager.shared.utils.ui.WithContentColor
|
||||||
|
import com.abdownloadmanager.shared.utils.ui.icon.MyIcons
|
||||||
|
import com.abdownloadmanager.shared.utils.ui.myColors
|
||||||
|
import com.abdownloadmanager.shared.utils.ui.theme.myTextSizes
|
||||||
|
import com.abdownloadmanager.shared.utils.ui.widget.MyIcon
|
||||||
|
import ir.amirab.util.compose.IconSource
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FileChecksumPage(component: FileChecksumComponent) {
|
||||||
|
WindowTitle(myStringResource(Res.string.file_checksum_page))
|
||||||
|
val horizontalPadding = 16.dp
|
||||||
|
Column {
|
||||||
|
Table(
|
||||||
|
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||||
|
list = component.state.collectAsState().value.items,
|
||||||
|
tableState = remember {
|
||||||
|
TableState(FileChecksumTableCells.cells)
|
||||||
|
},
|
||||||
|
wrapHeader = {
|
||||||
|
MyStyledTableHeader(
|
||||||
|
itemHorizontalPadding = horizontalPadding,
|
||||||
|
content = it,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wrapItem = { index, item, content ->
|
||||||
|
Box(Modifier.padding(horizontal = horizontalPadding).let {
|
||||||
|
val mutableInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
it.indication(mutableInteractionSource, LocalIndication.current)
|
||||||
|
.hoverable(mutableInteractionSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding(vertical = 8.dp)) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderCell = { cell, item ->
|
||||||
|
when (cell) {
|
||||||
|
FileChecksumTableCells.Name -> {
|
||||||
|
FileChecksumTableCellRenderers.RenderName(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
FileChecksumTableCells.Status -> {
|
||||||
|
FileChecksumTableCellRenderers.RenderStatus(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
FileChecksumTableCells.Algorithm -> {
|
||||||
|
FileChecksumTableCellRenderers.RenderAlgorithm(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
FileChecksumTableCells.CalculatedChecksum -> {
|
||||||
|
FileChecksumTableCellRenderers.RenderCalculatedChecksum(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
FileChecksumTableCells.SavedChecksum -> {
|
||||||
|
FileChecksumTableCellRenderers.RenderSavedChecksum(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Actions(
|
||||||
|
Modifier,
|
||||||
|
component,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Actions(
|
||||||
|
modifier: Modifier,
|
||||||
|
component: FileChecksumComponent,
|
||||||
|
) {
|
||||||
|
val uiState by component.state.collectAsState()
|
||||||
|
Column(modifier) {
|
||||||
|
Spacer(
|
||||||
|
Modifier.fillMaxWidth().height(1.dp).background(myColors.onBackground / 0.15f)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth().background(myColors.surface / 0.5f).padding(horizontal = 16.dp)
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = myStringResource(Res.string.file_checksum_page_file_checksum_default_algorithm)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Help(
|
||||||
|
myStringResource(Res.string.file_checksum_page_file_checksum_default_algorithm_help)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.size(8.dp))
|
||||||
|
RenderSpinner(
|
||||||
|
modifier = Modifier,
|
||||||
|
possibleValues = FileChecksumAlgorithm.all(),
|
||||||
|
value = uiState.defaultAlgorithm,
|
||||||
|
enabled = !uiState.isChecking,
|
||||||
|
onSelect = {
|
||||||
|
component.onAlgorithmChange(it)
|
||||||
|
},
|
||||||
|
render = {
|
||||||
|
Text(it.algorithm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Row {
|
||||||
|
ActionButton(
|
||||||
|
myStringResource(Res.string.start),
|
||||||
|
onClick = component::onRequestStartCheck,
|
||||||
|
enabled = !uiState.isChecking
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
ActionButton(
|
||||||
|
myStringResource(Res.string.close),
|
||||||
|
onClick = component::onRequestClose,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private data object FileChecksumTableCellRenderers {
|
||||||
|
@Composable
|
||||||
|
fun RenderName(item: DownloadItemWithChecksum) {
|
||||||
|
SimpleText(item.downloadItem.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderStatus(item: DownloadItemWithChecksum) {
|
||||||
|
when (val status = item.checksumStatus) {
|
||||||
|
is ChecksumStatus.Checking -> {
|
||||||
|
RenderCheckingStatus(status.percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
ChecksumStatus.Error.DownloadNotFinished -> {
|
||||||
|
RenderErrorStatus(myStringResource(Res.string.download_not_finished))
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChecksumStatus.Error.Exception -> {
|
||||||
|
RenderErrorStatus(status.t.localizedMessage ?: status.t::class.simpleName.orEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
ChecksumStatus.Error.FileNotFound -> {
|
||||||
|
RenderErrorStatus(myStringResource(Res.string.file_not_found))
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChecksumStatus.Finished -> {
|
||||||
|
RenderFinishedStatus(
|
||||||
|
status = status,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ChecksumStatus.Waiting -> {
|
||||||
|
RenderWaitingStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderAlgorithm(item: DownloadItemWithChecksum) {
|
||||||
|
SimpleText(item.algorithm)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CopyableText(text: String) {
|
||||||
|
Tooltip(
|
||||||
|
Res.string.copy_to_clipboard.asStringSource()
|
||||||
|
) {
|
||||||
|
SimpleText(
|
||||||
|
text,
|
||||||
|
Modifier.clickable {
|
||||||
|
ClipboardUtil.copy(text)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderCalculatedChecksum(item: DownloadItemWithChecksum) {
|
||||||
|
if (item.calculatedChecksum != null) {
|
||||||
|
CopyableText(item.calculatedChecksum)
|
||||||
|
} else if (item.isProcessing) {
|
||||||
|
//shimmer
|
||||||
|
ShimmerEffect(
|
||||||
|
centerColor = myColors.onBackground / 0.4f,
|
||||||
|
surroundingColor = myColors.onBackground / 0.1f,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(6.dp))
|
||||||
|
.height(myTextSizes.base.value.dp)
|
||||||
|
)
|
||||||
|
} else if (item.isError) {
|
||||||
|
SimpleText("!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderSavedChecksum(item: DownloadItemWithChecksum) {
|
||||||
|
CopyableText(item.savedChecksum.orEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ShimmerEffect(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
centerColor: Color = Color.Gray,
|
||||||
|
surroundingColor: Color = Color.Gray,
|
||||||
|
) {
|
||||||
|
val transition = rememberInfiniteTransition()
|
||||||
|
val translateAnim = transition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 1000f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(
|
||||||
|
durationMillis = 3000,
|
||||||
|
easing = LinearEasing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
surroundingColor,
|
||||||
|
centerColor,
|
||||||
|
surroundingColor,
|
||||||
|
),
|
||||||
|
start = Offset(0f, 0f),
|
||||||
|
end = Offset(translateAnim.value, 0f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.background(brush = brush)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderErrorStatus(message: String) {
|
||||||
|
IconWithText(
|
||||||
|
icon = MyIcons.info,
|
||||||
|
text = message,
|
||||||
|
color = myColors.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderFinishedStatus(
|
||||||
|
status: ChecksumStatus.Finished,
|
||||||
|
) {
|
||||||
|
val text: StringSource
|
||||||
|
val color: Color
|
||||||
|
val icon: IconSource
|
||||||
|
when (status) {
|
||||||
|
ChecksumStatus.Finished.Done -> {
|
||||||
|
text = Res.string.done.asStringSource()
|
||||||
|
icon = MyIcons.check
|
||||||
|
color = myColors.info
|
||||||
|
}
|
||||||
|
|
||||||
|
ChecksumStatus.Finished.Matches -> {
|
||||||
|
text = Res.string.matches.asStringSource()
|
||||||
|
icon = MyIcons.check
|
||||||
|
color = myColors.success
|
||||||
|
}
|
||||||
|
|
||||||
|
ChecksumStatus.Finished.NotMatches -> {
|
||||||
|
text = Res.string.not_matches.asStringSource()
|
||||||
|
icon = MyIcons.info
|
||||||
|
color = myColors.warning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconWithText(
|
||||||
|
icon = icon,
|
||||||
|
text = text.rememberString(),
|
||||||
|
color = color,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun IconWithText(
|
||||||
|
icon: IconSource,
|
||||||
|
text: String,
|
||||||
|
color: Color,
|
||||||
|
) {
|
||||||
|
WithContentColor(color) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
MyIcon(
|
||||||
|
icon,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(2.dp))
|
||||||
|
SimpleText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderCheckingStatus(percent: Int) {
|
||||||
|
Column {
|
||||||
|
ProgressStatus(percent, myColors.primaryGradient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderWaitingStatus() {
|
||||||
|
Row {
|
||||||
|
SimpleText("${myStringResource(Res.string.waiting)} ${rememberDotLoading()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProgressStatus(
|
||||||
|
percent: Int?,
|
||||||
|
background: Brush = myColors.primaryGradient,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier.fillMaxWidth().clip(CircleShape).background(myColors.surface)
|
||||||
|
) {
|
||||||
|
if (percent != null) {
|
||||||
|
val w = (percent / 100f).coerceIn(0f..1f)
|
||||||
|
Spacer(
|
||||||
|
Modifier.height(5.dp).fillMaxWidth(
|
||||||
|
animateFloatAsState(
|
||||||
|
w, tween(100)
|
||||||
|
).value
|
||||||
|
).background(background)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SimpleText(string: String, modifier: Modifier = Modifier) {
|
||||||
|
Text(
|
||||||
|
string,
|
||||||
|
modifier = modifier,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FileChecksumTableCells : TableCell<DownloadItemWithChecksum> {
|
||||||
|
data object Name : FileChecksumTableCells() {
|
||||||
|
override val id: String = "name"
|
||||||
|
override val name: StringSource = Res.string.name.asStringSource()
|
||||||
|
override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp, 300.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Status : FileChecksumTableCells() {
|
||||||
|
override val id: String = "status"
|
||||||
|
override val name: StringSource = Res.string.status.asStringSource()
|
||||||
|
override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp, 150.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Algorithm : FileChecksumTableCells() {
|
||||||
|
override val id: String = "algorithm"
|
||||||
|
override val name: StringSource = Res.string.checksum_algorithm.asStringSource()
|
||||||
|
override val size: CellSize = CellSize.Resizeable(60.dp..300.dp, 60.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object SavedChecksum : FileChecksumTableCells() {
|
||||||
|
override val id: String = "saved_checksum"
|
||||||
|
override val name: StringSource = Res.string.saved_checksum.asStringSource()
|
||||||
|
override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp, 150.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object CalculatedChecksum : FileChecksumTableCells() {
|
||||||
|
override val id: String = "calculated_checksum"
|
||||||
|
override val name: StringSource = Res.string.calculated_checksum.asStringSource()
|
||||||
|
override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp, 150.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val cells = listOf(
|
||||||
|
Name,
|
||||||
|
Status,
|
||||||
|
Algorithm,
|
||||||
|
CalculatedChecksum,
|
||||||
|
SavedChecksum,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package com.abdownloadmanager.desktop.pages.filehash
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.WindowPosition
|
||||||
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
|
import com.abdownloadmanager.desktop.AppComponent
|
||||||
|
import com.abdownloadmanager.desktop.window.custom.CustomWindow
|
||||||
|
import com.abdownloadmanager.shared.utils.ui.theme.LocalUiScale
|
||||||
|
import com.arkivanov.decompose.Child
|
||||||
|
import com.arkivanov.decompose.router.slot.ChildSlot
|
||||||
|
import ir.amirab.util.desktop.screen.applyUiScale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FileChecksumWindow(
|
||||||
|
component: AppComponent
|
||||||
|
) {
|
||||||
|
component.openedFileChecksumDialog.collectAsState().value.child?.instance?.let {
|
||||||
|
FileChecksumWindow(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FileChecksumWindow(
|
||||||
|
component: FileChecksumComponent
|
||||||
|
) {
|
||||||
|
val uiScale = LocalUiScale.current
|
||||||
|
CustomWindow(
|
||||||
|
state = rememberWindowState(
|
||||||
|
position = WindowPosition.Aligned(Alignment.Center),
|
||||||
|
size = DpSize(900.dp, 400.dp).applyUiScale(uiScale)
|
||||||
|
),
|
||||||
|
onCloseRequest = component::onRequestClose
|
||||||
|
) {
|
||||||
|
FileChecksumPage(component)
|
||||||
|
}
|
||||||
|
}
|
@ -84,6 +84,7 @@ class DownloadActions(
|
|||||||
downloadSystem: DownloadSystem,
|
downloadSystem: DownloadSystem,
|
||||||
downloadDialogManager: DownloadDialogManager,
|
downloadDialogManager: DownloadDialogManager,
|
||||||
editDownloadDialogManager: EditDownloadDialogManager,
|
editDownloadDialogManager: EditDownloadDialogManager,
|
||||||
|
fileChecksumDialogManager: FileChecksumDialogManager,
|
||||||
val selections: StateFlow<List<IDownloadItemState>>,
|
val selections: StateFlow<List<IDownloadItemState>>,
|
||||||
private val mainItem: StateFlow<Long?>,
|
private val mainItem: StateFlow<Long?>,
|
||||||
private val queueManager: QueueManager,
|
private val queueManager: QueueManager,
|
||||||
@ -236,6 +237,18 @@ class DownloadActions(
|
|||||||
downloadDialogManager.openDownloadDialog(id)
|
downloadDialogManager.openDownloadDialog(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private val fileChecksumAction = simpleAction(
|
||||||
|
title = Res.string.file_checksum.asStringSource(), MyIcons.info,
|
||||||
|
checkEnable = selections.mapStateFlow { list ->
|
||||||
|
list.any { iiDownloadItemState ->
|
||||||
|
iiDownloadItemState.isFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
fileChecksumDialogManager.openFileChecksumPage(
|
||||||
|
selections.value.map { it.id }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private val moveToQueueItems = MenuItem.SubMenu(
|
private val moveToQueueItems = MenuItem.SubMenu(
|
||||||
title = Res.string.move_to_queue.asStringSource(),
|
title = Res.string.move_to_queue.asStringSource(),
|
||||||
@ -290,6 +303,7 @@ class DownloadActions(
|
|||||||
separator()
|
separator()
|
||||||
+(copyDownloadLinkAction)
|
+(copyDownloadLinkAction)
|
||||||
+editDownloadAction
|
+editDownloadAction
|
||||||
|
+fileChecksumAction
|
||||||
+(openDownloadDialogAction)
|
+(openDownloadDialogAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -405,6 +419,7 @@ class HomeComponent(
|
|||||||
private val downloadDialogManager: DownloadDialogManager,
|
private val downloadDialogManager: DownloadDialogManager,
|
||||||
private val editDownloadDialogManager: EditDownloadDialogManager,
|
private val editDownloadDialogManager: EditDownloadDialogManager,
|
||||||
private val addDownloadDialogManager: AddDownloadDialogManager,
|
private val addDownloadDialogManager: AddDownloadDialogManager,
|
||||||
|
private val fileChecksumDialogManager: FileChecksumDialogManager,
|
||||||
private val categoryDialogManager: CategoryDialogManager,
|
private val categoryDialogManager: CategoryDialogManager,
|
||||||
private val notificationSender: NotificationSender,
|
private val notificationSender: NotificationSender,
|
||||||
) : BaseComponent(ctx),
|
) : BaseComponent(ctx),
|
||||||
@ -917,6 +932,7 @@ class HomeComponent(
|
|||||||
downloadSystem = downloadSystem,
|
downloadSystem = downloadSystem,
|
||||||
downloadDialogManager = downloadDialogManager,
|
downloadDialogManager = downloadDialogManager,
|
||||||
editDownloadDialogManager = editDownloadDialogManager,
|
editDownloadDialogManager = editDownloadDialogManager,
|
||||||
|
fileChecksumDialogManager = fileChecksumDialogManager,
|
||||||
selections = selectionListItems,
|
selections = selectionListItems,
|
||||||
mainItem = mainItem,
|
mainItem = mainItem,
|
||||||
queueManager = queueManager,
|
queueManager = queueManager,
|
||||||
@ -998,4 +1014,4 @@ class HomeComponent(
|
|||||||
private var homeComponentCreationCount = 0
|
private var homeComponentCreationCount = 0
|
||||||
val CATEGORIES_SIZE_RANGE = 0.dp..500.dp
|
val CATEGORIES_SIZE_RANGE = 0.dp..500.dp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package com.abdownloadmanager.desktop.pages.settings.configurable
|
package com.abdownloadmanager.desktop.pages.settings.configurable
|
||||||
|
|
||||||
import com.abdownloadmanager.desktop.pages.settings.ThemeInfo
|
import com.abdownloadmanager.desktop.pages.settings.ThemeInfo
|
||||||
|
import com.abdownloadmanager.shared.utils.FileChecksum
|
||||||
|
import com.abdownloadmanager.shared.utils.FileChecksumAlgorithm
|
||||||
import com.abdownloadmanager.shared.utils.proxy.ProxyData
|
import com.abdownloadmanager.shared.utils.proxy.ProxyData
|
||||||
import ir.amirab.util.compose.StringSource
|
import ir.amirab.util.compose.StringSource
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@ -26,7 +28,7 @@ sealed class Configurable<T>(
|
|||||||
if (validate(value)) {
|
if (validate(value)) {
|
||||||
// don't use update function here maybe this is a mappedByTwoWayMutableStateFlow
|
// don't use update function here maybe this is a mappedByTwoWayMutableStateFlow
|
||||||
// IMPROVE
|
// IMPROVE
|
||||||
backedBy.value=value
|
backedBy.value = value
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -141,7 +143,7 @@ class FloatConfigurable(
|
|||||||
describe = describe,
|
describe = describe,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
visible = visible,
|
visible = visible,
|
||||||
){
|
) {
|
||||||
enum class RenderMode {
|
enum class RenderMode {
|
||||||
TextField,
|
TextField,
|
||||||
}
|
}
|
||||||
@ -262,6 +264,22 @@ class SpeedLimitConfigurable(
|
|||||||
visible = visible,
|
visible = visible,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class FileChecksumConfigurable(
|
||||||
|
title: StringSource,
|
||||||
|
description: StringSource,
|
||||||
|
backedBy: MutableStateFlow<FileChecksum?>,
|
||||||
|
describe: (FileChecksum?) -> StringSource,
|
||||||
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
|
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||||
|
) : Configurable<FileChecksum?>(
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
backedBy = backedBy,
|
||||||
|
describe = describe,
|
||||||
|
enabled = enabled,
|
||||||
|
visible = visible,
|
||||||
|
)
|
||||||
|
|
||||||
class TimeConfigurable(
|
class TimeConfigurable(
|
||||||
title: StringSource,
|
title: StringSource,
|
||||||
description: StringSource,
|
description: StringSource,
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
package com.abdownloadmanager.desktop.pages.settings.configurable.widgets
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.desktop.pages.settings.configurable.FileChecksumConfigurable
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.shared.ui.widget.*
|
||||||
|
import com.abdownloadmanager.shared.utils.FileChecksum
|
||||||
|
import com.abdownloadmanager.shared.utils.FileChecksumAlgorithm
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderFileChecksumConfig(cfg: FileChecksumConfigurable, modifier: Modifier) {
|
||||||
|
val value by cfg.stateFlow.collectAsState()
|
||||||
|
val setValue = cfg::set
|
||||||
|
|
||||||
|
val enabled = isConfigEnabled()
|
||||||
|
val hasFileChecksum = value != null
|
||||||
|
ConfigTemplate(
|
||||||
|
modifier,
|
||||||
|
title = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
TitleAndDescription(cfg, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nestedContent = {
|
||||||
|
Column(Modifier.align(Alignment.End)) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
hasFileChecksum,
|
||||||
|
) {
|
||||||
|
value?.let { value ->
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
MyTextField(
|
||||||
|
text = value.value,
|
||||||
|
onTextChange = {
|
||||||
|
setValue(value.copy(value = it))
|
||||||
|
},
|
||||||
|
shape = RectangleShape,
|
||||||
|
textPadding = PaddingValues(4.dp),
|
||||||
|
enabled = enabled,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
placeholder = myStringResource(Res.string.file_checksum),
|
||||||
|
)
|
||||||
|
RenderSpinner(
|
||||||
|
possibleValues = FileChecksumAlgorithm
|
||||||
|
.all()
|
||||||
|
.map { it.algorithm },
|
||||||
|
value = value.algorithm,
|
||||||
|
modifier = Modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
onSelect = {
|
||||||
|
setValue(value.copy(algorithm = it))
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value = {
|
||||||
|
CheckBox(
|
||||||
|
value = hasFileChecksum,
|
||||||
|
enabled = enabled,
|
||||||
|
onValueChange = {
|
||||||
|
if (it) {
|
||||||
|
setValue(
|
||||||
|
FileChecksum(
|
||||||
|
FileChecksumAlgorithm.default().algorithm,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setValue(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
@ -49,6 +49,7 @@ fun RenderConfigurable(
|
|||||||
|
|
||||||
is DayOfWeekConfigurable -> RenderDayOfWeekConfigurable(cfg,modifier)
|
is DayOfWeekConfigurable -> RenderDayOfWeekConfigurable(cfg,modifier)
|
||||||
is ProxyConfigurable -> RenderProxyConfig(cfg, modifier)
|
is ProxyConfigurable -> RenderProxyConfig(cfg, modifier)
|
||||||
|
is FileChecksumConfigurable -> RenderFileChecksumConfig(cfg, modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import com.abdownloadmanager.desktop.pages.category.ShowCategoryDialogs
|
|||||||
import com.abdownloadmanager.desktop.pages.confirmexit.ConfirmExit
|
import com.abdownloadmanager.desktop.pages.confirmexit.ConfirmExit
|
||||||
import com.abdownloadmanager.desktop.pages.credits.translators.ShowTranslators
|
import com.abdownloadmanager.desktop.pages.credits.translators.ShowTranslators
|
||||||
import com.abdownloadmanager.desktop.pages.editdownload.EditDownloadWindow
|
import com.abdownloadmanager.desktop.pages.editdownload.EditDownloadWindow
|
||||||
|
import com.abdownloadmanager.desktop.pages.filehash.FileChecksumWindow
|
||||||
import com.abdownloadmanager.desktop.pages.home.HomeWindow
|
import com.abdownloadmanager.desktop.pages.home.HomeWindow
|
||||||
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
|
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
|
||||||
import com.abdownloadmanager.desktop.pages.updater.ShowUpdaterDialog
|
import com.abdownloadmanager.desktop.pages.updater.ShowUpdaterDialog
|
||||||
@ -93,6 +94,7 @@ object Ui : KoinComponent {
|
|||||||
ShowAddDownloadDialogs(appComponent)
|
ShowAddDownloadDialogs(appComponent)
|
||||||
ShowDownloadDialogs(appComponent)
|
ShowDownloadDialogs(appComponent)
|
||||||
ShowCategoryDialogs(appComponent)
|
ShowCategoryDialogs(appComponent)
|
||||||
|
FileChecksumWindow(appComponent)
|
||||||
ShowUpdaterDialog(appComponent.updater)
|
ShowUpdaterDialog(appComponent.updater)
|
||||||
ShowAboutDialog(appComponent)
|
ShowAboutDialog(appComponent)
|
||||||
NewQueueDialog(appComponent)
|
NewQueueDialog(appComponent)
|
||||||
|
@ -24,6 +24,8 @@ data class DownloadItem(
|
|||||||
var status: DownloadStatus = DownloadStatus.Added,
|
var status: DownloadStatus = DownloadStatus.Added,
|
||||||
var preferredConnectionCount: Int? = null,
|
var preferredConnectionCount: Int? = null,
|
||||||
var speedLimit: Long = 0,//0 is unlimited
|
var speedLimit: Long = 0,//0 is unlimited
|
||||||
|
|
||||||
|
var fileChecksum: String? = null,
|
||||||
) : IDownloadCredentials {
|
) : IDownloadCredentials {
|
||||||
companion object {
|
companion object {
|
||||||
const val LENGTH_UNKNOWN = -1L
|
const val LENGTH_UNKNOWN = -1L
|
||||||
@ -51,6 +53,8 @@ fun DownloadItem.applyFrom(other: DownloadItem) {
|
|||||||
status = other.status
|
status = other.status
|
||||||
preferredConnectionCount = other.preferredConnectionCount
|
preferredConnectionCount = other.preferredConnectionCount
|
||||||
speedLimit = other.speedLimit
|
speedLimit = other.speedLimit
|
||||||
|
|
||||||
|
fileChecksum = other.fileChecksum
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DownloadItem.withCredentials(credentials: IDownloadCredentials) = apply {
|
fun DownloadItem.withCredentials(credentials: IDownloadCredentials) = apply {
|
||||||
|
@ -3,11 +3,21 @@ package ir.amirab.downloader.monitor
|
|||||||
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
||||||
|
|
||||||
fun IDownloadItemState.statusOrFinished(): DownloadJobStatus {
|
fun IDownloadItemState.statusOrFinished(): DownloadJobStatus {
|
||||||
return (this as? ProcessingDownloadItemState)?.status?:DownloadJobStatus.Finished
|
return (this as? ProcessingDownloadItemState)?.status ?: DownloadJobStatus.Finished
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun IDownloadItemState.isFinished(): Boolean {
|
||||||
|
return this is CompletedDownloadItemState
|
||||||
|
}
|
||||||
|
|
||||||
|
fun IDownloadItemState.isNotFinished(): Boolean {
|
||||||
|
return this is ProcessingDownloadItemState
|
||||||
|
}
|
||||||
|
|
||||||
fun IDownloadItemState.speedOrNull(): Long? {
|
fun IDownloadItemState.speedOrNull(): Long? {
|
||||||
return (this as? ProcessingDownloadItemState)?.speed
|
return (this as? ProcessingDownloadItemState)?.speed
|
||||||
}
|
}
|
||||||
|
|
||||||
fun IDownloadItemState.remainingOrNull(): Long? {
|
fun IDownloadItemState.remainingOrNull(): Long? {
|
||||||
return (this as? ProcessingDownloadItemState)?.remainingTime
|
return (this as? ProcessingDownloadItemState)?.remainingTime
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,116 @@
|
|||||||
|
package com.abdownloadmanager.shared.utils
|
||||||
|
|
||||||
|
import ir.amirab.downloader.utils.calcPercent
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
sealed class FileChecksumAlgorithm(
|
||||||
|
val algorithm: String,
|
||||||
|
) {
|
||||||
|
data object MD5 : FileChecksumAlgorithm("MD5")
|
||||||
|
data object SHA1 : FileChecksumAlgorithm("SHA-1")
|
||||||
|
data object SHA256 : FileChecksumAlgorithm("SHA-256")
|
||||||
|
data object SHA512 : FileChecksumAlgorithm("SHA-512")
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun default() = SHA256
|
||||||
|
fun all() = listOf(
|
||||||
|
MD5,
|
||||||
|
SHA1,
|
||||||
|
SHA256,
|
||||||
|
SHA512,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FileChecksum(
|
||||||
|
val algorithm: String,
|
||||||
|
val value: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$algorithm:$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(string: String): FileChecksum {
|
||||||
|
val segments = string.split(":")
|
||||||
|
require(segments.size == 2) {
|
||||||
|
"Invalid checksum string: $string it should be in format algorithm:value"
|
||||||
|
}
|
||||||
|
return FileChecksum(
|
||||||
|
algorithm = segments[0],
|
||||||
|
value = segments[1],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromNullableString(string: String?): FileChecksum? {
|
||||||
|
return string?.let {
|
||||||
|
fromString(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as FileChecksum
|
||||||
|
return algorithm.equals(other.algorithm, true) && value.equals(other.value, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = algorithm.hashCode()
|
||||||
|
result = 31 * result + value.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object HashUtil {
|
||||||
|
fun hash(
|
||||||
|
algorithm: String,
|
||||||
|
inputStream: InputStream,
|
||||||
|
size: Long,
|
||||||
|
onNewPercent: (Int) -> Unit,
|
||||||
|
): String {
|
||||||
|
val messageDigest = MessageDigest.getInstance(algorithm)
|
||||||
|
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
var processedBytes = 0L
|
||||||
|
var lastPercent = 0
|
||||||
|
while (true) {
|
||||||
|
val readCount = inputStream.read(buffer)
|
||||||
|
if (readCount == -1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
messageDigest.update(buffer, 0, readCount)
|
||||||
|
processedBytes += readCount
|
||||||
|
val newPercent = calcPercent(processedBytes, size)
|
||||||
|
if (newPercent != lastPercent) {
|
||||||
|
onNewPercent(newPercent)
|
||||||
|
lastPercent = newPercent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messageDigest
|
||||||
|
.digest()
|
||||||
|
.joinToString("") {
|
||||||
|
"%02x".format(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fileHash(
|
||||||
|
algorithm: String,
|
||||||
|
file: File,
|
||||||
|
onNewPercent: (Int) -> Unit
|
||||||
|
): String {
|
||||||
|
val fileSize = file.length()
|
||||||
|
return file.inputStream().use {
|
||||||
|
hash(
|
||||||
|
algorithm = algorithm,
|
||||||
|
inputStream = it,
|
||||||
|
size = fileSize,
|
||||||
|
onNewPercent = onNewPercent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,16 +5,22 @@ import kotlinx.coroutines.flow.*
|
|||||||
|
|
||||||
interface ContainsScreenState<ScreenState> {
|
interface ContainsScreenState<ScreenState> {
|
||||||
val state: StateFlow<ScreenState>
|
val state: StateFlow<ScreenState>
|
||||||
fun setState(state:ScreenState)
|
fun setState(state: ScreenState)
|
||||||
|
fun setState(updater: (ScreenState) -> ScreenState)
|
||||||
}
|
}
|
||||||
|
|
||||||
class SupportsScreenState<ScreenState>(
|
class SupportsScreenState<ScreenState>(
|
||||||
initialState:ScreenState
|
initialState: ScreenState
|
||||||
): ContainsScreenState<ScreenState> {
|
) : ContainsScreenState<ScreenState> {
|
||||||
private val _state = MutableStateFlow<ScreenState>(initialState)
|
private val _state = MutableStateFlow<ScreenState>(initialState)
|
||||||
override val state = _state.asStateFlow()
|
override val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
override fun setState(updater: (ScreenState) -> ScreenState) {
|
||||||
|
_state.update(updater)
|
||||||
|
}
|
||||||
|
|
||||||
override fun setState(state: ScreenState) {
|
override fun setState(state: ScreenState) {
|
||||||
_state.update { state }
|
setState { state }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,6 +232,23 @@ download_item_settings_username_description=Provide a username if the link is a
|
|||||||
download_item_settings_password_description=Provide a password if the link is a protected resource
|
download_item_settings_password_description=Provide a password if the link is a protected resource
|
||||||
download_item_settings_download_page=Download Page
|
download_item_settings_download_page=Download Page
|
||||||
download_item_settings_download_page_description=The webpage where this download was initiated
|
download_item_settings_download_page_description=The webpage where this download was initiated
|
||||||
|
download_item_settings_file_checksum=File Checksum
|
||||||
|
download_item_settings_file_checksum_description=A hash string which can be used to check if file is downloaded correctly
|
||||||
|
file_checksum=File Checksum
|
||||||
|
file_checksum_page=File Checksum Checker
|
||||||
|
file_checksum_page_file_checksum_default_algorithm=Default Algorithm
|
||||||
|
file_checksum_page_file_checksum_default_algorithm_help=The default algorithm used to calculate the checksum of the file if the file checksum is not provided
|
||||||
|
start=Start
|
||||||
|
calculated_checksum=Calculated Checksum
|
||||||
|
saved_checksum=Saved Checksum
|
||||||
|
checksum_algorithm=Algorithm
|
||||||
|
file_not_found=File not found
|
||||||
|
download_not_finished=Download not finished
|
||||||
|
done=Done
|
||||||
|
waiting=Waiting
|
||||||
|
matches=Matches
|
||||||
|
not_matches=Not Matches
|
||||||
|
copy_to_clipboard=Copy To Clipboard
|
||||||
username=Username
|
username=Username
|
||||||
password=Password
|
password=Password
|
||||||
average_speed=Average Speed
|
average_speed=Average Speed
|
||||||
@ -328,4 +345,4 @@ update_check_for_update=Check for Update
|
|||||||
update_checking_for_update=Checking for Update
|
update_checking_for_update=Checking for Update
|
||||||
update_no_update=You are using the latest version
|
update_no_update=You are using the latest version
|
||||||
update_check_error=Error while checking for update
|
update_check_error=Error while checking for update
|
||||||
update_app_updated_to_version_n=App updated to version {{version}}
|
update_app_updated_to_version_n=App updated to version {{version}}
|
||||||
|
@ -221,7 +221,7 @@ fun <T1, T2, T3, T4, T5, T6, T7, R> combineStateFlows(
|
|||||||
getValue = {
|
getValue = {
|
||||||
transform(a.value, b.value, c.value, d.value, e.value, f.value, g.value)
|
transform(a.value, b.value, c.value, d.value, e.value, f.value, g.value)
|
||||||
},
|
},
|
||||||
flow = combine(a, b, c, d, e, f) { array ->
|
flow = combine(a, b, c, d, e, f, g) { array ->
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
transform(
|
transform(
|
||||||
array[0] as T1,
|
array[0] as T1,
|
||||||
@ -260,4 +260,4 @@ inline fun <reified T, R> combineStateFlows(
|
|||||||
noinline transform: (list: Array<T>) -> R
|
noinline transform: (list: Array<T>) -> R
|
||||||
): StateFlow<R> {
|
): StateFlow<R> {
|
||||||
return combineStateFlows(listOf(*flows), transform)
|
return combineStateFlows(listOf(*flows), transform)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user