From f106c9b7028b335425316854e5d937a3584daf75 Mon Sep 17 00:00:00 2001 From: AmirHossein Abdolmotallebi Date: Wed, 1 Jan 2025 08:44:37 +0330 Subject: [PATCH] add speed unit (#339) --- .../abdownloadmanager/desktop/AppComponent.kt | 2 +- .../addDownload/multiple/AddMultiItemTable.kt | 2 +- .../addDownload/single/AddDownloadPage.kt | 2 +- .../single/AddSingleDownloadComponent.kt | 4 +- .../pages/editdownload/EditDownload.kt | 2 +- .../editdownload/EditDownloadComponent.kt | 5 +- .../pages/editdownload/EditDownloadState.kt | 7 +- .../desktop/pages/home/HomePage.kt | 11 +- .../pages/home/sections/TableDownloadItem.kt | 11 +- .../pages/settings/SettingsComponent.kt | 27 ++++- .../settings/configurable/widgets/Speed.kt | 74 ++++++++----- .../CompletedDownloadPage.kt | 13 ++- .../singleDownloadPage/SingleDownloadPage.kt | 9 +- .../SingleDownloadPageComponent.kt | 10 +- .../desktop/repository/AppRepository.kt | 23 +++- .../desktop/storage/AppSettingsStorage.kt | 6 ++ .../com/abdownloadmanager/desktop/ui/Ui.kt | 84 ++++++++------- .../desktop/utils/SizeUtil.kt | 100 ++++++++---------- .../amirab/downloader/utils/ByteConverter.kt | 36 ------- .../resources/locales/en_US.properties | 2 + .../ir/amirab/util/datasize/BaseSize.kt | 35 ++++++ .../util/datasize/CommonSizeConvertConfigs.kt | 14 +++ .../amirab/util/datasize/CommonSizeUnits.kt | 14 +++ .../amirab/util/datasize/ConvertSizeConfig.kt | 8 ++ .../ir/amirab/util/datasize/SizeConverter.kt | 50 +++++++++ .../ir/amirab/util/datasize/SizeFactors.kt | 96 +++++++++++++++++ .../ir/amirab/util/datasize/SizeUnit.kt | 12 +++ .../ir/amirab/util/datasize/SizeWithUnit.kt | 32 ++++++ .../ir/amirab/util/datasize/extensions.kt | 37 +++++++ 29 files changed, 538 insertions(+), 190 deletions(-) delete mode 100644 downloader/core/src/main/kotlin/ir/amirab/downloader/utils/ByteConverter.kt create mode 100644 shared/utils/src/main/kotlin/ir/amirab/util/datasize/BaseSize.kt create mode 100644 shared/utils/src/main/kotlin/ir/amirab/util/datasize/CommonSizeConvertConfigs.kt create mode 100644 shared/utils/src/main/kotlin/ir/amirab/util/datasize/CommonSizeUnits.kt create mode 100644 shared/utils/src/main/kotlin/ir/amirab/util/datasize/ConvertSizeConfig.kt create mode 100644 shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeConverter.kt create mode 100644 shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeFactors.kt create mode 100644 shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeUnit.kt create mode 100644 shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeWithUnit.kt create mode 100644 shared/utils/src/main/kotlin/ir/amirab/util/datasize/extensions.kt diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/AppComponent.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/AppComponent.kt index 767a37fc..7ba82c95 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/AppComponent.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/AppComponent.kt @@ -81,7 +81,7 @@ class AppComponent( DownloadItemOpener, ContainsEffects by supportEffects(), KoinComponent { - private val appRepository: AppRepository by inject() + val appRepository: AppRepository by inject() private val appSettings: AppSettingsStorage by inject() private val integration: Integration by inject() diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/multiple/AddMultiItemTable.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/multiple/AddMultiItemTable.kt index 3873c305..67103005 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/multiple/AddMultiItemTable.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/multiple/AddMultiItemTable.kt @@ -296,7 +296,7 @@ private fun SizeCell( val length by downloadChecker.length.collectAsState() CellText( length?.let { - convertSizeToHumanReadable(it).rememberString() + convertPositiveSizeToHumanReadable(it, LocalSizeUnit.current).rememberString() } ?: "" ) } diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/AddDownloadPage.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/AddDownloadPage.kt index c0ce80d2..df585753 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/AddDownloadPage.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/AddDownloadPage.kt @@ -559,7 +559,7 @@ fun RenderFileTypeAndSize( iconModifier ) val size = fileInfo.totalLength?.let { - convertSizeToHumanReadable(it) + convertPositiveSizeToHumanReadable(it, LocalSizeUnit.current) }.takeIf { // this is a length of a html page (error) fileInfo.isSuccessFul diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/AddSingleDownloadComponent.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/AddSingleDownloadComponent.kt index edbb325a..69ca1708 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/AddSingleDownloadComponent.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/AddSingleDownloadComponent.kt @@ -257,7 +257,9 @@ class AddSingleDownloadComponent( backedBy = speedLimit, describe = { if (it == 0L) Res.string.unlimited.asStringSource() - else convertSpeedToHumanReadable(it).asStringSource() + else convertPositiveSpeedToHumanReadable( + it, appSettings.speedUnit.value + ).asStringSource() } ), IntConfigurable( diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/EditDownload.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/EditDownload.kt index bad65a6f..c41d654a 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/EditDownload.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/EditDownload.kt @@ -462,7 +462,7 @@ private fun RenderFileTypeAndSize( iconModifier ) val size = fileInfo.totalLength?.let { - convertSizeToHumanReadable(it) + convertPositiveSizeToHumanReadable(it, LocalSizeUnit.current) }.takeIf { // this is a length of a html page (error) fileInfo.isSuccessFul diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/EditDownloadComponent.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/EditDownloadComponent.kt index 82adbcc6..a5722e7b 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/EditDownloadComponent.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/EditDownloadComponent.kt @@ -1,5 +1,6 @@ package com.abdownloadmanager.desktop.pages.editdownload +import com.abdownloadmanager.desktop.repository.AppRepository import com.abdownloadmanager.desktop.utils.* import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects import com.abdownloadmanager.desktop.utils.mvi.supportEffects @@ -30,6 +31,7 @@ class EditDownloadComponent( private val downloaderClient: DownloaderClient by inject() val iconProvider: FileIconProvider by inject() val downloadSystem: DownloadSystem by inject() + private val appRepository: AppRepository by inject() val editDownloadUiChecker = MutableStateFlow(null as EditDownloadState?) init { @@ -73,7 +75,8 @@ class EditDownloadComponent( .contains(editedDownloadFile) } }, - scope, + scope = scope, + appRepository = appRepository, ) editDownloadUiChecker.value = editDownloadState pendingCredential?.let { credentials -> diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/EditDownloadState.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/EditDownloadState.kt index 9257ec82..8d69a60b 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/EditDownloadState.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/EditDownloadState.kt @@ -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.SpeedLimitConfigurable 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.LinkChecker -import com.abdownloadmanager.desktop.utils.convertSpeedToHumanReadable +import com.abdownloadmanager.desktop.utils.convertPositiveSpeedToHumanReadable import com.abdownloadmanager.resources.Res import com.abdownloadmanager.utils.isValidUrl 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.asStringSource import ir.amirab.util.compose.asStringSourceWithARgs -import ir.amirab.util.flow.createMutableStateFlowFromStateFlow import ir.amirab.util.flow.mapStateFlow import ir.amirab.util.flow.mapTwoWayStateFlow import ir.amirab.util.flow.onEachLatest @@ -126,6 +126,7 @@ class EditDownloadState( val currentDownloadItem: MutableStateFlow, val editedDownloadItem: MutableStateFlow, val downloaderClient: DownloaderClient, + val appRepository: AppRepository, conflictDetector: DownloadConflictDetector, scope: CoroutineScope, ) { @@ -165,7 +166,7 @@ class EditDownloadState( ), describe = { if (it == 0L) Res.string.unlimited.asStringSource() - else convertSpeedToHumanReadable(it).asStringSource() + else convertPositiveSpeedToHumanReadable(it, appRepository.speedUnit.value).asStringSource() } ), IntConfigurable( diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomePage.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomePage.kt index 475833c1..6ea2bd40 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomePage.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomePage.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import ir.amirab.downloader.utils.ByteConverter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import com.abdownloadmanager.desktop.ui.widget.ActionButton @@ -735,14 +734,10 @@ private fun Footer(component: HomeComponent) { val activeCount by component.activeDownloadCountFlow.collectAsState() FooterItem(MyIcons.activeCount, activeCount.toString(), "") val size by component.globalSpeedFlow.collectAsState(0) - val speed = baseConvertBytesToHumanReadable(size) + val speed = convertPositiveBytesToSizeUnit(size, LocalSpeedUnit.current) if (speed != null) { - val speedText = ByteConverter.prettify(speed.value) - val unitText = ByteConverter.unitPrettify(speed.unit) - ?.let { - "$it/s" - } - .orEmpty() + val speedText = speed.formatedValue() + val unitText = speed.unit.toString() + "/s" FooterItem(MyIcons.speed, speedText, unitText) } } diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/sections/TableDownloadItem.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/sections/TableDownloadItem.kt index 80d27826..77332107 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/sections/TableDownloadItem.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/sections/TableDownloadItem.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.abdownloadmanager.resources.Res -import com.abdownloadmanager.resources.* import com.abdownloadmanager.utils.FileIconProvider import com.abdownloadmanager.utils.category.Category import ir.amirab.util.compose.resources.myStringResource @@ -175,7 +174,10 @@ fun SpeedCell( (itemState as? ProcessingDownloadItemState)?.speed?.let { remaining -> if (itemState.status == DownloadJobStatus.Downloading) { Text( - text = convertSpeedToHumanReadable(remaining), + text = convertPositiveSpeedToHumanReadable( + remaining, + LocalSpeedUnit.current, + ), maxLines = 1, fontSize = myTextSizes.base, overflow = TextOverflow.Ellipsis, @@ -190,7 +192,10 @@ fun SizeCell( ) { item.contentLength.let { Text( - convertSizeToHumanReadable(it).rememberString(), + convertPositiveSizeToHumanReadable( + it, + LocalSizeUnit.current + ).rememberString(), maxLines = 1, fontSize = myTextSizes.base, overflow = TextOverflow.Ellipsis, diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/SettingsComponent.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/SettingsComponent.kt index 250353d9..40b48a3d 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/SettingsComponent.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/SettingsComponent.kt @@ -7,7 +7,7 @@ import com.abdownloadmanager.desktop.storage.AppSettingsStorage import ir.amirab.util.compose.IconSource import com.abdownloadmanager.desktop.ui.icon.MyIcons 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.supportEffects 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.localizationmanager.LanguageInfo 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.flow.createMutableStateFlowFromStateFlow import ir.amirab.util.flow.mapStateFlow @@ -121,6 +123,26 @@ fun trackDeletedFilesOnDisk(appRepository: AppRepository): BooleanConfigurable { ) } +fun speedUnit(appRepository: AppRepository, scope: CoroutineScope): EnumConfigurable { + 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 { return BooleanConfigurable( title = Res.string.settings_show_completion_dialog.asStringSource(), @@ -152,7 +174,7 @@ fun speedLimitConfig(appRepository: AppRepository): SpeedLimitConfigurable { if (it == 0L) { Res.string.unlimited.asStringSource() } else { - convertSpeedToHumanReadable(it).asStringSource() + convertPositiveSpeedToHumanReadable(it, appRepository.speedUnit.value).asStringSource() } } ) @@ -398,6 +420,7 @@ class SettingsComponent( uiScaleConfig(appSettings), autoStartConfig(appSettings), mergeTopBarWithTitleBarConfig(appSettings), + speedUnit(appRepository, scope), playSoundNotification(appSettings), ) diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/configurable/widgets/Speed.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/configurable/widgets/Speed.kt index 4f8b3012..c354f47e 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/configurable/widgets/Speed.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/configurable/widgets/Speed.kt @@ -3,7 +3,6 @@ package com.abdownloadmanager.desktop.pages.settings.configurable.widgets import com.abdownloadmanager.desktop.pages.settings.configurable.SpeedLimitConfigurable import com.abdownloadmanager.desktop.ui.widget.CheckBox import com.abdownloadmanager.desktop.ui.widget.DoubleTextField -import com.abdownloadmanager.desktop.utils.baseConvertBytesToHumanReadable import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.* import com.abdownloadmanager.desktop.ui.widget.Text @@ -11,35 +10,48 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import ir.amirab.downloader.utils.ByteConverter -import ir.amirab.downloader.utils.ByteConverter.BYTES -import ir.amirab.downloader.utils.ByteConverter.K_BYTES -import ir.amirab.downloader.utils.ByteConverter.M_BYTES +import com.abdownloadmanager.desktop.utils.LocalSpeedUnit +import ir.amirab.util.datasize.* @Composable fun RenderSpeedConfig(cfg: SpeedLimitConfigurable, modifier: Modifier) { val value by cfg.stateFlow.collectAsState() val setValue = cfg::set - val units = listOf( - BYTES, - K_BYTES, - M_BYTES, + + val speedUnit = LocalSpeedUnit.current + val allowedFactors = listOf( + SizeFactors.FactorValue.Kilo, + SizeFactors.FactorValue.Mega, ) - val enabled= isConfigEnabled() - val hasLimitSpeed = value != 0L - - var currentUnit by remember(hasLimitSpeed) { mutableStateOf(baseConvertBytesToHumanReadable(value)?.unit ?: BYTES) } + 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) { - val v = ByteConverter.run { - prettify( - byteTo(value, currentUnit) - ).toDouble() - } + val v = SizeConverter.bytesToSize( + value, currentUnit.asConverterConfig() + ).formatedValue().toDouble() mutableStateOf(v) } LaunchedEffect(currentValue, currentUnit) { setValue( - ByteConverter.unitToByte(currentValue, currentUnit) + SizeConverter.sizeToBytes( + SizeWithUnit(currentValue, currentUnit), + ) ) } ConfigTemplate( @@ -77,7 +89,7 @@ fun RenderSpeedConfig(cfg: SpeedLimitConfigurable, modifier: Modifier) { } ) { val prettified = remember(it) { - ByteConverter.unitPrettify(it) + "/s" + "$it/s" } Text(prettified) } @@ -90,12 +102,22 @@ fun RenderSpeedConfig(cfg: SpeedLimitConfigurable, modifier: Modifier) { value = hasLimitSpeed, enabled = enabled, onValueChange = { - if (it) { - setValue(ByteConverter.unitToByte(10.0, K_BYTES)) - } else { - setValue(0) - } - }) + if (it) { + setValue( + SizeConverter.sizeToBytes( + SizeWithUnit( + 256.0, SizeUnit( + SizeFactors.FactorValue.Kilo, + BaseSize.Bytes, + SizeFactors.BinarySizeFactors, + ) + ) + ) + ) + } else { + setValue(0) + } + }) } ) } \ No newline at end of file diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/CompletedDownloadPage.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/CompletedDownloadPage.kt index 95dab609..d4ead95b 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/CompletedDownloadPage.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/CompletedDownloadPage.kt @@ -2,14 +2,10 @@ package com.abdownloadmanager.desktop.pages.singleDownloadPage import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.border import androidx.compose.foundation.layout.* -import androidx.compose.foundation.onClick -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp 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.widget.ActionButton 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.resources.Res import com.abdownloadmanager.utils.compose.WithContentColor @@ -150,8 +147,10 @@ private fun RenderFileIconAndSize( ) Spacer(Modifier.height(4.dp)) Text( - text = convertSizeToHumanReadable(itemState.contentLength) - .rememberString(), + text = convertPositiveSizeToHumanReadable( + itemState.contentLength, + LocalSizeUnit.current, + ).rememberString(), ) } } diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/SingleDownloadPage.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/SingleDownloadPage.kt index d0453bfd..ab123589 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/SingleDownloadPage.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/SingleDownloadPage.kt @@ -342,13 +342,18 @@ fun ColumnScope.RenderPartInfo(itemState: ProcessingDownloadItemState) { } PartInfoCells.Downloaded -> { - SimpleCellText(convertSizeToHumanReadable(it.value.howMuchProceed).rememberString()) + SimpleCellText( + convertPositiveSizeToHumanReadable( + it.value.howMuchProceed, + LocalSizeUnit.current + ).rememberString() + ) } PartInfoCells.Total -> { SimpleCellText( it.value.length?.let { length -> - convertSizeToHumanReadable(length).rememberString() + convertPositiveSizeToHumanReadable(length, LocalSizeUnit.current).rememberString() } ?: myStringResource(Res.string.unknown), ) } diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/SingleDownloadPageComponent.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/SingleDownloadPageComponent.kt index 63d6ff32..abdb52dd 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/SingleDownloadPageComponent.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/SingleDownloadPageComponent.kt @@ -8,6 +8,7 @@ import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects import com.abdownloadmanager.desktop.utils.mvi.supportEffects import arrow.optics.copy 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.PageStatesStorage import com.abdownloadmanager.resources.Res @@ -55,6 +56,7 @@ class SingleDownloadComponent( KoinComponent { private val downloadSystem: DownloadSystem by inject() private val appSettings: AppSettingsStorage by inject() + private val appRepository: AppRepository by inject() val fileIconProvider: FileIconProvider by inject() private val singleDownloadPageStateToPersist by lazy { get().downloadPage @@ -122,19 +124,19 @@ class SingleDownloadComponent( add( SingleDownloadPagePropertyItem( Res.string.size.asStringSource(), - convertSizeToHumanReadable(it.contentLength) + convertPositiveSizeToHumanReadable(it.contentLength, appRepository.sizeUnit.value) ) ) add( SingleDownloadPagePropertyItem( Res.string.download_page_downloaded_size.asStringSource(), - convertBytesToHumanReadable(it.progress).orEmpty().asStringSource() + convertPositiveSizeToHumanReadable(it.progress, appRepository.sizeUnit.value) ) ) add( SingleDownloadPagePropertyItem( Res.string.speed.asStringSource(), - convertSpeedToHumanReadable(it.speed).asStringSource() + convertPositiveSpeedToHumanReadable(it.speed, appRepository.speedUnit.value).asStringSource() ) ) add( @@ -331,7 +333,7 @@ class SingleDownloadComponent( if (it == 0L) { Res.string.unlimited.asStringSource() } else { - convertSpeedToHumanReadable(it).asStringSource() + convertPositiveSpeedToHumanReadable(it, appRepository.speedUnit.value).asStringSource() } }, ), diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/repository/AppRepository.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/repository/AppRepository.kt index 75deaaa2..5a565ed9 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/repository/AppRepository.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/repository/AppRepository.kt @@ -1,5 +1,6 @@ package com.abdownloadmanager.desktop.repository +import ir.amirab.util.datasize.CommonSizeConvertConfigs import com.abdownloadmanager.desktop.storage.AppSettingsStorage import com.abdownloadmanager.desktop.utils.AutoStartManager import com.abdownloadmanager.utils.DownloadSystem @@ -11,12 +12,12 @@ import com.abdownloadmanager.utils.category.CategoryManager import com.abdownloadmanager.utils.proxy.ProxyManager import ir.amirab.downloader.DownloadManager 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 kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -45,6 +46,20 @@ class AppRepository : KoinComponent { val integrationEnabled = appSettings.browserIntegrationEnabled val integrationPort = appSettings.browserIntegrationPort 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 { saveLocation diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/storage/AppSettingsStorage.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/storage/AppSettingsStorage.kt index dd749652..3f55a371 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/storage/AppSettingsStorage.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/storage/AppSettingsStorage.kt @@ -7,6 +7,7 @@ import arrow.optics.optics import com.abdownloadmanager.desktop.App import ir.amirab.util.compose.localizationmanager.LanguageStorage import ir.amirab.util.config.* +import ir.amirab.util.datasize.BaseSize import kotlinx.serialization.Serializable import org.koin.core.component.KoinComponent import java.io.File @@ -34,6 +35,7 @@ data class AppSettingsModel( val browserIntegrationEnabled: Boolean = true, val browserIntegrationPort: Int = 15151, val trackDeletedFilesOnDisk: Boolean = false, + val useBitsForSpeed: Boolean = false, ) { companion object { val default: AppSettingsModel get() = AppSettingsModel() @@ -59,6 +61,7 @@ data class AppSettingsModel( val browserIntegrationEnabled = booleanKeyOf("browserIntegrationEnabled") val browserIntegrationPort = intKeyOf("browserIntegrationPort") val trackDeletedFilesOnDisk = booleanKeyOf("trackDeletedFilesOnDisk") + val useBitsForSpeed = booleanKeyOf("useBitsForSpeed") } @@ -87,6 +90,7 @@ data class AppSettingsModel( ?: default.browserIntegrationEnabled, browserIntegrationPort = source.get(Keys.browserIntegrationPort) ?: default.browserIntegrationPort, 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.browserIntegrationPort, focus.browserIntegrationPort) put(Keys.trackDeletedFilesOnDisk, focus.trackDeletedFilesOnDisk) + put(Keys.useBitsForSpeed, focus.useBitsForSpeed) } } } @@ -148,4 +153,5 @@ class AppSettingsStorage( val browserIntegrationEnabled = from(AppSettingsModel.browserIntegrationEnabled) val browserIntegrationPort = from(AppSettingsModel.browserIntegrationPort) val trackDeletedFilesOnDisk = from(AppSettingsModel.trackDeletedFilesOnDisk) + val useBitsForSpeed = from(AppSettingsModel.useBitsForSpeed) } \ No newline at end of file diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/Ui.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/Ui.kt index 28ba4a1c..79595f25 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/Ui.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/Ui.kt @@ -14,11 +14,7 @@ import com.abdownloadmanager.desktop.pages.singleDownloadPage.ShowDownloadDialog import com.abdownloadmanager.desktop.ui.icon.MyIcons import com.abdownloadmanager.desktop.ui.theme.ABDownloaderTheme 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 com.abdownloadmanager.desktop.utils.isInDebugMode import com.abdownloadmanager.desktop.utils.mvi.HandleEffects import androidx.compose.runtime.* 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.updater.ShowUpdaterDialog import com.abdownloadmanager.desktop.ui.widget.* +import com.abdownloadmanager.desktop.utils.* import com.abdownloadmanager.utils.compose.ProvideDebugInfo import ir.amirab.util.compose.localizationmanager.LanguageManager import kotlinx.coroutines.CoroutineScope @@ -56,6 +53,7 @@ object Ui : KoinComponent { } application { val theme by themeManager.currentThemeColor.collectAsState() + ProvideDebugInfo(AppInfo.isInDebugMode()) { ProvideLanguageManager(languageManager) { ProvideNotificationManager { @@ -64,39 +62,41 @@ object Ui : KoinComponent { uiScale = appComponent.uiScale.collectAsState().value ) { ProvideGlobalExceptionHandler(globalAppExceptionHandler) { - val trayState = rememberTrayState() - HandleEffectsForApp(appComponent) - SystemTray(appComponent, trayState) - val showHomeSlot = appComponent.showHomeSlot.collectAsState().value - 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) + ProvideSizeUnits(appComponent) { + val trayState = rememberTrayState() + HandleEffectsForApp(appComponent) + SystemTray(appComponent, trayState) + val showHomeSlot = appComponent.showHomeSlot.collectAsState().value + 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 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 private fun HandleEffectsForApp(appComponent: AppComponent) { val notificationManager = useNotification() diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/SizeUtil.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/SizeUtil.kt index 483f979e..fb903962 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/SizeUtil.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/SizeUtil.kt @@ -1,70 +1,64 @@ 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 ir.amirab.downloader.utils.ByteConverter import ir.amirab.util.compose.StringSource import ir.amirab.util.compose.asStringSource +import ir.amirab.util.datasize.* -data class HumanReadableSize( - val value:Double, - 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, - ) - } +val LocalSpeedUnit = compositionLocalOf { + CommonSizeConvertConfigs.BinaryBytes +} +val LocalSizeUnit = compositionLocalOf { + CommonSizeConvertConfigs.BinaryBytes +} - in K_BYTES until M_BYTES -> { - HumanReadableSize( - byteTo(size, K_BYTES), - K_BYTES, - ) - } +@Composable +fun ProvideSizeAndSpeedUnit( + sizeUnitConfig: ConvertSizeConfig, + speedUnitConfig: ConvertSizeConfig, + content: @Composable () -> Unit, +) { + CompositionLocalProvider( + LocalSpeedUnit provides speedUnitConfig, + LocalSizeUnit provides sizeUnitConfig, + content = content + ) +} - 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, - ) - } +// they are used for ui +// size == -1 means that its unknown - else -> error("should not happened! we covered all range but not this ? $size") - } - } +fun convertPositiveBytesToSizeUnit( + size: Long, + target: ConvertSizeConfig, +): SizeWithUnit? { + if (size < 0) return null + return SizeConverter.bytesToSize( + bytes = size, + target = target, + ) } -fun convertBytesToHumanReadable(size: Long): String? { - ByteConverter.run { - return baseConvertBytesToHumanReadable(size)?.let { - "${prettify(it.value)} ${unitPrettify(it.unit)}" - } - } + +fun convertPositiveBytesToHumanReadable(size: Long, target: ConvertSizeConfig): String? { + return convertPositiveBytesToSizeUnit(size, target) + ?.let { "${it.formatedValue()} ${it.unit}" } } -fun convertSizeToHumanReadable(size: Long): StringSource { - return convertBytesToHumanReadable(size)?.asStringSource() +fun convertPositiveSizeToHumanReadable(size: Long, target: ConvertSizeConfig): StringSource { + return convertPositiveBytesToHumanReadable(size, target) + ?.asStringSource() ?: Res.string.unknown.asStringSource() } -fun convertSpeedToHumanReadable(size: Long, perUnit: String="s"): String { - return convertBytesToHumanReadable(size)?.let { - "$it/$perUnit" - } ?: "-" +fun convertPositiveSpeedToHumanReadable(size: Long, target: ConvertSizeConfig, perUnit: String = "s"): String { + return convertPositiveBytesToHumanReadable(size, target) + ?.let { "$it/$perUnit" } + ?: "-" } -//fun main() { -// println(convertBytesToHumanReadable(2048000)) -//} diff --git a/downloader/core/src/main/kotlin/ir/amirab/downloader/utils/ByteConverter.kt b/downloader/core/src/main/kotlin/ir/amirab/downloader/utils/ByteConverter.kt deleted file mode 100644 index ccb3cdd0..00000000 --- a/downloader/core/src/main/kotlin/ir/amirab/downloader/utils/ByteConverter.kt +++ /dev/null @@ -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 - } - } -} \ No newline at end of file diff --git a/shared/resources/src/main/resources/com/abdownloadmanager/resources/locales/en_US.properties b/shared/resources/src/main/resources/com/abdownloadmanager/resources/locales/en_US.properties index 258b295e..aabc9447 100644 --- a/shared/resources/src/main/resources/com/abdownloadmanager/resources/locales/en_US.properties +++ b/shared/resources/src/main/resources/com/abdownloadmanager/resources/locales/en_US.properties @@ -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_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_download_speed_unit=Download Speed Unit +settings_download_speed_unit_description=Unit used to display the download speed settings_theme=Theme settings_theme_description=Select a theme for the App settings_ui_scale=UI Scale diff --git a/shared/utils/src/main/kotlin/ir/amirab/util/datasize/BaseSize.kt b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/BaseSize.kt new file mode 100644 index 00000000..fb8b8da6 --- /dev/null +++ b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/BaseSize.kt @@ -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" + } + } + + +} \ No newline at end of file diff --git a/shared/utils/src/main/kotlin/ir/amirab/util/datasize/CommonSizeConvertConfigs.kt b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/CommonSizeConvertConfigs.kt new file mode 100644 index 00000000..30aa4ee9 --- /dev/null +++ b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/CommonSizeConvertConfigs.kt @@ -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, + ) +} \ No newline at end of file diff --git a/shared/utils/src/main/kotlin/ir/amirab/util/datasize/CommonSizeUnits.kt b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/CommonSizeUnits.kt new file mode 100644 index 00000000..e5909419 --- /dev/null +++ b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/CommonSizeUnits.kt @@ -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, + ) +} \ No newline at end of file diff --git a/shared/utils/src/main/kotlin/ir/amirab/util/datasize/ConvertSizeConfig.kt b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/ConvertSizeConfig.kt new file mode 100644 index 00000000..a92e98a7 --- /dev/null +++ b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/ConvertSizeConfig.kt @@ -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.entries, +) \ No newline at end of file diff --git a/shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeConverter.kt b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeConverter.kt new file mode 100644 index 00000000..6ef58e64 --- /dev/null +++ b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeConverter.kt @@ -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, + ) + ) + } +} \ No newline at end of file diff --git a/shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeFactors.kt b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeFactors.kt new file mode 100644 index 00000000..e93ad41b --- /dev/null +++ b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeFactors.kt @@ -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.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" + } + } + } +} \ No newline at end of file diff --git a/shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeUnit.kt b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeUnit.kt new file mode 100644 index 00000000..8fe85afd --- /dev/null +++ b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeUnit.kt @@ -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" + } +} \ No newline at end of file diff --git a/shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeWithUnit.kt b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeWithUnit.kt new file mode 100644 index 00000000..a901c057 --- /dev/null +++ b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/SizeWithUnit.kt @@ -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, + ) + ) + } +} \ No newline at end of file diff --git a/shared/utils/src/main/kotlin/ir/amirab/util/datasize/extensions.kt b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/extensions.kt new file mode 100644 index 00000000..09bd4839 --- /dev/null +++ b/shared/utils/src/main/kotlin/ir/amirab/util/datasize/extensions.kt @@ -0,0 +1,37 @@ +package ir.amirab.util.datasize + +fun SizeUnit.asConverterConfig( + acceptedFactors: List = 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) +) \ No newline at end of file