mirror of
https://github.com/amir1376/ab-download-manager.git
synced 2025-02-20 11:43:24 +08:00
add speed unit (#339)
This commit is contained in:
parent
a2077c7e4b
commit
f106c9b702
@ -81,7 +81,7 @@ class AppComponent(
|
|||||||
DownloadItemOpener,
|
DownloadItemOpener,
|
||||||
ContainsEffects<AppEffects> by supportEffects(),
|
ContainsEffects<AppEffects> by supportEffects(),
|
||||||
KoinComponent {
|
KoinComponent {
|
||||||
private val appRepository: AppRepository by inject()
|
val appRepository: AppRepository by inject()
|
||||||
private val appSettings: AppSettingsStorage by inject()
|
private val appSettings: AppSettingsStorage by inject()
|
||||||
private val integration: Integration by inject()
|
private val integration: Integration by inject()
|
||||||
|
|
||||||
|
@ -296,7 +296,7 @@ private fun SizeCell(
|
|||||||
val length by downloadChecker.length.collectAsState()
|
val length by downloadChecker.length.collectAsState()
|
||||||
CellText(
|
CellText(
|
||||||
length?.let {
|
length?.let {
|
||||||
convertSizeToHumanReadable(it).rememberString()
|
convertPositiveSizeToHumanReadable(it, LocalSizeUnit.current).rememberString()
|
||||||
} ?: ""
|
} ?: ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -559,7 +559,7 @@ fun RenderFileTypeAndSize(
|
|||||||
iconModifier
|
iconModifier
|
||||||
)
|
)
|
||||||
val size = fileInfo.totalLength?.let {
|
val size = fileInfo.totalLength?.let {
|
||||||
convertSizeToHumanReadable(it)
|
convertPositiveSizeToHumanReadable(it, LocalSizeUnit.current)
|
||||||
}.takeIf {
|
}.takeIf {
|
||||||
// this is a length of a html page (error)
|
// this is a length of a html page (error)
|
||||||
fileInfo.isSuccessFul
|
fileInfo.isSuccessFul
|
||||||
|
@ -257,7 +257,9 @@ class AddSingleDownloadComponent(
|
|||||||
backedBy = speedLimit,
|
backedBy = speedLimit,
|
||||||
describe = {
|
describe = {
|
||||||
if (it == 0L) Res.string.unlimited.asStringSource()
|
if (it == 0L) Res.string.unlimited.asStringSource()
|
||||||
else convertSpeedToHumanReadable(it).asStringSource()
|
else convertPositiveSpeedToHumanReadable(
|
||||||
|
it, appSettings.speedUnit.value
|
||||||
|
).asStringSource()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
IntConfigurable(
|
IntConfigurable(
|
||||||
|
@ -462,7 +462,7 @@ private fun RenderFileTypeAndSize(
|
|||||||
iconModifier
|
iconModifier
|
||||||
)
|
)
|
||||||
val size = fileInfo.totalLength?.let {
|
val size = fileInfo.totalLength?.let {
|
||||||
convertSizeToHumanReadable(it)
|
convertPositiveSizeToHumanReadable(it, LocalSizeUnit.current)
|
||||||
}.takeIf {
|
}.takeIf {
|
||||||
// this is a length of a html page (error)
|
// this is a length of a html page (error)
|
||||||
fileInfo.isSuccessFul
|
fileInfo.isSuccessFul
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.abdownloadmanager.desktop.pages.editdownload
|
package com.abdownloadmanager.desktop.pages.editdownload
|
||||||
|
|
||||||
|
import com.abdownloadmanager.desktop.repository.AppRepository
|
||||||
import com.abdownloadmanager.desktop.utils.*
|
import com.abdownloadmanager.desktop.utils.*
|
||||||
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
||||||
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
||||||
@ -30,6 +31,7 @@ class EditDownloadComponent(
|
|||||||
private val downloaderClient: DownloaderClient by inject()
|
private val downloaderClient: DownloaderClient by inject()
|
||||||
val iconProvider: FileIconProvider by inject()
|
val iconProvider: FileIconProvider by inject()
|
||||||
val downloadSystem: DownloadSystem by inject()
|
val downloadSystem: DownloadSystem by inject()
|
||||||
|
private val appRepository: AppRepository by inject()
|
||||||
val editDownloadUiChecker = MutableStateFlow(null as EditDownloadState?)
|
val editDownloadUiChecker = MutableStateFlow(null as EditDownloadState?)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -73,7 +75,8 @@ class EditDownloadComponent(
|
|||||||
.contains(editedDownloadFile)
|
.contains(editedDownloadFile)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scope,
|
scope = scope,
|
||||||
|
appRepository = appRepository,
|
||||||
)
|
)
|
||||||
editDownloadUiChecker.value = editDownloadState
|
editDownloadUiChecker.value = editDownloadState
|
||||||
pendingCredential?.let { credentials ->
|
pendingCredential?.let { credentials ->
|
||||||
|
@ -3,9 +3,10 @@ package com.abdownloadmanager.desktop.pages.editdownload
|
|||||||
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.utils.FileNameValidator
|
import com.abdownloadmanager.desktop.utils.FileNameValidator
|
||||||
import com.abdownloadmanager.desktop.utils.LinkChecker
|
import com.abdownloadmanager.desktop.utils.LinkChecker
|
||||||
import com.abdownloadmanager.desktop.utils.convertSpeedToHumanReadable
|
import com.abdownloadmanager.desktop.utils.convertPositiveSpeedToHumanReadable
|
||||||
import com.abdownloadmanager.resources.Res
|
import com.abdownloadmanager.resources.Res
|
||||||
import com.abdownloadmanager.utils.isValidUrl
|
import com.abdownloadmanager.utils.isValidUrl
|
||||||
import ir.amirab.downloader.connection.DownloaderClient
|
import ir.amirab.downloader.connection.DownloaderClient
|
||||||
@ -16,7 +17,6 @@ import ir.amirab.downloader.downloaditem.withCredentials
|
|||||||
import ir.amirab.util.compose.StringSource
|
import ir.amirab.util.compose.StringSource
|
||||||
import ir.amirab.util.compose.asStringSource
|
import ir.amirab.util.compose.asStringSource
|
||||||
import ir.amirab.util.compose.asStringSourceWithARgs
|
import ir.amirab.util.compose.asStringSourceWithARgs
|
||||||
import ir.amirab.util.flow.createMutableStateFlowFromStateFlow
|
|
||||||
import ir.amirab.util.flow.mapStateFlow
|
import ir.amirab.util.flow.mapStateFlow
|
||||||
import ir.amirab.util.flow.mapTwoWayStateFlow
|
import ir.amirab.util.flow.mapTwoWayStateFlow
|
||||||
import ir.amirab.util.flow.onEachLatest
|
import ir.amirab.util.flow.onEachLatest
|
||||||
@ -126,6 +126,7 @@ class EditDownloadState(
|
|||||||
val currentDownloadItem: MutableStateFlow<DownloadItem>,
|
val currentDownloadItem: MutableStateFlow<DownloadItem>,
|
||||||
val editedDownloadItem: MutableStateFlow<DownloadItem>,
|
val editedDownloadItem: MutableStateFlow<DownloadItem>,
|
||||||
val downloaderClient: DownloaderClient,
|
val downloaderClient: DownloaderClient,
|
||||||
|
val appRepository: AppRepository,
|
||||||
conflictDetector: DownloadConflictDetector,
|
conflictDetector: DownloadConflictDetector,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
@ -165,7 +166,7 @@ class EditDownloadState(
|
|||||||
),
|
),
|
||||||
describe = {
|
describe = {
|
||||||
if (it == 0L) Res.string.unlimited.asStringSource()
|
if (it == 0L) Res.string.unlimited.asStringSource()
|
||||||
else convertSpeedToHumanReadable(it).asStringSource()
|
else convertPositiveSpeedToHumanReadable(it, appRepository.speedUnit.value).asStringSource()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
IntConfigurable(
|
IntConfigurable(
|
||||||
|
@ -26,7 +26,6 @@ import androidx.compose.ui.draw.alpha
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ir.amirab.downloader.utils.ByteConverter
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import com.abdownloadmanager.desktop.ui.widget.ActionButton
|
import com.abdownloadmanager.desktop.ui.widget.ActionButton
|
||||||
@ -735,14 +734,10 @@ private fun Footer(component: HomeComponent) {
|
|||||||
val activeCount by component.activeDownloadCountFlow.collectAsState()
|
val activeCount by component.activeDownloadCountFlow.collectAsState()
|
||||||
FooterItem(MyIcons.activeCount, activeCount.toString(), "")
|
FooterItem(MyIcons.activeCount, activeCount.toString(), "")
|
||||||
val size by component.globalSpeedFlow.collectAsState(0)
|
val size by component.globalSpeedFlow.collectAsState(0)
|
||||||
val speed = baseConvertBytesToHumanReadable(size)
|
val speed = convertPositiveBytesToSizeUnit(size, LocalSpeedUnit.current)
|
||||||
if (speed != null) {
|
if (speed != null) {
|
||||||
val speedText = ByteConverter.prettify(speed.value)
|
val speedText = speed.formatedValue()
|
||||||
val unitText = ByteConverter.unitPrettify(speed.unit)
|
val unitText = speed.unit.toString() + "/s"
|
||||||
?.let {
|
|
||||||
"$it/s"
|
|
||||||
}
|
|
||||||
.orEmpty()
|
|
||||||
FooterItem(MyIcons.speed, speedText, unitText)
|
FooterItem(MyIcons.speed, speedText, unitText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ import androidx.compose.ui.graphics.Brush
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.abdownloadmanager.resources.Res
|
import com.abdownloadmanager.resources.Res
|
||||||
import com.abdownloadmanager.resources.*
|
|
||||||
import com.abdownloadmanager.utils.FileIconProvider
|
import com.abdownloadmanager.utils.FileIconProvider
|
||||||
import com.abdownloadmanager.utils.category.Category
|
import com.abdownloadmanager.utils.category.Category
|
||||||
import ir.amirab.util.compose.resources.myStringResource
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
@ -175,7 +174,10 @@ fun SpeedCell(
|
|||||||
(itemState as? ProcessingDownloadItemState)?.speed?.let { remaining ->
|
(itemState as? ProcessingDownloadItemState)?.speed?.let { remaining ->
|
||||||
if (itemState.status == DownloadJobStatus.Downloading) {
|
if (itemState.status == DownloadJobStatus.Downloading) {
|
||||||
Text(
|
Text(
|
||||||
text = convertSpeedToHumanReadable(remaining),
|
text = convertPositiveSpeedToHumanReadable(
|
||||||
|
remaining,
|
||||||
|
LocalSpeedUnit.current,
|
||||||
|
),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
@ -190,7 +192,10 @@ fun SizeCell(
|
|||||||
) {
|
) {
|
||||||
item.contentLength.let {
|
item.contentLength.let {
|
||||||
Text(
|
Text(
|
||||||
convertSizeToHumanReadable(it).rememberString(),
|
convertPositiveSizeToHumanReadable(
|
||||||
|
it,
|
||||||
|
LocalSizeUnit.current
|
||||||
|
).rememberString(),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
@ -7,7 +7,7 @@ import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
|||||||
import ir.amirab.util.compose.IconSource
|
import ir.amirab.util.compose.IconSource
|
||||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||||
import com.abdownloadmanager.desktop.utils.BaseComponent
|
import com.abdownloadmanager.desktop.utils.BaseComponent
|
||||||
import com.abdownloadmanager.desktop.utils.convertSpeedToHumanReadable
|
import com.abdownloadmanager.desktop.utils.convertPositiveSpeedToHumanReadable
|
||||||
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
||||||
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
@ -20,6 +20,8 @@ import ir.amirab.util.compose.asStringSource
|
|||||||
import ir.amirab.util.compose.asStringSourceWithARgs
|
import ir.amirab.util.compose.asStringSourceWithARgs
|
||||||
import ir.amirab.util.compose.localizationmanager.LanguageInfo
|
import ir.amirab.util.compose.localizationmanager.LanguageInfo
|
||||||
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
||||||
|
import ir.amirab.util.datasize.CommonSizeConvertConfigs
|
||||||
|
import ir.amirab.util.datasize.ConvertSizeConfig
|
||||||
import ir.amirab.util.osfileutil.FileUtils
|
import ir.amirab.util.osfileutil.FileUtils
|
||||||
import ir.amirab.util.flow.createMutableStateFlowFromStateFlow
|
import ir.amirab.util.flow.createMutableStateFlowFromStateFlow
|
||||||
import ir.amirab.util.flow.mapStateFlow
|
import ir.amirab.util.flow.mapStateFlow
|
||||||
@ -121,6 +123,26 @@ fun trackDeletedFilesOnDisk(appRepository: AppRepository): BooleanConfigurable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun speedUnit(appRepository: AppRepository, scope: CoroutineScope): EnumConfigurable<ConvertSizeConfig> {
|
||||||
|
return EnumConfigurable(
|
||||||
|
title = Res.string.settings_download_speed_unit.asStringSource(),
|
||||||
|
description = Res.string.settings_download_speed_unit_description.asStringSource(),
|
||||||
|
backedBy = createMutableStateFlowFromStateFlow(
|
||||||
|
appRepository.speedUnit,
|
||||||
|
updater = { appRepository.setSpeedUnit(it) },
|
||||||
|
scope = scope
|
||||||
|
),
|
||||||
|
possibleValues = listOf(
|
||||||
|
CommonSizeConvertConfigs.BinaryBytes,
|
||||||
|
CommonSizeConvertConfigs.BinaryBits,
|
||||||
|
),
|
||||||
|
describe = {
|
||||||
|
val u = it.baseSize.longString()
|
||||||
|
"$u/s".asStringSource()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun showDownloadFinishWindow(settingsStorage: AppSettingsStorage): BooleanConfigurable {
|
fun showDownloadFinishWindow(settingsStorage: AppSettingsStorage): BooleanConfigurable {
|
||||||
return BooleanConfigurable(
|
return BooleanConfigurable(
|
||||||
title = Res.string.settings_show_completion_dialog.asStringSource(),
|
title = Res.string.settings_show_completion_dialog.asStringSource(),
|
||||||
@ -152,7 +174,7 @@ fun speedLimitConfig(appRepository: AppRepository): SpeedLimitConfigurable {
|
|||||||
if (it == 0L) {
|
if (it == 0L) {
|
||||||
Res.string.unlimited.asStringSource()
|
Res.string.unlimited.asStringSource()
|
||||||
} else {
|
} else {
|
||||||
convertSpeedToHumanReadable(it).asStringSource()
|
convertPositiveSpeedToHumanReadable(it, appRepository.speedUnit.value).asStringSource()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -398,6 +420,7 @@ class SettingsComponent(
|
|||||||
uiScaleConfig(appSettings),
|
uiScaleConfig(appSettings),
|
||||||
autoStartConfig(appSettings),
|
autoStartConfig(appSettings),
|
||||||
mergeTopBarWithTitleBarConfig(appSettings),
|
mergeTopBarWithTitleBarConfig(appSettings),
|
||||||
|
speedUnit(appRepository, scope),
|
||||||
playSoundNotification(appSettings),
|
playSoundNotification(appSettings),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ package com.abdownloadmanager.desktop.pages.settings.configurable.widgets
|
|||||||
import com.abdownloadmanager.desktop.pages.settings.configurable.SpeedLimitConfigurable
|
import com.abdownloadmanager.desktop.pages.settings.configurable.SpeedLimitConfigurable
|
||||||
import com.abdownloadmanager.desktop.ui.widget.CheckBox
|
import com.abdownloadmanager.desktop.ui.widget.CheckBox
|
||||||
import com.abdownloadmanager.desktop.ui.widget.DoubleTextField
|
import com.abdownloadmanager.desktop.ui.widget.DoubleTextField
|
||||||
import com.abdownloadmanager.desktop.utils.baseConvertBytesToHumanReadable
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import com.abdownloadmanager.desktop.ui.widget.Text
|
import com.abdownloadmanager.desktop.ui.widget.Text
|
||||||
@ -11,35 +10,48 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ir.amirab.downloader.utils.ByteConverter
|
import com.abdownloadmanager.desktop.utils.LocalSpeedUnit
|
||||||
import ir.amirab.downloader.utils.ByteConverter.BYTES
|
import ir.amirab.util.datasize.*
|
||||||
import ir.amirab.downloader.utils.ByteConverter.K_BYTES
|
|
||||||
import ir.amirab.downloader.utils.ByteConverter.M_BYTES
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderSpeedConfig(cfg: SpeedLimitConfigurable, modifier: Modifier) {
|
fun RenderSpeedConfig(cfg: SpeedLimitConfigurable, modifier: Modifier) {
|
||||||
val value by cfg.stateFlow.collectAsState()
|
val value by cfg.stateFlow.collectAsState()
|
||||||
val setValue = cfg::set
|
val setValue = cfg::set
|
||||||
val units = listOf(
|
|
||||||
BYTES,
|
|
||||||
K_BYTES,
|
|
||||||
M_BYTES,
|
|
||||||
)
|
|
||||||
val enabled= isConfigEnabled()
|
|
||||||
val hasLimitSpeed = value != 0L
|
|
||||||
|
|
||||||
var currentUnit by remember(hasLimitSpeed) { mutableStateOf(baseConvertBytesToHumanReadable(value)?.unit ?: BYTES) }
|
val speedUnit = LocalSpeedUnit.current
|
||||||
|
val allowedFactors = listOf(
|
||||||
|
SizeFactors.FactorValue.Kilo,
|
||||||
|
SizeFactors.FactorValue.Mega,
|
||||||
|
)
|
||||||
|
val units = allowedFactors.map {
|
||||||
|
SizeUnit(
|
||||||
|
factorValue = it,
|
||||||
|
baseSize = speedUnit.baseSize,
|
||||||
|
factors = speedUnit.factors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val enabled = isConfigEnabled()
|
||||||
|
val hasLimitSpeed = value > 0L
|
||||||
|
|
||||||
|
var currentUnit by remember(hasLimitSpeed) {
|
||||||
|
mutableStateOf(
|
||||||
|
SizeConverter.bytesToSize(
|
||||||
|
value,
|
||||||
|
speedUnit.copy(acceptedFactors = allowedFactors)
|
||||||
|
).unit
|
||||||
|
)
|
||||||
|
}
|
||||||
var currentValue by remember(value) {
|
var currentValue by remember(value) {
|
||||||
val v = ByteConverter.run {
|
val v = SizeConverter.bytesToSize(
|
||||||
prettify(
|
value, currentUnit.asConverterConfig()
|
||||||
byteTo(value, currentUnit)
|
).formatedValue().toDouble()
|
||||||
).toDouble()
|
|
||||||
}
|
|
||||||
mutableStateOf(v)
|
mutableStateOf(v)
|
||||||
}
|
}
|
||||||
LaunchedEffect(currentValue, currentUnit) {
|
LaunchedEffect(currentValue, currentUnit) {
|
||||||
setValue(
|
setValue(
|
||||||
ByteConverter.unitToByte(currentValue, currentUnit)
|
SizeConverter.sizeToBytes(
|
||||||
|
SizeWithUnit(currentValue, currentUnit),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ConfigTemplate(
|
ConfigTemplate(
|
||||||
@ -77,7 +89,7 @@ fun RenderSpeedConfig(cfg: SpeedLimitConfigurable, modifier: Modifier) {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val prettified = remember(it) {
|
val prettified = remember(it) {
|
||||||
ByteConverter.unitPrettify(it) + "/s"
|
"$it/s"
|
||||||
}
|
}
|
||||||
Text(prettified)
|
Text(prettified)
|
||||||
}
|
}
|
||||||
@ -90,12 +102,22 @@ fun RenderSpeedConfig(cfg: SpeedLimitConfigurable, modifier: Modifier) {
|
|||||||
value = hasLimitSpeed,
|
value = hasLimitSpeed,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
if (it) {
|
if (it) {
|
||||||
setValue(ByteConverter.unitToByte(10.0, K_BYTES))
|
setValue(
|
||||||
} else {
|
SizeConverter.sizeToBytes(
|
||||||
setValue(0)
|
SizeWithUnit(
|
||||||
}
|
256.0, SizeUnit(
|
||||||
})
|
SizeFactors.FactorValue.Kilo,
|
||||||
|
BaseSize.Bytes,
|
||||||
|
SizeFactors.BinarySizeFactors,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setValue(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -2,14 +2,10 @@ package com.abdownloadmanager.desktop.pages.singleDownloadPage
|
|||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.basicMarquee
|
import androidx.compose.foundation.basicMarquee
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.onClick
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||||
@ -17,7 +13,8 @@ import com.abdownloadmanager.desktop.ui.theme.myColors
|
|||||||
import com.abdownloadmanager.desktop.ui.theme.myTextSizes
|
import com.abdownloadmanager.desktop.ui.theme.myTextSizes
|
||||||
import com.abdownloadmanager.desktop.ui.widget.ActionButton
|
import com.abdownloadmanager.desktop.ui.widget.ActionButton
|
||||||
import com.abdownloadmanager.desktop.ui.widget.Text
|
import com.abdownloadmanager.desktop.ui.widget.Text
|
||||||
import com.abdownloadmanager.desktop.utils.convertSizeToHumanReadable
|
import com.abdownloadmanager.desktop.utils.LocalSizeUnit
|
||||||
|
import com.abdownloadmanager.desktop.utils.convertPositiveSizeToHumanReadable
|
||||||
import com.abdownloadmanager.desktop.utils.div
|
import com.abdownloadmanager.desktop.utils.div
|
||||||
import com.abdownloadmanager.resources.Res
|
import com.abdownloadmanager.resources.Res
|
||||||
import com.abdownloadmanager.utils.compose.WithContentColor
|
import com.abdownloadmanager.utils.compose.WithContentColor
|
||||||
@ -150,8 +147,10 @@ private fun RenderFileIconAndSize(
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = convertSizeToHumanReadable(itemState.contentLength)
|
text = convertPositiveSizeToHumanReadable(
|
||||||
.rememberString(),
|
itemState.contentLength,
|
||||||
|
LocalSizeUnit.current,
|
||||||
|
).rememberString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -342,13 +342,18 @@ fun ColumnScope.RenderPartInfo(itemState: ProcessingDownloadItemState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PartInfoCells.Downloaded -> {
|
PartInfoCells.Downloaded -> {
|
||||||
SimpleCellText(convertSizeToHumanReadable(it.value.howMuchProceed).rememberString())
|
SimpleCellText(
|
||||||
|
convertPositiveSizeToHumanReadable(
|
||||||
|
it.value.howMuchProceed,
|
||||||
|
LocalSizeUnit.current
|
||||||
|
).rememberString()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PartInfoCells.Total -> {
|
PartInfoCells.Total -> {
|
||||||
SimpleCellText(
|
SimpleCellText(
|
||||||
it.value.length?.let { length ->
|
it.value.length?.let { length ->
|
||||||
convertSizeToHumanReadable(length).rememberString()
|
convertPositiveSizeToHumanReadable(length, LocalSizeUnit.current).rememberString()
|
||||||
} ?: myStringResource(Res.string.unknown),
|
} ?: myStringResource(Res.string.unknown),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
|||||||
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
||||||
import arrow.optics.copy
|
import arrow.optics.copy
|
||||||
import com.abdownloadmanager.desktop.pages.settings.configurable.BooleanConfigurable
|
import com.abdownloadmanager.desktop.pages.settings.configurable.BooleanConfigurable
|
||||||
|
import com.abdownloadmanager.desktop.repository.AppRepository
|
||||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
||||||
import com.abdownloadmanager.desktop.storage.PageStatesStorage
|
import com.abdownloadmanager.desktop.storage.PageStatesStorage
|
||||||
import com.abdownloadmanager.resources.Res
|
import com.abdownloadmanager.resources.Res
|
||||||
@ -55,6 +56,7 @@ class SingleDownloadComponent(
|
|||||||
KoinComponent {
|
KoinComponent {
|
||||||
private val downloadSystem: DownloadSystem by inject()
|
private val downloadSystem: DownloadSystem by inject()
|
||||||
private val appSettings: AppSettingsStorage by inject()
|
private val appSettings: AppSettingsStorage by inject()
|
||||||
|
private val appRepository: AppRepository by inject()
|
||||||
val fileIconProvider: FileIconProvider by inject()
|
val fileIconProvider: FileIconProvider by inject()
|
||||||
private val singleDownloadPageStateToPersist by lazy {
|
private val singleDownloadPageStateToPersist by lazy {
|
||||||
get<PageStatesStorage>().downloadPage
|
get<PageStatesStorage>().downloadPage
|
||||||
@ -122,19 +124,19 @@ class SingleDownloadComponent(
|
|||||||
add(
|
add(
|
||||||
SingleDownloadPagePropertyItem(
|
SingleDownloadPagePropertyItem(
|
||||||
Res.string.size.asStringSource(),
|
Res.string.size.asStringSource(),
|
||||||
convertSizeToHumanReadable(it.contentLength)
|
convertPositiveSizeToHumanReadable(it.contentLength, appRepository.sizeUnit.value)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
SingleDownloadPagePropertyItem(
|
SingleDownloadPagePropertyItem(
|
||||||
Res.string.download_page_downloaded_size.asStringSource(),
|
Res.string.download_page_downloaded_size.asStringSource(),
|
||||||
convertBytesToHumanReadable(it.progress).orEmpty().asStringSource()
|
convertPositiveSizeToHumanReadable(it.progress, appRepository.sizeUnit.value)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
SingleDownloadPagePropertyItem(
|
SingleDownloadPagePropertyItem(
|
||||||
Res.string.speed.asStringSource(),
|
Res.string.speed.asStringSource(),
|
||||||
convertSpeedToHumanReadable(it.speed).asStringSource()
|
convertPositiveSpeedToHumanReadable(it.speed, appRepository.speedUnit.value).asStringSource()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
@ -331,7 +333,7 @@ class SingleDownloadComponent(
|
|||||||
if (it == 0L) {
|
if (it == 0L) {
|
||||||
Res.string.unlimited.asStringSource()
|
Res.string.unlimited.asStringSource()
|
||||||
} else {
|
} else {
|
||||||
convertSpeedToHumanReadable(it).asStringSource()
|
convertPositiveSpeedToHumanReadable(it, appRepository.speedUnit.value).asStringSource()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.abdownloadmanager.desktop.repository
|
package com.abdownloadmanager.desktop.repository
|
||||||
|
|
||||||
|
import ir.amirab.util.datasize.CommonSizeConvertConfigs
|
||||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
||||||
import com.abdownloadmanager.desktop.utils.AutoStartManager
|
import com.abdownloadmanager.desktop.utils.AutoStartManager
|
||||||
import com.abdownloadmanager.utils.DownloadSystem
|
import com.abdownloadmanager.utils.DownloadSystem
|
||||||
@ -11,12 +12,12 @@ import com.abdownloadmanager.utils.category.CategoryManager
|
|||||||
import com.abdownloadmanager.utils.proxy.ProxyManager
|
import com.abdownloadmanager.utils.proxy.ProxyManager
|
||||||
import ir.amirab.downloader.DownloadManager
|
import ir.amirab.downloader.DownloadManager
|
||||||
import ir.amirab.downloader.monitor.IDownloadMonitor
|
import ir.amirab.downloader.monitor.IDownloadMonitor
|
||||||
|
import ir.amirab.util.datasize.BaseSize
|
||||||
|
import ir.amirab.util.datasize.ConvertSizeConfig
|
||||||
|
import ir.amirab.util.flow.mapStateFlow
|
||||||
import ir.amirab.util.flow.withPrevious
|
import ir.amirab.util.flow.withPrevious
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
@ -45,6 +46,20 @@ class AppRepository : KoinComponent {
|
|||||||
val integrationEnabled = appSettings.browserIntegrationEnabled
|
val integrationEnabled = appSettings.browserIntegrationEnabled
|
||||||
val integrationPort = appSettings.browserIntegrationPort
|
val integrationPort = appSettings.browserIntegrationPort
|
||||||
val trackDeletedFilesOnDisk = appSettings.trackDeletedFilesOnDisk
|
val trackDeletedFilesOnDisk = appSettings.trackDeletedFilesOnDisk
|
||||||
|
val sizeUnit = MutableStateFlow(
|
||||||
|
CommonSizeConvertConfigs.BinaryBytes
|
||||||
|
)
|
||||||
|
val speedUnit = appSettings.useBitsForSpeed.mapStateFlow { useBits ->
|
||||||
|
if (useBits) {
|
||||||
|
CommonSizeConvertConfigs.BinaryBits
|
||||||
|
} else {
|
||||||
|
CommonSizeConvertConfigs.BinaryBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSpeedUnit(speedUnit: ConvertSizeConfig) {
|
||||||
|
appSettings.useBitsForSpeed.value = speedUnit.baseSize == BaseSize.Bits
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
saveLocation
|
saveLocation
|
||||||
|
@ -7,6 +7,7 @@ import arrow.optics.optics
|
|||||||
import com.abdownloadmanager.desktop.App
|
import com.abdownloadmanager.desktop.App
|
||||||
import ir.amirab.util.compose.localizationmanager.LanguageStorage
|
import ir.amirab.util.compose.localizationmanager.LanguageStorage
|
||||||
import ir.amirab.util.config.*
|
import ir.amirab.util.config.*
|
||||||
|
import ir.amirab.util.datasize.BaseSize
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -34,6 +35,7 @@ data class AppSettingsModel(
|
|||||||
val browserIntegrationEnabled: Boolean = true,
|
val browserIntegrationEnabled: Boolean = true,
|
||||||
val browserIntegrationPort: Int = 15151,
|
val browserIntegrationPort: Int = 15151,
|
||||||
val trackDeletedFilesOnDisk: Boolean = false,
|
val trackDeletedFilesOnDisk: Boolean = false,
|
||||||
|
val useBitsForSpeed: Boolean = false,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val default: AppSettingsModel get() = AppSettingsModel()
|
val default: AppSettingsModel get() = AppSettingsModel()
|
||||||
@ -59,6 +61,7 @@ data class AppSettingsModel(
|
|||||||
val browserIntegrationEnabled = booleanKeyOf("browserIntegrationEnabled")
|
val browserIntegrationEnabled = booleanKeyOf("browserIntegrationEnabled")
|
||||||
val browserIntegrationPort = intKeyOf("browserIntegrationPort")
|
val browserIntegrationPort = intKeyOf("browserIntegrationPort")
|
||||||
val trackDeletedFilesOnDisk = booleanKeyOf("trackDeletedFilesOnDisk")
|
val trackDeletedFilesOnDisk = booleanKeyOf("trackDeletedFilesOnDisk")
|
||||||
|
val useBitsForSpeed = booleanKeyOf("useBitsForSpeed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -87,6 +90,7 @@ data class AppSettingsModel(
|
|||||||
?: default.browserIntegrationEnabled,
|
?: default.browserIntegrationEnabled,
|
||||||
browserIntegrationPort = source.get(Keys.browserIntegrationPort) ?: default.browserIntegrationPort,
|
browserIntegrationPort = source.get(Keys.browserIntegrationPort) ?: default.browserIntegrationPort,
|
||||||
trackDeletedFilesOnDisk = source.get(Keys.trackDeletedFilesOnDisk) ?: default.trackDeletedFilesOnDisk,
|
trackDeletedFilesOnDisk = source.get(Keys.trackDeletedFilesOnDisk) ?: default.trackDeletedFilesOnDisk,
|
||||||
|
useBitsForSpeed = source.get(Keys.useBitsForSpeed) ?: default.useBitsForSpeed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +114,7 @@ data class AppSettingsModel(
|
|||||||
put(Keys.browserIntegrationEnabled, focus.browserIntegrationEnabled)
|
put(Keys.browserIntegrationEnabled, focus.browserIntegrationEnabled)
|
||||||
put(Keys.browserIntegrationPort, focus.browserIntegrationPort)
|
put(Keys.browserIntegrationPort, focus.browserIntegrationPort)
|
||||||
put(Keys.trackDeletedFilesOnDisk, focus.trackDeletedFilesOnDisk)
|
put(Keys.trackDeletedFilesOnDisk, focus.trackDeletedFilesOnDisk)
|
||||||
|
put(Keys.useBitsForSpeed, focus.useBitsForSpeed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,4 +153,5 @@ class AppSettingsStorage(
|
|||||||
val browserIntegrationEnabled = from(AppSettingsModel.browserIntegrationEnabled)
|
val browserIntegrationEnabled = from(AppSettingsModel.browserIntegrationEnabled)
|
||||||
val browserIntegrationPort = from(AppSettingsModel.browserIntegrationPort)
|
val browserIntegrationPort = from(AppSettingsModel.browserIntegrationPort)
|
||||||
val trackDeletedFilesOnDisk = from(AppSettingsModel.trackDeletedFilesOnDisk)
|
val trackDeletedFilesOnDisk = from(AppSettingsModel.trackDeletedFilesOnDisk)
|
||||||
|
val useBitsForSpeed = from(AppSettingsModel.useBitsForSpeed)
|
||||||
}
|
}
|
@ -14,11 +14,7 @@ import com.abdownloadmanager.desktop.pages.singleDownloadPage.ShowDownloadDialog
|
|||||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||||
import com.abdownloadmanager.desktop.ui.theme.ABDownloaderTheme
|
import com.abdownloadmanager.desktop.ui.theme.ABDownloaderTheme
|
||||||
import com.abdownloadmanager.desktop.ui.widget.tray.ComposeTray
|
import com.abdownloadmanager.desktop.ui.widget.tray.ComposeTray
|
||||||
import com.abdownloadmanager.desktop.utils.AppInfo
|
|
||||||
import com.abdownloadmanager.desktop.utils.GlobalAppExceptionHandler
|
|
||||||
import com.abdownloadmanager.desktop.utils.ProvideGlobalExceptionHandler
|
|
||||||
import ir.amirab.util.compose.action.buildMenu
|
import ir.amirab.util.compose.action.buildMenu
|
||||||
import com.abdownloadmanager.desktop.utils.isInDebugMode
|
|
||||||
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
|
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.window.*
|
import androidx.compose.ui.window.*
|
||||||
@ -31,6 +27,7 @@ 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
|
||||||
import com.abdownloadmanager.desktop.ui.widget.*
|
import com.abdownloadmanager.desktop.ui.widget.*
|
||||||
|
import com.abdownloadmanager.desktop.utils.*
|
||||||
import com.abdownloadmanager.utils.compose.ProvideDebugInfo
|
import com.abdownloadmanager.utils.compose.ProvideDebugInfo
|
||||||
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -56,6 +53,7 @@ object Ui : KoinComponent {
|
|||||||
}
|
}
|
||||||
application {
|
application {
|
||||||
val theme by themeManager.currentThemeColor.collectAsState()
|
val theme by themeManager.currentThemeColor.collectAsState()
|
||||||
|
|
||||||
ProvideDebugInfo(AppInfo.isInDebugMode()) {
|
ProvideDebugInfo(AppInfo.isInDebugMode()) {
|
||||||
ProvideLanguageManager(languageManager) {
|
ProvideLanguageManager(languageManager) {
|
||||||
ProvideNotificationManager {
|
ProvideNotificationManager {
|
||||||
@ -64,39 +62,41 @@ object Ui : KoinComponent {
|
|||||||
uiScale = appComponent.uiScale.collectAsState().value
|
uiScale = appComponent.uiScale.collectAsState().value
|
||||||
) {
|
) {
|
||||||
ProvideGlobalExceptionHandler(globalAppExceptionHandler) {
|
ProvideGlobalExceptionHandler(globalAppExceptionHandler) {
|
||||||
val trayState = rememberTrayState()
|
ProvideSizeUnits(appComponent) {
|
||||||
HandleEffectsForApp(appComponent)
|
val trayState = rememberTrayState()
|
||||||
SystemTray(appComponent, trayState)
|
HandleEffectsForApp(appComponent)
|
||||||
val showHomeSlot = appComponent.showHomeSlot.collectAsState().value
|
SystemTray(appComponent, trayState)
|
||||||
showHomeSlot.child?.instance?.let {
|
val showHomeSlot = appComponent.showHomeSlot.collectAsState().value
|
||||||
HomeWindow(it, appComponent::closeHome)
|
showHomeSlot.child?.instance?.let {
|
||||||
|
HomeWindow(it, appComponent::closeHome)
|
||||||
|
}
|
||||||
|
val showSettingSlot = appComponent.showSettingSlot.collectAsState().value
|
||||||
|
showSettingSlot.child?.instance?.let {
|
||||||
|
SettingWindow(it, appComponent::closeSettings)
|
||||||
|
}
|
||||||
|
val showQueuesSlot = appComponent.showQueuesSlot.collectAsState().value
|
||||||
|
showQueuesSlot.child?.instance?.let {
|
||||||
|
QueuesWindow(it)
|
||||||
|
}
|
||||||
|
val batchDownloadSlot = appComponent.batchDownloadSlot.collectAsState().value
|
||||||
|
batchDownloadSlot.child?.instance?.let {
|
||||||
|
BatchDownloadWindow(it)
|
||||||
|
}
|
||||||
|
val editDownloadSlot = appComponent.editDownloadSlot.collectAsState().value
|
||||||
|
editDownloadSlot.child?.instance?.let {
|
||||||
|
EditDownloadWindow(it)
|
||||||
|
}
|
||||||
|
ShowAddDownloadDialogs(appComponent)
|
||||||
|
ShowDownloadDialogs(appComponent)
|
||||||
|
ShowCategoryDialogs(appComponent)
|
||||||
|
ShowUpdaterDialog(appComponent.updater)
|
||||||
|
ShowAboutDialog(appComponent)
|
||||||
|
NewQueueDialog(appComponent)
|
||||||
|
ShowMessageDialogs(appComponent)
|
||||||
|
ShowOpenSourceLibraries(appComponent)
|
||||||
|
ShowTranslators(appComponent)
|
||||||
|
ConfirmExit(appComponent)
|
||||||
}
|
}
|
||||||
val showSettingSlot = appComponent.showSettingSlot.collectAsState().value
|
|
||||||
showSettingSlot.child?.instance?.let {
|
|
||||||
SettingWindow(it, appComponent::closeSettings)
|
|
||||||
}
|
|
||||||
val showQueuesSlot = appComponent.showQueuesSlot.collectAsState().value
|
|
||||||
showQueuesSlot.child?.instance?.let {
|
|
||||||
QueuesWindow(it)
|
|
||||||
}
|
|
||||||
val batchDownloadSlot = appComponent.batchDownloadSlot.collectAsState().value
|
|
||||||
batchDownloadSlot.child?.instance?.let {
|
|
||||||
BatchDownloadWindow(it)
|
|
||||||
}
|
|
||||||
val editDownloadSlot = appComponent.editDownloadSlot.collectAsState().value
|
|
||||||
editDownloadSlot.child?.instance?.let {
|
|
||||||
EditDownloadWindow(it)
|
|
||||||
}
|
|
||||||
ShowAddDownloadDialogs(appComponent)
|
|
||||||
ShowDownloadDialogs(appComponent)
|
|
||||||
ShowCategoryDialogs(appComponent)
|
|
||||||
ShowUpdaterDialog(appComponent.updater)
|
|
||||||
ShowAboutDialog(appComponent)
|
|
||||||
NewQueueDialog(appComponent)
|
|
||||||
ShowMessageDialogs(appComponent)
|
|
||||||
ShowOpenSourceLibraries(appComponent)
|
|
||||||
ShowTranslators(appComponent)
|
|
||||||
ConfirmExit(appComponent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,6 +106,18 @@ object Ui : KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProvideSizeUnits(
|
||||||
|
component: AppComponent,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
ProvideSizeAndSpeedUnit(
|
||||||
|
sizeUnitConfig = component.appRepository.sizeUnit.collectAsState().value,
|
||||||
|
speedUnitConfig = component.appRepository.speedUnit.collectAsState().value,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HandleEffectsForApp(appComponent: AppComponent) {
|
private fun HandleEffectsForApp(appComponent: AppComponent) {
|
||||||
val notificationManager = useNotification()
|
val notificationManager = useNotification()
|
||||||
|
@ -1,70 +1,64 @@
|
|||||||
package com.abdownloadmanager.desktop.utils
|
package com.abdownloadmanager.desktop.utils
|
||||||
|
|
||||||
|
import ir.amirab.util.datasize.CommonSizeConvertConfigs
|
||||||
|
import ir.amirab.util.datasize.ConvertSizeConfig
|
||||||
|
import ir.amirab.util.datasize.SizeWithUnit
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import com.abdownloadmanager.resources.Res
|
import com.abdownloadmanager.resources.Res
|
||||||
import ir.amirab.downloader.utils.ByteConverter
|
|
||||||
import ir.amirab.util.compose.StringSource
|
import ir.amirab.util.compose.StringSource
|
||||||
import ir.amirab.util.compose.asStringSource
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
import ir.amirab.util.datasize.*
|
||||||
|
|
||||||
data class HumanReadableSize(
|
val LocalSpeedUnit = compositionLocalOf {
|
||||||
val value:Double,
|
CommonSizeConvertConfigs.BinaryBytes
|
||||||
val unit:Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun baseConvertBytesToHumanReadable(size: Long):HumanReadableSize?{
|
|
||||||
return ByteConverter.run {
|
|
||||||
when (size) {
|
|
||||||
in Long.MIN_VALUE until 0 -> null
|
|
||||||
|
|
||||||
in 0 until K_BYTES -> {
|
|
||||||
HumanReadableSize(
|
|
||||||
size.toDouble(),
|
|
||||||
BYTES,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
in K_BYTES until M_BYTES -> {
|
|
||||||
HumanReadableSize(
|
|
||||||
byteTo(size, K_BYTES),
|
|
||||||
K_BYTES,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
in M_BYTES until G_BYTES -> {
|
|
||||||
HumanReadableSize(
|
|
||||||
byteTo(size, M_BYTES),
|
|
||||||
M_BYTES
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
in G_BYTES..Long.MAX_VALUE -> {
|
|
||||||
HumanReadableSize(
|
|
||||||
byteTo(size, G_BYTES),
|
|
||||||
G_BYTES,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> error("should not happened! we covered all range but not this ? $size")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fun convertBytesToHumanReadable(size: Long): String? {
|
val LocalSizeUnit = compositionLocalOf {
|
||||||
ByteConverter.run {
|
CommonSizeConvertConfigs.BinaryBytes
|
||||||
return baseConvertBytesToHumanReadable(size)?.let {
|
|
||||||
"${prettify(it.value)} ${unitPrettify(it.unit)}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertSizeToHumanReadable(size: Long): StringSource {
|
@Composable
|
||||||
return convertBytesToHumanReadable(size)?.asStringSource()
|
fun ProvideSizeAndSpeedUnit(
|
||||||
|
sizeUnitConfig: ConvertSizeConfig,
|
||||||
|
speedUnitConfig: ConvertSizeConfig,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalSpeedUnit provides speedUnitConfig,
|
||||||
|
LocalSizeUnit provides sizeUnitConfig,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// they are used for ui
|
||||||
|
// size == -1 means that its unknown
|
||||||
|
|
||||||
|
fun convertPositiveBytesToSizeUnit(
|
||||||
|
size: Long,
|
||||||
|
target: ConvertSizeConfig,
|
||||||
|
): SizeWithUnit? {
|
||||||
|
if (size < 0) return null
|
||||||
|
return SizeConverter.bytesToSize(
|
||||||
|
bytes = size,
|
||||||
|
target = target,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertPositiveBytesToHumanReadable(size: Long, target: ConvertSizeConfig): String? {
|
||||||
|
return convertPositiveBytesToSizeUnit(size, target)
|
||||||
|
?.let { "${it.formatedValue()} ${it.unit}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertPositiveSizeToHumanReadable(size: Long, target: ConvertSizeConfig): StringSource {
|
||||||
|
return convertPositiveBytesToHumanReadable(size, target)
|
||||||
|
?.asStringSource()
|
||||||
?: Res.string.unknown.asStringSource()
|
?: Res.string.unknown.asStringSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertSpeedToHumanReadable(size: Long, perUnit: String="s"): String {
|
fun convertPositiveSpeedToHumanReadable(size: Long, target: ConvertSizeConfig, perUnit: String = "s"): String {
|
||||||
return convertBytesToHumanReadable(size)?.let {
|
return convertPositiveBytesToHumanReadable(size, target)
|
||||||
"$it/$perUnit"
|
?.let { "$it/$perUnit" }
|
||||||
} ?: "-"
|
?: "-"
|
||||||
}
|
}
|
||||||
//fun main() {
|
|
||||||
// println(convertBytesToHumanReadable(2048000))
|
|
||||||
//}
|
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
package ir.amirab.downloader.utils
|
|
||||||
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
import java.text.DecimalFormatSymbols
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
object ByteConverter {
|
|
||||||
const val BYTES = 1L
|
|
||||||
const val K_BYTES = BYTES*1024L
|
|
||||||
const val M_BYTES = K_BYTES * 1024L
|
|
||||||
const val G_BYTES = M_BYTES * 1024L
|
|
||||||
const val T_BYTES = G_BYTES * 1024L
|
|
||||||
private val format = DecimalFormat("#.##", DecimalFormatSymbols(Locale.US))
|
|
||||||
fun byteTo(value: Long, unit: Long): Double {
|
|
||||||
return (value / unit.toDouble())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unitToByte(value: Double, unit: Long): Long {
|
|
||||||
return (value * unit).toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun prettify(value: Number): String {
|
|
||||||
return format.format(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unitPrettify(unit:Long): String? {
|
|
||||||
return when(unit){
|
|
||||||
BYTES->"B"
|
|
||||||
K_BYTES->"KB"
|
|
||||||
M_BYTES->"MB"
|
|
||||||
G_BYTES->"GB"
|
|
||||||
T_BYTES->"TB"
|
|
||||||
else ->null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -197,6 +197,8 @@ settings_use_proxy_describe_system_proxy=System Proxy will be used
|
|||||||
settings_use_proxy_describe_manual_proxy="{{value}}" will be used
|
settings_use_proxy_describe_manual_proxy="{{value}}" will be used
|
||||||
settings_track_deleted_files_on_disk=Track Deleted Files On Disk
|
settings_track_deleted_files_on_disk=Track Deleted Files On Disk
|
||||||
settings_track_deleted_files_on_disk_description=Automatically remove files from the list when they are deleted or moved from the download directory.
|
settings_track_deleted_files_on_disk_description=Automatically remove files from the list when they are deleted or moved from the download directory.
|
||||||
|
settings_download_speed_unit=Download Speed Unit
|
||||||
|
settings_download_speed_unit_description=Unit used to display the download speed
|
||||||
settings_theme=Theme
|
settings_theme=Theme
|
||||||
settings_theme_description=Select a theme for the App
|
settings_theme_description=Select a theme for the App
|
||||||
settings_ui_scale=UI Scale
|
settings_ui_scale=UI Scale
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
package ir.amirab.util.datasize
|
||||||
|
|
||||||
|
sealed class BaseSize(
|
||||||
|
val size: Long,
|
||||||
|
) {
|
||||||
|
abstract fun longString(): String
|
||||||
|
fun scaleInto(baseSize: BaseSize): Double {
|
||||||
|
return when {
|
||||||
|
baseSize == this -> 1.0
|
||||||
|
else -> size / baseSize.size.toDouble()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Bits : BaseSize(1) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "b"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun longString(): String {
|
||||||
|
return "Bits"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Bytes : BaseSize(8) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "B"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun longString(): String {
|
||||||
|
return "Bytes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package ir.amirab.util.datasize
|
||||||
|
|
||||||
|
object CommonSizeConvertConfigs {
|
||||||
|
val BinaryBytes
|
||||||
|
get() = ConvertSizeConfig(
|
||||||
|
baseSize = BaseSize.Bytes,
|
||||||
|
factors = SizeFactors.BinarySizeFactors,
|
||||||
|
)
|
||||||
|
val BinaryBits
|
||||||
|
get() = ConvertSizeConfig(
|
||||||
|
baseSize = BaseSize.Bits,
|
||||||
|
factors = SizeFactors.BinarySizeFactors,
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package ir.amirab.util.datasize
|
||||||
|
|
||||||
|
object CommonSizeUnits {
|
||||||
|
val BinaryBytes = SizeUnit(
|
||||||
|
factorValue = SizeFactors.FactorValue.None,
|
||||||
|
baseSize = BaseSize.Bytes,
|
||||||
|
factors = SizeFactors.BinarySizeFactors,
|
||||||
|
)
|
||||||
|
val BinaryBits = SizeUnit(
|
||||||
|
factorValue = SizeFactors.FactorValue.None,
|
||||||
|
baseSize = BaseSize.Bits,
|
||||||
|
factors = SizeFactors.BinarySizeFactors,
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package ir.amirab.util.datasize
|
||||||
|
|
||||||
|
data class ConvertSizeConfig(
|
||||||
|
val baseSize: BaseSize,
|
||||||
|
val factors: SizeFactors,
|
||||||
|
// default to auto
|
||||||
|
val acceptedFactors: List<SizeFactors.FactorValue> = SizeFactors.FactorValue.entries,
|
||||||
|
)
|
@ -0,0 +1,50 @@
|
|||||||
|
package ir.amirab.util.datasize
|
||||||
|
|
||||||
|
object SizeConverter {
|
||||||
|
fun sizeToBytes(
|
||||||
|
sizeWithUnit: SizeWithUnit,
|
||||||
|
): Long {
|
||||||
|
return convert(
|
||||||
|
sizeWithUnit,
|
||||||
|
CommonSizeConvertConfigs
|
||||||
|
.BinaryBytes
|
||||||
|
.fixedFactor(SizeFactors.FactorValue.None)
|
||||||
|
).value.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bytesToSize(
|
||||||
|
bytes: Long,
|
||||||
|
target: ConvertSizeConfig,
|
||||||
|
): SizeWithUnit {
|
||||||
|
return convert(
|
||||||
|
SizeWithUnit(
|
||||||
|
bytes.toDouble(),
|
||||||
|
CommonSizeUnits.BinaryBytes,
|
||||||
|
),
|
||||||
|
target
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convert(
|
||||||
|
src: SizeWithUnit,
|
||||||
|
target: ConvertSizeConfig,
|
||||||
|
): SizeWithUnit {
|
||||||
|
val valueWithoutFactor = src.unit.factors.removeFactor(
|
||||||
|
src.value, src.unit.factorValue
|
||||||
|
)
|
||||||
|
val valueWithBaseSize = valueWithoutFactor * src.unit.baseSize.scaleInto(target.baseSize)
|
||||||
|
val factorValue = target.factors.bestFactor(
|
||||||
|
valueWithBaseSize.toLong(),
|
||||||
|
target.acceptedFactors,
|
||||||
|
)
|
||||||
|
val finalValue = target.factors.withFactor(valueWithBaseSize, factorValue)
|
||||||
|
return SizeWithUnit(
|
||||||
|
value = finalValue,
|
||||||
|
SizeUnit(
|
||||||
|
factorValue = factorValue,
|
||||||
|
factors = target.factors,
|
||||||
|
baseSize = target.baseSize,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
package ir.amirab.util.datasize
|
||||||
|
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
sealed class SizeFactors(
|
||||||
|
val baseValue: Long,
|
||||||
|
) {
|
||||||
|
enum class FactorValue {
|
||||||
|
None,
|
||||||
|
Kilo,
|
||||||
|
Mega,
|
||||||
|
Giga,
|
||||||
|
Tera,
|
||||||
|
// Peta,
|
||||||
|
// Exa,
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun get(factorValue: FactorValue): Long {
|
||||||
|
return getFactorSize(factorValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFactorSize(factorValue: FactorValue): Long {
|
||||||
|
return factors[factorValue.ordinal]
|
||||||
|
}
|
||||||
|
|
||||||
|
private val factors = FactorValue.entries.map {
|
||||||
|
baseValue.toDouble().pow(it.ordinal).toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bestFactor(
|
||||||
|
value: Long,
|
||||||
|
acceptedFactors: List<FactorValue> = FactorValue.entries,
|
||||||
|
): FactorValue {
|
||||||
|
require(acceptedFactors.isNotEmpty()) {
|
||||||
|
"acceptedFactors must not be empty"
|
||||||
|
}
|
||||||
|
// we need lowest
|
||||||
|
if (value == 0L) {
|
||||||
|
acceptedFactors.first()
|
||||||
|
}
|
||||||
|
// no other choice
|
||||||
|
if (acceptedFactors.size == 1) return acceptedFactors.first()
|
||||||
|
// find in range
|
||||||
|
val inRange = acceptedFactors.lastOrNull {
|
||||||
|
getFactorSize(it) <= value
|
||||||
|
}
|
||||||
|
if (inRange != null) {
|
||||||
|
return inRange
|
||||||
|
}
|
||||||
|
// find rearrest
|
||||||
|
return acceptedFactors.minBy {
|
||||||
|
(value - getFactorSize(it)).absoluteValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeFactor(value: Double, factorValue: FactorValue): Long {
|
||||||
|
return (value * getFactorSize(factorValue)).toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withFactor(value: Double, factorValue: FactorValue): Double {
|
||||||
|
if (factorValue == FactorValue.None) return value
|
||||||
|
return value / getFactorSize(factorValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun toString(factorValue: FactorValue): String
|
||||||
|
|
||||||
|
data object DecimalSizeFactors : SizeFactors(baseValue = 1000) {
|
||||||
|
override fun toString(factorValue: FactorValue): String {
|
||||||
|
return when (factorValue) {
|
||||||
|
FactorValue.None -> ""
|
||||||
|
FactorValue.Kilo -> "k"
|
||||||
|
FactorValue.Mega -> "m"
|
||||||
|
FactorValue.Giga -> "g"
|
||||||
|
FactorValue.Tera -> "t"
|
||||||
|
// FactorValue.Peta -> "p"
|
||||||
|
// FactorValue.Exa -> "e"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data object BinarySizeFactors : SizeFactors(baseValue = 1024) {
|
||||||
|
override fun toString(factorValue: FactorValue): String {
|
||||||
|
return when (factorValue) {
|
||||||
|
FactorValue.None -> ""
|
||||||
|
FactorValue.Kilo -> "K"
|
||||||
|
FactorValue.Mega -> "M"
|
||||||
|
FactorValue.Giga -> "G"
|
||||||
|
FactorValue.Tera -> "T"
|
||||||
|
// FactorValue.Peta -> "P"
|
||||||
|
// FactorValue.Exa -> "E"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package ir.amirab.util.datasize
|
||||||
|
|
||||||
|
data class SizeUnit(
|
||||||
|
val factorValue: SizeFactors.FactorValue = SizeFactors.FactorValue.None,
|
||||||
|
val baseSize: BaseSize,
|
||||||
|
val factors: SizeFactors,
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
val factor = factors.toString(factorValue)
|
||||||
|
return "$factor$baseSize"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package ir.amirab.util.datasize
|
||||||
|
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import java.text.DecimalFormatSymbols
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
data class SizeWithUnit(
|
||||||
|
val value: Double,
|
||||||
|
val unit: SizeUnit,
|
||||||
|
) {
|
||||||
|
fun toString(format: NumberFormat?): String {
|
||||||
|
val formattedValue = formatedValue(format)
|
||||||
|
return "$formattedValue $unit"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatedValue(format: NumberFormat? = DefaultFormat) = format
|
||||||
|
?.format(value)
|
||||||
|
?: value.toString()
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return toString(DefaultFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DefaultFormat = DecimalFormat(
|
||||||
|
"#.##", DecimalFormatSymbols(
|
||||||
|
Locale.US,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package ir.amirab.util.datasize
|
||||||
|
|
||||||
|
fun SizeUnit.asConverterConfig(
|
||||||
|
acceptedFactors: List<SizeFactors.FactorValue> = listOf(
|
||||||
|
factorValue
|
||||||
|
),
|
||||||
|
): ConvertSizeConfig {
|
||||||
|
return ConvertSizeConfig(
|
||||||
|
factors = factors,
|
||||||
|
baseSize = baseSize,
|
||||||
|
acceptedFactors = acceptedFactors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ConvertSizeConfig.bits() = copy(
|
||||||
|
baseSize = BaseSize.Bits
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ConvertSizeConfig.bytes() = copy(
|
||||||
|
baseSize = BaseSize.Bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ConvertSizeConfig.decimal() = copy(
|
||||||
|
factors = SizeFactors.DecimalSizeFactors
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ConvertSizeConfig.binary() = copy(
|
||||||
|
factors = SizeFactors.BinarySizeFactors
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ConvertSizeConfig.autoSelectFactors() = copy(
|
||||||
|
acceptedFactors = SizeFactors.FactorValue.entries
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ConvertSizeConfig.fixedFactor(factorValue: SizeFactors.FactorValue) = copy(
|
||||||
|
acceptedFactors = listOf(factorValue)
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user