add speed unit (#339)

This commit is contained in:
AmirHossein Abdolmotallebi 2025-01-01 08:44:37 +03:30 committed by GitHub
parent a2077c7e4b
commit f106c9b702
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 542 additions and 194 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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