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.CategoryDialogManager
|
||||
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.queue.QueuesComponent
|
||||
import com.abdownloadmanager.desktop.pages.settings.SettingsComponent
|
||||
@ -80,6 +82,7 @@ class AppComponent(
|
||||
AddDownloadDialogManager,
|
||||
CategoryDialogManager,
|
||||
EditDownloadDialogManager,
|
||||
FileChecksumDialogManager,
|
||||
NotificationSender,
|
||||
DownloadItemOpener,
|
||||
ContainsEffects<AppEffects> by supportEffects(),
|
||||
@ -120,6 +123,7 @@ class AppComponent(
|
||||
downloadItemOpener = this,
|
||||
downloadDialogManager = this,
|
||||
addDownloadDialogManager = this,
|
||||
fileChecksumDialogManager = this,
|
||||
categoryDialogManager = this,
|
||||
notificationSender = 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(
|
||||
items: List<DownloadItem>,
|
||||
onDuplicateStrategy: (DownloadItem) -> OnDuplicateStrategy,
|
||||
@ -881,4 +921,10 @@ interface AddDownloadDialogManager {
|
||||
)
|
||||
|
||||
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.utils.*
|
||||
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.supportEffects
|
||||
import com.abdownloadmanager.resources.Res
|
||||
@ -214,6 +216,7 @@ class AddSingleDownloadComponent(
|
||||
//extra settings
|
||||
private var threadCount = MutableStateFlow(null as Int?)
|
||||
private var speedLimit = MutableStateFlow(0L)
|
||||
private var fileChecksum = MutableStateFlow(null as FileChecksum?)
|
||||
|
||||
|
||||
val downloadItem = combineStateFlows(
|
||||
@ -222,7 +225,8 @@ class AddSingleDownloadComponent(
|
||||
this.name,
|
||||
this.length,
|
||||
this.speedLimit,
|
||||
this.threadCount
|
||||
this.threadCount,
|
||||
this.fileChecksum,
|
||||
) {
|
||||
credentials,
|
||||
folder,
|
||||
@ -230,6 +234,7 @@ class AddSingleDownloadComponent(
|
||||
length,
|
||||
speedLimit,
|
||||
threadCount,
|
||||
fileChecksum,
|
||||
->
|
||||
DownloadItem(
|
||||
id = -1,
|
||||
@ -242,7 +247,8 @@ class AddSingleDownloadComponent(
|
||||
completeTime = null,
|
||||
status = DownloadStatus.Added,
|
||||
preferredConnectionCount = threadCount,
|
||||
speedLimit = speedLimit
|
||||
speedLimit = speedLimit,
|
||||
fileChecksum = fileChecksum?.toString()
|
||||
).withCredentials(credentials)
|
||||
}
|
||||
|
||||
@ -262,6 +268,12 @@ class AddSingleDownloadComponent(
|
||||
).asStringSource()
|
||||
}
|
||||
),
|
||||
FileChecksumConfigurable(
|
||||
Res.string.download_item_settings_file_checksum.asStringSource(),
|
||||
Res.string.download_item_settings_file_checksum_description.asStringSource(),
|
||||
backedBy = fileChecksum,
|
||||
describe = { "".asStringSource() }
|
||||
),
|
||||
IntConfigurable(
|
||||
Res.string.settings_download_thread_count.asStringSource(),
|
||||
Res.string.settings_download_thread_count_description.asStringSource(),
|
||||
@ -425,4 +437,4 @@ fun interface OnRequestDownloadSingleItem {
|
||||
onDuplicateStrategy: OnDuplicateStrategy,
|
||||
categoryId: Long?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -98,4 +98,4 @@ class EditDownloadComponent(
|
||||
fun bringToFront() {
|
||||
sendEffect(EditDownloadPageEffects.BringToFront)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
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.SpeedLimitConfigurable
|
||||
import com.abdownloadmanager.desktop.pages.settings.configurable.StringConfigurable
|
||||
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.shared.utils.isValidUrl
|
||||
import com.abdownloadmanager.shared.utils.*
|
||||
import ir.amirab.downloader.connection.DownloaderClient
|
||||
import ir.amirab.downloader.downloaditem.DownloadCredentials
|
||||
import ir.amirab.downloader.downloaditem.DownloadItem
|
||||
@ -169,6 +167,25 @@ class EditDownloadState(
|
||||
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(
|
||||
Res.string.settings_download_thread_count.asStringSource(),
|
||||
Res.string.settings_download_thread_count_description.asStringSource(),
|
||||
@ -335,4 +352,4 @@ class EditDownloadState(
|
||||
scheduleRefresh(alsoRecheckLink = false)
|
||||
}.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,
|
||||
downloadDialogManager: DownloadDialogManager,
|
||||
editDownloadDialogManager: EditDownloadDialogManager,
|
||||
fileChecksumDialogManager: FileChecksumDialogManager,
|
||||
val selections: StateFlow<List<IDownloadItemState>>,
|
||||
private val mainItem: StateFlow<Long?>,
|
||||
private val queueManager: QueueManager,
|
||||
@ -236,6 +237,18 @@ class DownloadActions(
|
||||
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(
|
||||
title = Res.string.move_to_queue.asStringSource(),
|
||||
@ -290,6 +303,7 @@ class DownloadActions(
|
||||
separator()
|
||||
+(copyDownloadLinkAction)
|
||||
+editDownloadAction
|
||||
+fileChecksumAction
|
||||
+(openDownloadDialogAction)
|
||||
}
|
||||
}
|
||||
@ -405,6 +419,7 @@ class HomeComponent(
|
||||
private val downloadDialogManager: DownloadDialogManager,
|
||||
private val editDownloadDialogManager: EditDownloadDialogManager,
|
||||
private val addDownloadDialogManager: AddDownloadDialogManager,
|
||||
private val fileChecksumDialogManager: FileChecksumDialogManager,
|
||||
private val categoryDialogManager: CategoryDialogManager,
|
||||
private val notificationSender: NotificationSender,
|
||||
) : BaseComponent(ctx),
|
||||
@ -917,6 +932,7 @@ class HomeComponent(
|
||||
downloadSystem = downloadSystem,
|
||||
downloadDialogManager = downloadDialogManager,
|
||||
editDownloadDialogManager = editDownloadDialogManager,
|
||||
fileChecksumDialogManager = fileChecksumDialogManager,
|
||||
selections = selectionListItems,
|
||||
mainItem = mainItem,
|
||||
queueManager = queueManager,
|
||||
@ -998,4 +1014,4 @@ class HomeComponent(
|
||||
private var homeComponentCreationCount = 0
|
||||
val CATEGORIES_SIZE_RANGE = 0.dp..500.dp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package com.abdownloadmanager.desktop.pages.settings.configurable
|
||||
|
||||
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 ir.amirab.util.compose.StringSource
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -26,7 +28,7 @@ sealed class Configurable<T>(
|
||||
if (validate(value)) {
|
||||
// don't use update function here maybe this is a mappedByTwoWayMutableStateFlow
|
||||
// IMPROVE
|
||||
backedBy.value=value
|
||||
backedBy.value = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -141,7 +143,7 @@ class FloatConfigurable(
|
||||
describe = describe,
|
||||
enabled = enabled,
|
||||
visible = visible,
|
||||
){
|
||||
) {
|
||||
enum class RenderMode {
|
||||
TextField,
|
||||
}
|
||||
@ -262,6 +264,22 @@ class SpeedLimitConfigurable(
|
||||
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(
|
||||
title: 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 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.credits.translators.ShowTranslators
|
||||
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.settings.ThemeManager
|
||||
import com.abdownloadmanager.desktop.pages.updater.ShowUpdaterDialog
|
||||
@ -93,6 +94,7 @@ object Ui : KoinComponent {
|
||||
ShowAddDownloadDialogs(appComponent)
|
||||
ShowDownloadDialogs(appComponent)
|
||||
ShowCategoryDialogs(appComponent)
|
||||
FileChecksumWindow(appComponent)
|
||||
ShowUpdaterDialog(appComponent.updater)
|
||||
ShowAboutDialog(appComponent)
|
||||
NewQueueDialog(appComponent)
|
||||
|
@ -24,6 +24,8 @@ data class DownloadItem(
|
||||
var status: DownloadStatus = DownloadStatus.Added,
|
||||
var preferredConnectionCount: Int? = null,
|
||||
var speedLimit: Long = 0,//0 is unlimited
|
||||
|
||||
var fileChecksum: String? = null,
|
||||
) : IDownloadCredentials {
|
||||
companion object {
|
||||
const val LENGTH_UNKNOWN = -1L
|
||||
@ -51,6 +53,8 @@ fun DownloadItem.applyFrom(other: DownloadItem) {
|
||||
status = other.status
|
||||
preferredConnectionCount = other.preferredConnectionCount
|
||||
speedLimit = other.speedLimit
|
||||
|
||||
fileChecksum = other.fileChecksum
|
||||
}
|
||||
|
||||
fun DownloadItem.withCredentials(credentials: IDownloadCredentials) = apply {
|
||||
|
@ -3,11 +3,21 @@ package ir.amirab.downloader.monitor
|
||||
import ir.amirab.downloader.downloaditem.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? {
|
||||
return (this as? ProcessingDownloadItemState)?.speed
|
||||
}
|
||||
|
||||
fun IDownloadItemState.remainingOrNull(): Long? {
|
||||
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> {
|
||||
val state: StateFlow<ScreenState>
|
||||
fun setState(state:ScreenState)
|
||||
fun setState(state: ScreenState)
|
||||
fun setState(updater: (ScreenState) -> ScreenState)
|
||||
}
|
||||
|
||||
class SupportsScreenState<ScreenState>(
|
||||
initialState:ScreenState
|
||||
): ContainsScreenState<ScreenState> {
|
||||
initialState: ScreenState
|
||||
) : ContainsScreenState<ScreenState> {
|
||||
private val _state = MutableStateFlow<ScreenState>(initialState)
|
||||
override val state = _state.asStateFlow()
|
||||
|
||||
override fun setState(updater: (ScreenState) -> ScreenState) {
|
||||
_state.update(updater)
|
||||
}
|
||||
|
||||
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_download_page=Download Page
|
||||
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
|
||||
password=Password
|
||||
average_speed=Average Speed
|
||||
@ -328,4 +345,4 @@ update_check_for_update=Check for Update
|
||||
update_checking_for_update=Checking for Update
|
||||
update_no_update=You are using the latest version
|
||||
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 = {
|
||||
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")
|
||||
transform(
|
||||
array[0] as T1,
|
||||
@ -260,4 +260,4 @@ inline fun <reified T, R> combineStateFlows(
|
||||
noinline transform: (list: Array<T>) -> R
|
||||
): StateFlow<R> {
|
||||
return combineStateFlows(listOf(*flows), transform)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user