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,
ContainsEffects<AppEffects> 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()

View File

@ -296,7 +296,7 @@ private fun SizeCell(
val length by downloadChecker.length.collectAsState()
CellText(
length?.let {
convertSizeToHumanReadable(it).rememberString()
convertPositiveSizeToHumanReadable(it, LocalSizeUnit.current).rememberString()
} ?: ""
)
}

View File

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

View File

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

View File

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

View File

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

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.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<DownloadItem>,
val editedDownloadItem: MutableStateFlow<DownloadItem>,
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(

View File

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

View File

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

View File

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

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.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 enabled= isConfigEnabled()
val hasLimitSpeed = value != 0L
var currentUnit by remember(hasLimitSpeed) { mutableStateOf(baseConvertBytesToHumanReadable(value)?.unit ?: BYTES) }
var currentValue by remember(value) {
val v = ByteConverter.run {
prettify(
byteTo(value, currentUnit)
).toDouble()
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) {
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)
}
@ -91,7 +103,17 @@ fun RenderSpeedConfig(cfg: SpeedLimitConfigurable, modifier: Modifier) {
enabled = enabled,
onValueChange = {
if (it) {
setValue(ByteConverter.unitToByte(10.0, K_BYTES))
setValue(
SizeConverter.sizeToBytes(
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.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(),
)
}
}

View File

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

View File

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

View File

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

View File

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

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.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,6 +62,7 @@ object Ui : KoinComponent {
uiScale = appComponent.uiScale.collectAsState().value
) {
ProvideGlobalExceptionHandler(globalAppExceptionHandler) {
ProvideSizeUnits(appComponent) {
val trayState = rememberTrayState()
HandleEffectsForApp(appComponent)
SystemTray(appComponent, trayState)
@ -105,6 +104,19 @@ 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) {

View File

@ -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,
)
val LocalSpeedUnit = compositionLocalOf {
CommonSizeConvertConfigs.BinaryBytes
}
val LocalSizeUnit = compositionLocalOf {
CommonSizeConvertConfigs.BinaryBytes
}
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,
@Composable
fun ProvideSizeAndSpeedUnit(
sizeUnitConfig: ConvertSizeConfig,
speedUnitConfig: ConvertSizeConfig,
content: @Composable () -> Unit,
) {
CompositionLocalProvider(
LocalSpeedUnit provides speedUnitConfig,
LocalSizeUnit provides sizeUnitConfig,
content = content
)
}
in K_BYTES until M_BYTES -> {
HumanReadableSize(
byteTo(size, K_BYTES),
K_BYTES,
// 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,
)
}
in M_BYTES until G_BYTES -> {
HumanReadableSize(
byteTo(size, M_BYTES),
M_BYTES
)
fun convertPositiveBytesToHumanReadable(size: Long, target: ConvertSizeConfig): String? {
return convertPositiveBytesToSizeUnit(size, target)
?.let { "${it.formatedValue()} ${it.unit}" }
}
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? {
ByteConverter.run {
return baseConvertBytesToHumanReadable(size)?.let {
"${prettify(it.value)} ${unitPrettify(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))
//}

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

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