mirror of
https://github.com/amir1376/ab-download-manager.git
synced 2025-02-20 11:43:24 +08:00
commit
22b51fc2c0
@ -24,7 +24,7 @@ import ir.amirab.downloader.utils.IDiskStat
|
||||
import ir.amirab.util.startup.Startup
|
||||
import com.abdownloadmanager.integration.Integration
|
||||
import ir.amirab.downloader.DownloadManager
|
||||
import ir.amirab.util.config.datastore.createMyConfigPreferences
|
||||
import ir.amirab.util.config.datastore.createMapConfigDatastore
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Dispatcher
|
||||
@ -39,8 +39,13 @@ import com.abdownloadmanager.utils.FileIconProvider
|
||||
import com.abdownloadmanager.utils.FileIconProviderUsingCategoryIcons
|
||||
import com.abdownloadmanager.utils.category.*
|
||||
import com.abdownloadmanager.utils.compose.IMyIcons
|
||||
import com.abdownloadmanager.utils.proxy.IProxyStorage
|
||||
import com.abdownloadmanager.utils.proxy.ProxyData
|
||||
import com.abdownloadmanager.utils.proxy.ProxyManager
|
||||
import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider
|
||||
import ir.amirab.downloader.monitor.IDownloadMonitor
|
||||
import ir.amirab.downloader.utils.EmptyFileCreator
|
||||
import ir.amirab.util.config.datastore.kotlinxSerializationDataStore
|
||||
|
||||
val downloaderModule = module {
|
||||
single<IDownloadQueueDatabase> {
|
||||
@ -84,6 +89,11 @@ val downloaderModule = module {
|
||||
8,
|
||||
)
|
||||
}
|
||||
single {
|
||||
ProxyManager(
|
||||
get()
|
||||
)
|
||||
}.bind<ProxyStrategyProvider>()
|
||||
single<DownloaderClient> {
|
||||
OkHttpDownloaderClient(
|
||||
OkHttpClient
|
||||
@ -92,9 +102,9 @@ val downloaderModule = module {
|
||||
//bypass limit on concurrent connections!
|
||||
maxRequests = Int.MAX_VALUE
|
||||
maxRequestsPerHost = Int.MAX_VALUE
|
||||
}).build()
|
||||
}).build(),
|
||||
get()
|
||||
)
|
||||
|
||||
}
|
||||
single {
|
||||
val downloadSettings: DownloadSettings = get()
|
||||
@ -215,9 +225,18 @@ val appModule = module {
|
||||
single {
|
||||
MyIcons
|
||||
}.bind<IMyIcons>()
|
||||
single {
|
||||
ProxyDatastoreStorage(
|
||||
kotlinxSerializationDataStore(
|
||||
AppInfo.optionsDir.resolve("proxySettings.json"),
|
||||
get(),
|
||||
ProxyData::default,
|
||||
)
|
||||
)
|
||||
}.bind<IProxyStorage>()
|
||||
single {
|
||||
AppSettingsStorage(
|
||||
createMyConfigPreferences(
|
||||
createMapConfigDatastore(
|
||||
AppInfo.configDir.resolve("appSettings.json"),
|
||||
get(),
|
||||
)
|
||||
@ -225,7 +244,7 @@ val appModule = module {
|
||||
}
|
||||
single {
|
||||
PageStatesStorage(
|
||||
createMyConfigPreferences(
|
||||
createMapConfigDatastore(
|
||||
AppInfo.configDir.resolve("pageStatesStorage.json"),
|
||||
get(),
|
||||
)
|
||||
|
@ -223,7 +223,7 @@ private fun WildcardLengthUi(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Multiselect(
|
||||
selections = WildcardSelect.entries,
|
||||
selections = entries,
|
||||
selectedItem = WildcardSelect.fromWildcardLength(wildcardLength),
|
||||
onSelectionChange = {
|
||||
onChangeWildcardLength(
|
||||
@ -258,48 +258,6 @@ private fun WildcardLengthUi(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T> Multiselect(
|
||||
selections: List<T>,
|
||||
selectedItem: T,
|
||||
onSelectionChange: (T) -> Unit,
|
||||
render: @Composable (T) -> Unit,
|
||||
) {
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clip(shape)
|
||||
.background(myColors.surface)
|
||||
) {
|
||||
for (item in selections) {
|
||||
val isSelected = item == selectedItem
|
||||
Box(
|
||||
Modifier
|
||||
.padding(vertical = 4.dp, horizontal = 4.dp)
|
||||
.clip(shape)
|
||||
.ifThen(isSelected) {
|
||||
background(LocalContentColor.current / 10)
|
||||
}
|
||||
.clickable {
|
||||
onSelectionChange(item)
|
||||
}
|
||||
.padding(vertical = 2.dp, horizontal = 4.dp)
|
||||
) {
|
||||
WithContentAlpha(
|
||||
if (isSelected) {
|
||||
1f
|
||||
} else {
|
||||
0.5f
|
||||
}
|
||||
) {
|
||||
render(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LabeledContent(
|
||||
label: @Composable () -> Unit,
|
||||
|
@ -11,6 +11,8 @@ import com.abdownloadmanager.desktop.utils.convertSpeedToHumanReadable
|
||||
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
||||
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
||||
import androidx.compose.runtime.*
|
||||
import com.abdownloadmanager.utils.proxy.ProxyManager
|
||||
import com.abdownloadmanager.utils.proxy.ProxyMode
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import ir.amirab.util.osfileutil.FileUtils
|
||||
import ir.amirab.util.flow.createMutableStateFlowFromStateFlow
|
||||
@ -133,6 +135,28 @@ fun defaultDownloadFolderConfig(appSettings: AppSettingsStorage): FolderConfigur
|
||||
)
|
||||
}
|
||||
|
||||
fun proxyConfig(proxyManager: ProxyManager, scope: CoroutineScope): ProxyConfigurable {
|
||||
return ProxyConfigurable(
|
||||
title = "Use Proxy",
|
||||
description = "Use proxy for downloading files",
|
||||
backedBy = proxyManager.proxyData,
|
||||
|
||||
validate = {
|
||||
true
|
||||
},
|
||||
describe = {
|
||||
val str = when (it.proxyMode) {
|
||||
ProxyMode.Direct -> "No proxy"
|
||||
ProxyMode.UseSystem -> "System proxy"
|
||||
ProxyMode.Manual -> it.proxyWithRules.proxy.run {
|
||||
"$type $host:$port"
|
||||
}
|
||||
}
|
||||
"$str will be used"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
fun uiScaleConfig(appSettings: AppSettings): EnumConfigurable<Float?> {
|
||||
return EnumConfigurable(
|
||||
@ -270,6 +294,7 @@ class SettingsComponent(
|
||||
ContainsEffects<SettingPageEffects> by supportEffects() {
|
||||
val appSettings by inject<AppSettingsStorage>()
|
||||
val appRepository by inject<AppRepository>()
|
||||
val proxyManager by inject<ProxyManager>()
|
||||
val themeManager by inject<ThemeManager>()
|
||||
val allConfigs = object : SettingSectionGetter {
|
||||
override operator fun get(key: SettingSections): List<Configurable<*>> {
|
||||
@ -290,6 +315,7 @@ class SettingsComponent(
|
||||
|
||||
DownloadEngine -> listOf(
|
||||
defaultDownloadFolderConfig(appSettings),
|
||||
proxyConfig(proxyManager, scope),
|
||||
useAverageSpeedConfig(appRepository),
|
||||
speedLimitConfig(appRepository),
|
||||
threadCountConfig(appRepository),
|
||||
|
@ -5,6 +5,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import com.abdownloadmanager.desktop.pages.settings.ThemeInfo
|
||||
import com.abdownloadmanager.desktop.pages.settings.configurable.BooleanConfigurable.RenderMode
|
||||
import com.abdownloadmanager.desktop.ui.theme.MyColors
|
||||
import com.abdownloadmanager.utils.proxy.ProxyData
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@ -298,3 +299,21 @@ class DayOfWeekConfigurable(
|
||||
enabled = enabled,
|
||||
visible = visible,
|
||||
)
|
||||
|
||||
class ProxyConfigurable(
|
||||
title: String,
|
||||
description: String,
|
||||
backedBy: MutableStateFlow<ProxyData>,
|
||||
describe: (ProxyData) -> String,
|
||||
validate: (ProxyData) -> Boolean,
|
||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||
) : Configurable<ProxyData>(
|
||||
title = title,
|
||||
description = description,
|
||||
backedBy = backedBy,
|
||||
describe = describe,
|
||||
validate = validate,
|
||||
enabled = enabled,
|
||||
visible = visible,
|
||||
)
|
||||
|
@ -0,0 +1,427 @@
|
||||
package com.abdownloadmanager.desktop.pages.settings.configurable.widgets
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.abdownloadmanager.desktop.pages.settings.configurable.ProxyConfigurable
|
||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||
import com.abdownloadmanager.desktop.ui.theme.myColors
|
||||
import com.abdownloadmanager.desktop.ui.theme.myTextSizes
|
||||
import com.abdownloadmanager.desktop.ui.widget.*
|
||||
import com.abdownloadmanager.desktop.utils.div
|
||||
import com.abdownloadmanager.utils.compose.LocalContentColor
|
||||
import com.abdownloadmanager.utils.compose.widget.Icon
|
||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||
import com.abdownloadmanager.utils.proxy.ProxyMode
|
||||
import com.abdownloadmanager.utils.proxy.ProxyRules
|
||||
import com.abdownloadmanager.utils.proxy.ProxyWithRules
|
||||
import ir.amirab.downloader.connection.proxy.Proxy
|
||||
import ir.amirab.downloader.connection.proxy.ProxyType
|
||||
import ir.amirab.util.desktop.DesktopUtils
|
||||
|
||||
|
||||
@Composable
|
||||
fun RenderProxyConfig(cfg: ProxyConfigurable, modifier: Modifier) {
|
||||
val value by cfg.stateFlow.collectAsState()
|
||||
val setValue = cfg::set
|
||||
val enabled = isConfigEnabled()
|
||||
ConfigTemplate(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
TitleAndDescription(cfg, true)
|
||||
},
|
||||
value = {
|
||||
RenderSpinner(
|
||||
enabled = enabled,
|
||||
possibleValues = ProxyMode.usableValues(),
|
||||
value = value.proxyMode,
|
||||
onSelect = {
|
||||
setValue(
|
||||
value.copy(
|
||||
proxyMode = it
|
||||
)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.widthIn(min = 120.dp),
|
||||
render = {
|
||||
val text = when (it) {
|
||||
ProxyMode.Direct -> "No Proxy"
|
||||
ProxyMode.UseSystem -> "Use System Proxy"
|
||||
ProxyMode.Manual -> "Manual Proxy"
|
||||
}
|
||||
Text(text)
|
||||
},
|
||||
)
|
||||
},
|
||||
nestedContent = {
|
||||
AnimatedContent(value.proxyMode.takeIf { enabled }) {
|
||||
when (it) {
|
||||
ProxyMode.Direct -> {}
|
||||
ProxyMode.UseSystem -> {
|
||||
ActionButton(
|
||||
"Open System Proxy Settings",
|
||||
onClick = {
|
||||
DesktopUtils.openSystemProxySettings()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
ProxyMode.Manual -> {
|
||||
RenderManualProxyConfig(
|
||||
proxyWithRules = value.proxyWithRules,
|
||||
setProxyWithRules = {
|
||||
setValue(
|
||||
value.copy(
|
||||
proxyWithRules = it
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
private class ProxyEditState(
|
||||
private val proxyWithRules: ProxyWithRules,
|
||||
private val setProxyWithRules: (ProxyWithRules) -> Unit,
|
||||
) {
|
||||
var proxyType = mutableStateOf(proxyWithRules.proxy.type)
|
||||
|
||||
var proxyHost = mutableStateOf(proxyWithRules.proxy.host)
|
||||
var proxyPort = mutableStateOf(proxyWithRules.proxy.port)
|
||||
|
||||
var useAuth = mutableStateOf(proxyWithRules.proxy.username != null)
|
||||
var proxyUsername = mutableStateOf(proxyWithRules.proxy.username.orEmpty())
|
||||
var proxyPassword = mutableStateOf(proxyWithRules.proxy.password.orEmpty())
|
||||
|
||||
var excludeURLPatterns = mutableStateOf(proxyWithRules.rules.excludeURLPatterns.joinToString(" "))
|
||||
|
||||
val canSave: Boolean by derivedStateOf {
|
||||
val hostValid = proxyHost.value.isNotBlank()
|
||||
hostValid
|
||||
}
|
||||
|
||||
fun save() {
|
||||
val useAuth = useAuth.value
|
||||
if (!canSave) {
|
||||
return
|
||||
}
|
||||
setProxyWithRules(
|
||||
proxyWithRules.copy(
|
||||
proxy = Proxy(
|
||||
type = proxyType.value,
|
||||
host = proxyHost.value.trim(),
|
||||
port = proxyPort.value,
|
||||
username = proxyUsername.value.takeIf { it.isNotEmpty() && useAuth },
|
||||
password = proxyPassword.value.takeIf { it.isNotEmpty() && useAuth },
|
||||
),
|
||||
rules = ProxyRules(
|
||||
excludeURLPatterns = excludeURLPatterns.value
|
||||
.split(" ")
|
||||
.map { it.trim() }
|
||||
.filterNot { it.isEmpty() },
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderManualProxyConfig(
|
||||
proxyWithRules: ProxyWithRules,
|
||||
setProxyWithRules: (ProxyWithRules) -> Unit,
|
||||
) {
|
||||
var showManualProxyConfig by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
ActionButton(
|
||||
"Change proxy",
|
||||
onClick = {
|
||||
showManualProxyConfig = true
|
||||
},
|
||||
)
|
||||
if (showManualProxyConfig) {
|
||||
val dismiss = {
|
||||
showManualProxyConfig = false
|
||||
}
|
||||
val state = remember(setProxyWithRules) {
|
||||
ProxyEditState(
|
||||
proxyWithRules = proxyWithRules,
|
||||
setProxyWithRules = {
|
||||
setProxyWithRules(it)
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
ProxyEditDialog(state, onDismiss = dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProxyEditDialog(
|
||||
state: ProxyEditState,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = (onDismiss),
|
||||
content = {
|
||||
val (type, setType) = state.proxyType
|
||||
val (host, setHost) = state.proxyHost
|
||||
val (port, setPort) = state.proxyPort
|
||||
val (useAuth, setUseAuth) = state.useAuth
|
||||
val (username, setUsername) = state.proxyUsername
|
||||
val (password, setPassword) = state.proxyPassword
|
||||
val (excludeURLPatterns, setExcludeURLPatterns) = state.excludeURLPatterns
|
||||
|
||||
SettingsDialog(
|
||||
headerTitle = "Edit Proxy",
|
||||
onDismiss = onDismiss,
|
||||
content = {
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
val spacer = @Composable {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
DialogConfigItem(
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
Text("Type")
|
||||
},
|
||||
value = {
|
||||
Multiselect(
|
||||
selections = ProxyType.entries.toList(),
|
||||
selectedItem = type,
|
||||
onSelectionChange = setType,
|
||||
modifier = Modifier,
|
||||
render = {
|
||||
Text(
|
||||
it.name,
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
|
||||
)
|
||||
},
|
||||
selectedColor = LocalContentColor.current / 15,
|
||||
unselectedAlpha = 0.8f,
|
||||
)
|
||||
}
|
||||
)
|
||||
spacer()
|
||||
DialogConfigItem(
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
Text("Address & Port")
|
||||
},
|
||||
value = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MyTextField(
|
||||
text = host,
|
||||
onTextChange = setHost,
|
||||
placeholder = "127.0.0.1",
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Text(":", Modifier.padding(horizontal = 8.dp))
|
||||
IntTextField(
|
||||
value = port,
|
||||
onValueChange = setPort,
|
||||
placeholder = "Port",
|
||||
range = 1..65535,
|
||||
modifier = Modifier.width(96.dp),
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
textPadding = PaddingValues(8.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
spacer()
|
||||
DialogConfigItem(
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier.onClick {
|
||||
setUseAuth(!useAuth)
|
||||
}
|
||||
) {
|
||||
CheckBox(
|
||||
value = useAuth,
|
||||
onValueChange = setUseAuth,
|
||||
size = 16.dp
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Use Authentication")
|
||||
}
|
||||
},
|
||||
value = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MyTextField(
|
||||
text = username,
|
||||
onTextChange = setUsername,
|
||||
placeholder = "Username",
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = useAuth,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
MyTextField(
|
||||
text = password,
|
||||
onTextChange = setPassword,
|
||||
placeholder = "Password",
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = useAuth,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
spacer()
|
||||
DialogConfigItem(
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
Row {
|
||||
Text("Don't Use proxy for")
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Help(
|
||||
"A list of urls that may not be proxied\nYou can use wildcard with *\nfor example 192.168.1.* example.com (space separated)"
|
||||
)
|
||||
}
|
||||
},
|
||||
value = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MyTextField(
|
||||
text = excludeURLPatterns,
|
||||
onTextChange = setExcludeURLPatterns,
|
||||
placeholder = "example.com 192.168.1.*",
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
ActionButton(
|
||||
"Save",
|
||||
enabled = state.canSave,
|
||||
onClick = {
|
||||
state.save()
|
||||
})
|
||||
Spacer(Modifier.width(8.dp))
|
||||
ActionButton("Cancel", onClick = {
|
||||
onDismiss()
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsDialog(
|
||||
headerTitle: String,
|
||||
onDismiss: () -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
actions: (@Composable RowScope.() -> Unit)? = null,
|
||||
) {
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(shape)
|
||||
.border(2.dp, myColors.onBackground / 10, shape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
myColors.surface,
|
||||
myColors.background,
|
||||
)
|
||||
)
|
||||
)
|
||||
.padding(16.dp)
|
||||
.width(450.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
headerTitle,
|
||||
fontSize = myTextSizes.lg,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
MyIcon(
|
||||
MyIcons.windowClose,
|
||||
"Close",
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.clickable { onDismiss() }
|
||||
.padding(12.dp)
|
||||
.size(12.dp),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
content()
|
||||
actions?.let {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(
|
||||
Modifier.align(Alignment.End),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
actions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DialogConfigItem(
|
||||
modifier: Modifier,
|
||||
title: @Composable ColumnScope.() -> Unit,
|
||||
value: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.height(IntrinsicSize.Max),
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
title()
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
value()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -48,6 +48,7 @@ fun RenderConfigurable(
|
||||
}
|
||||
|
||||
is DayOfWeekConfigurable -> RenderDayOfWeekConfigurable(cfg,modifier)
|
||||
is ProxyConfigurable -> RenderProxyConfig(cfg, modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.abdownloadmanager.desktop.utils.DownloadSystem
|
||||
import ir.amirab.downloader.DownloadSettings
|
||||
import com.abdownloadmanager.integration.Integration
|
||||
import com.abdownloadmanager.integration.IntegrationResult
|
||||
import com.abdownloadmanager.utils.proxy.ProxyManager
|
||||
import ir.amirab.downloader.DownloadManager
|
||||
import ir.amirab.downloader.monitor.IDownloadMonitor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -19,6 +20,7 @@ import org.koin.core.component.inject
|
||||
class AppRepository : KoinComponent {
|
||||
private val scope: CoroutineScope by inject()
|
||||
private val appSettings: AppSettingsStorage by inject()
|
||||
private val proxyManager: ProxyManager by inject()
|
||||
val theme = appSettings.theme
|
||||
|
||||
// val uiScale = appSettings.uiScale
|
||||
@ -38,6 +40,7 @@ class AppRepository : KoinComponent {
|
||||
val integrationEnabled = appSettings.browserIntegrationEnabled
|
||||
val integrationPort = appSettings.browserIntegrationPort
|
||||
|
||||
|
||||
init {
|
||||
//maybe its better to move this to another place
|
||||
appSettings.autoStartOnBoot
|
||||
|
@ -96,7 +96,7 @@ data class AppSettingsModel(
|
||||
|
||||
class AppSettingsStorage(
|
||||
settings: DataStore<MapConfig>,
|
||||
) : ConfigBaseSettings<AppSettingsModel>(settings, AppSettingsModel.ConfigLens) {
|
||||
) : ConfigBaseSettingsByMapConfig<AppSettingsModel>(settings, AppSettingsModel.ConfigLens) {
|
||||
var theme = from(AppSettingsModel.theme)
|
||||
var mergeTopBarWithTitleBar = from(AppSettingsModel.mergeTopBarWithTitleBar)
|
||||
val threadCount = from(AppSettingsModel.threadCount)
|
||||
|
@ -84,7 +84,7 @@ data class PageStatesModel(
|
||||
|
||||
class PageStatesStorage(
|
||||
settings: DataStore<MapConfig>,
|
||||
) : ConfigBaseSettings<PageStatesModel>(settings, PageStatesModel.ConfigLens) {
|
||||
) : ConfigBaseSettingsByMapConfig<PageStatesModel>(settings, PageStatesModel.ConfigLens) {
|
||||
val lastUsedSaveLocations = from(PageStatesModel.global.lastSavedLocations)
|
||||
val downloadPage = from(PageStatesModel.downloadPage)
|
||||
val homePageStorage = from(PageStatesModel.home)
|
||||
|
@ -0,0 +1,14 @@
|
||||
package com.abdownloadmanager.desktop.storage
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.abdownloadmanager.desktop.utils.ConfigBaseSettingsByJson
|
||||
import com.abdownloadmanager.utils.proxy.IProxyStorage
|
||||
import com.abdownloadmanager.utils.proxy.ProxyData
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class ProxyDatastoreStorage(
|
||||
dataStore: DataStore<ProxyData>,
|
||||
) : IProxyStorage, ConfigBaseSettingsByJson<ProxyData>(dataStore) {
|
||||
override val proxyDataFlow = data
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package com.abdownloadmanager.desktop.ui.widget
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.abdownloadmanager.desktop.ui.theme.myColors
|
||||
import com.abdownloadmanager.desktop.ui.util.ifThen
|
||||
import com.abdownloadmanager.desktop.utils.div
|
||||
import com.abdownloadmanager.utils.compose.LocalContentColor
|
||||
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
||||
|
||||
@Composable
|
||||
fun <T> Multiselect(
|
||||
selections: List<T>,
|
||||
selectedItem: T,
|
||||
onSelectionChange: (T) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
shape: Shape = RoundedCornerShape(6.dp),
|
||||
backgroundColour: Color = myColors.surface,
|
||||
selectedColor: Color = LocalContentColor.current / 10,
|
||||
unselectedAlpha: Float = 0.5f,
|
||||
render: @Composable (T) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.background(backgroundColour)
|
||||
) {
|
||||
for (item in selections) {
|
||||
val isSelected = item == selectedItem
|
||||
Box(
|
||||
Modifier
|
||||
.padding(vertical = 4.dp, horizontal = 4.dp)
|
||||
.clip(shape)
|
||||
.ifThen(isSelected) {
|
||||
background(selectedColor)
|
||||
}
|
||||
.clickable {
|
||||
onSelectionChange(item)
|
||||
}
|
||||
.padding(vertical = 2.dp, horizontal = 4.dp)
|
||||
) {
|
||||
WithContentAlpha(
|
||||
if (isSelected) {
|
||||
1f
|
||||
} else {
|
||||
unselectedAlpha
|
||||
}
|
||||
) {
|
||||
render(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
@ -33,6 +34,8 @@ fun IntTextField(
|
||||
keyboardOptions: KeyboardOptions,
|
||||
prettify: (Int) -> String = { it.toString() },
|
||||
placeholder: String = "",
|
||||
textPadding: PaddingValues = PaddingValues(4.dp),
|
||||
shape: Shape = RectangleShape,
|
||||
) {
|
||||
NumberTextField(
|
||||
value = value,
|
||||
@ -53,6 +56,8 @@ fun IntTextField(
|
||||
keyboardOptions = keyboardOptions,
|
||||
interactionSource = interactionSource,
|
||||
placeholder = placeholder,
|
||||
textPadding = textPadding,
|
||||
shape = shape,
|
||||
)
|
||||
}
|
||||
|
||||
@ -103,7 +108,7 @@ fun DoubleTextField(
|
||||
},
|
||||
enabled: Boolean = true,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
placeholder: String = ""
|
||||
placeholder: String = "",
|
||||
) {
|
||||
NumberTextField(
|
||||
value = value,
|
||||
@ -177,10 +182,11 @@ fun <T : Comparable<T>> NumberTextField(
|
||||
keyboardOptions: KeyboardOptions,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
placeholder: String = "",
|
||||
textPadding: PaddingValues = PaddingValues(4.dp),
|
||||
shape: Shape = RectangleShape,
|
||||
) {
|
||||
|
||||
val value by rememberUpdatedState(value)
|
||||
val shape = RectangleShape
|
||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||
var haveWrongValue by remember(value) {
|
||||
mutableStateOf(false)
|
||||
@ -203,7 +209,7 @@ fun <T : Comparable<T>> NumberTextField(
|
||||
myText = prettify(value)
|
||||
}
|
||||
}
|
||||
fun set(v:T,prettify: Boolean):Boolean{
|
||||
fun set(v: T, prettify: Boolean): Boolean {
|
||||
val isInRange = v in range
|
||||
val valueInRange = if (isInRange) v else v.coerceIn(range)
|
||||
lastEmittedValueByMe = if (prettify || !isInRange) {
|
||||
@ -224,7 +230,7 @@ fun <T : Comparable<T>> NumberTextField(
|
||||
}
|
||||
}
|
||||
MyTextField(
|
||||
textPadding = PaddingValues(4.dp),
|
||||
textPadding = textPadding,
|
||||
shape = shape,
|
||||
modifier = modifier.onKeyEvent {
|
||||
when (it.key) {
|
||||
@ -258,7 +264,7 @@ fun <T : Comparable<T>> NumberTextField(
|
||||
myText = it
|
||||
} else {
|
||||
val wasInRange = set(v, false)
|
||||
if (wasInRange){
|
||||
if (wasInRange) {
|
||||
myText = it
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ fun AppInfo.isInDebugMode(): Boolean {
|
||||
}
|
||||
|
||||
val AppInfo.configDir: File get() = File(AppProperties.getConfigDirectory())
|
||||
val AppInfo.optionsDir: File get() = AppInfo.configDir.resolve("options")
|
||||
val AppInfo.downloadDbDir:File get() = AppInfo.configDir.resolve("download_db")
|
||||
fun AppInfo.extensions(){
|
||||
|
||||
|
@ -10,34 +10,69 @@ import kotlinx.coroutines.runBlocking
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
abstract class ConfigBaseSettings<T>(
|
||||
dataStore: DataStore<MapConfig>,
|
||||
lens: Lens<MapConfig, T>
|
||||
) : KoinComponent {
|
||||
private val scope: CoroutineScope by inject()
|
||||
private val lastFileState = dataStore.data.let {
|
||||
runBlocking { it.stateIn(scope) }
|
||||
abstract class BaseStorage<T> : KoinComponent {
|
||||
val scope: CoroutineScope by inject()
|
||||
|
||||
protected abstract val inMemoryState: MutableStateFlow<T>
|
||||
protected abstract suspend fun saveData(data: T)
|
||||
|
||||
val data get() = inMemoryState
|
||||
|
||||
fun <K> from(lens: Lens<T, K>): MutableStateFlow<K> {
|
||||
return inMemoryState.mapTwoWayStateFlow(lens)
|
||||
}
|
||||
|
||||
private val inMemoryState = MutableStateFlow(
|
||||
lens.get(lastFileState.value)
|
||||
)
|
||||
|
||||
init {
|
||||
/**
|
||||
* call this on upper implementations where [inMemoryState] and [saveData] are implemented
|
||||
*/
|
||||
protected fun startPersistData() {
|
||||
inMemoryState
|
||||
//first
|
||||
.drop(1)
|
||||
.debounce(500)
|
||||
.onEach { s ->
|
||||
dataStore.updateData {
|
||||
val newData = lens.set(MapConfig(), s)
|
||||
newData
|
||||
}
|
||||
saveData(s)
|
||||
}.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
fun <K> from(lens: Lens<T, K>): MutableStateFlow<K> {
|
||||
return inMemoryState
|
||||
.mapTwoWayStateFlow(lens)
|
||||
abstract class ConfigBaseSettingsByMapConfig<T>(
|
||||
private val dataStore: DataStore<MapConfig>,
|
||||
private val lens: Lens<MapConfig, T>,
|
||||
) : BaseStorage<T>(), KoinComponent {
|
||||
private val lastFileState = dataStore.data.let {
|
||||
runBlocking { it.stateIn(scope) }
|
||||
}
|
||||
|
||||
override val inMemoryState = MutableStateFlow(
|
||||
lens.get(lastFileState.value)
|
||||
)
|
||||
|
||||
override suspend fun saveData(data: T) {
|
||||
dataStore.updateData {
|
||||
val newData = lens.set(MapConfig(), data)
|
||||
newData
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
startPersistData()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ConfigBaseSettingsByJson<T>(
|
||||
private val dataStore: DataStore<T>,
|
||||
) : BaseStorage<T>(), KoinComponent {
|
||||
private val lastFileState = dataStore.data.let {
|
||||
runBlocking { it.stateIn(scope) }
|
||||
}
|
||||
|
||||
override val inMemoryState = MutableStateFlow(lastFileState.value)
|
||||
override suspend fun saveData(data: T) {
|
||||
dataStore.updateData { data }
|
||||
}
|
||||
|
||||
init {
|
||||
startPersistData()
|
||||
}
|
||||
}
|
@ -2,3 +2,8 @@ plugins{
|
||||
id(MyPlugins.kotlin)
|
||||
id(MyPlugins.composeDesktop)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":shared:app-utils"))
|
||||
implementation(project(":shared:utils"))
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package ir.amirab.util.desktop
|
||||
|
||||
import ir.amirab.util.desktop.linux.LinuxUtils
|
||||
import ir.amirab.util.desktop.mac.MacOSUtils
|
||||
import ir.amirab.util.desktop.windows.WindowsUtils
|
||||
import ir.amirab.util.platform.Platform
|
||||
|
||||
|
||||
interface DesktopUtils {
|
||||
fun openSystemProxySettings()
|
||||
|
||||
companion object : DesktopUtils by getDesktopUtilOfCurrentOS()
|
||||
}
|
||||
|
||||
private fun getDesktopUtilOfCurrentOS(): DesktopUtils {
|
||||
val platform = Platform.getCurrentPlatform() as Platform.Desktop
|
||||
return when (platform) {
|
||||
Platform.Desktop.Windows -> WindowsUtils()
|
||||
Platform.Desktop.MacOS -> MacOSUtils()
|
||||
Platform.Desktop.Linux -> LinuxUtils()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
package ir.amirab.util.desktop.linux
|
||||
|
||||
import ir.amirab.util.desktop.DesktopUtils
|
||||
import ir.amirab.util.execAndWait
|
||||
|
||||
class LinuxUtils : DesktopUtils {
|
||||
override fun openSystemProxySettings() {
|
||||
val desktopEnv = System.getenv("XDG_CURRENT_DESKTOP")
|
||||
when {
|
||||
desktopEnv?.contains("GNOME") ?: false -> {
|
||||
execAndWait(
|
||||
arrayOf(
|
||||
"gnome-control-center network"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
desktopEnv?.contains("KDE") ?: false -> {
|
||||
execAndWait(
|
||||
arrayOf(
|
||||
"systemsettings5 proxy"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
println("Can't open System Proxy Settings: Unsupported desktop environment: $desktopEnv")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package ir.amirab.util.desktop.mac
|
||||
|
||||
import ir.amirab.util.desktop.DesktopUtils
|
||||
import ir.amirab.util.execAndWait
|
||||
|
||||
class MacOSUtils : DesktopUtils {
|
||||
override fun openSystemProxySettings() {
|
||||
execAndWait(
|
||||
arrayOf("open /System/Library/PreferencePanes/Network.prefPane")
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package ir.amirab.util.desktop.windows
|
||||
|
||||
import ir.amirab.util.desktop.DesktopUtils
|
||||
import ir.amirab.util.execAndWait
|
||||
|
||||
class WindowsUtils : DesktopUtils {
|
||||
override fun openSystemProxySettings() {
|
||||
val result = execAndWait(
|
||||
arrayOf(
|
||||
"cmd", "/c", "start",
|
||||
"ms-settings:network-proxy",
|
||||
)
|
||||
)
|
||||
if (!result) {
|
||||
arrayOf(
|
||||
"rundll32.exe shell32.dll,Control_RunDLL inetcpl.cpl,,4"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,19 @@
|
||||
package ir.amirab.downloader.connection
|
||||
|
||||
import ir.amirab.downloader.connection.proxy.ProxyStrategy
|
||||
import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider
|
||||
import ir.amirab.downloader.connection.proxy.ProxyType
|
||||
import ir.amirab.downloader.connection.response.ResponseInfo
|
||||
import ir.amirab.downloader.downloaditem.IDownloadCredentials
|
||||
import ir.amirab.downloader.utils.await
|
||||
import okhttp3.*
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.ProxySelector
|
||||
|
||||
class OkHttpDownloaderClient(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val proxyStrategyProvider: ProxyStrategyProvider,
|
||||
) : DownloaderClient() {
|
||||
private fun newCall(
|
||||
downloadCredentials: IDownloadCredentials,
|
||||
@ -15,38 +22,84 @@ class OkHttpDownloaderClient(
|
||||
extraBuilder: Request.Builder.() -> Unit,
|
||||
): Call {
|
||||
val rangeHeader = createRangeHeader(start, end)
|
||||
return okHttpClient.newCall(
|
||||
Request.Builder()
|
||||
.url(downloadCredentials.link)
|
||||
.apply {
|
||||
defaultHeadersInFirst().forEach { (k, v) ->
|
||||
header(k, v)
|
||||
}
|
||||
downloadCredentials.headers
|
||||
?.filter {
|
||||
//OkHttp handles this header and if we override it,
|
||||
//makes redirected links to have this "Host" instead of their own!, and cause error
|
||||
!it.key.equals("Host", true)
|
||||
}
|
||||
?.forEach { (k, v) ->
|
||||
return okHttpClient
|
||||
.applyProxy(downloadCredentials)
|
||||
.newCall(
|
||||
Request.Builder()
|
||||
.url(downloadCredentials.link)
|
||||
.apply {
|
||||
defaultHeadersInFirst().forEach { (k, v) ->
|
||||
header(k, v)
|
||||
}
|
||||
defaultHeadersInLast().forEach { (k, v) ->
|
||||
header(k, v)
|
||||
downloadCredentials.headers
|
||||
?.filter {
|
||||
//OkHttp handles this header and if we override it,
|
||||
//makes redirected links to have this "Host" instead of their own!, and cause error
|
||||
!it.key.equals("Host", true)
|
||||
}
|
||||
?.forEach { (k, v) ->
|
||||
header(k, v)
|
||||
}
|
||||
defaultHeadersInLast().forEach { (k, v) ->
|
||||
header(k, v)
|
||||
}
|
||||
val username = downloadCredentials.username
|
||||
val password = downloadCredentials.password
|
||||
if (username?.isNotBlank() == true && password?.isNotBlank() == true) {
|
||||
header("Authorization", Credentials.basic(username, password))
|
||||
}
|
||||
downloadCredentials.userAgent?.let { userAgent ->
|
||||
header("User-Agent", userAgent)
|
||||
}
|
||||
}
|
||||
val username = downloadCredentials.username
|
||||
val password = downloadCredentials.password
|
||||
if (username?.isNotBlank() == true && password?.isNotBlank() == true) {
|
||||
header("Authorization", Credentials.basic(username, password))
|
||||
}
|
||||
downloadCredentials.userAgent?.let { userAgent ->
|
||||
header("User-Agent", userAgent)
|
||||
}
|
||||
}
|
||||
.apply(extraBuilder)
|
||||
.header(rangeHeader.first, rangeHeader.second)
|
||||
.build()
|
||||
)
|
||||
.apply(extraBuilder)
|
||||
.header(rangeHeader.first, rangeHeader.second)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun OkHttpClient.applyProxy(
|
||||
downloadCredentials: IDownloadCredentials,
|
||||
): OkHttpClient {
|
||||
return when (
|
||||
val strategy = proxyStrategyProvider.getProxyStrategyFor(downloadCredentials.link)
|
||||
) {
|
||||
ProxyStrategy.Direct -> return this
|
||||
ProxyStrategy.UseSystem -> {
|
||||
newBuilder()
|
||||
.proxySelector(ProxySelector.getDefault())
|
||||
.build()
|
||||
}
|
||||
|
||||
is ProxyStrategy.ManualProxy -> {
|
||||
val proxy = strategy.proxy
|
||||
return newBuilder()
|
||||
.proxy(
|
||||
Proxy(
|
||||
when (proxy.type) {
|
||||
ProxyType.HTTP -> Proxy.Type.HTTP
|
||||
ProxyType.SOCKS -> Proxy.Type.SOCKS
|
||||
},
|
||||
InetSocketAddress(proxy.host, proxy.port)
|
||||
)
|
||||
).let {
|
||||
if (proxy.username != null && proxy.type == ProxyType.HTTP) {
|
||||
it.proxyAuthenticator { _, r ->
|
||||
val credentials = Credentials.basic(
|
||||
proxy.username,
|
||||
proxy.password.orEmpty()
|
||||
)
|
||||
r.request
|
||||
.newBuilder()
|
||||
.header("Proxy-Authorization", credentials)
|
||||
.build()
|
||||
}
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -0,0 +1,22 @@
|
||||
package ir.amirab.downloader.connection.proxy
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Proxy(
|
||||
val type: ProxyType,
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val username: String?,
|
||||
val password: String?,
|
||||
) {
|
||||
companion object {
|
||||
fun default() = Proxy(
|
||||
type = ProxyType.HTTP,
|
||||
host = "127.0.0.1",
|
||||
port = 2080,
|
||||
username = null,
|
||||
password = null,
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package ir.amirab.downloader.connection.proxy
|
||||
|
||||
sealed interface ProxyStrategy {
|
||||
data object Direct : ProxyStrategy
|
||||
data object UseSystem : ProxyStrategy
|
||||
data class ManualProxy(val proxy: Proxy) : ProxyStrategy
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package ir.amirab.downloader.connection.proxy
|
||||
|
||||
interface ProxyStrategyProvider {
|
||||
fun getProxyStrategyFor(url: String): ProxyStrategy
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package ir.amirab.downloader.connection.proxy
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
|
||||
enum class ProxyType {
|
||||
@SerialName("http")
|
||||
HTTP,
|
||||
|
||||
@SerialName("socks")
|
||||
SOCKS;
|
||||
}
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.remember
|
||||
import ir.amirab.util.compose.IconSource
|
||||
import ir.amirab.util.compose.fromUri
|
||||
import ir.amirab.util.wildcardMatch
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
@ -47,25 +48,14 @@ data class Category(
|
||||
return true
|
||||
}
|
||||
return acceptedUrlPatterns.any {
|
||||
test(
|
||||
patten = it,
|
||||
wildcardMatch(
|
||||
pattern = it,
|
||||
input = url
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun test(
|
||||
patten: String,
|
||||
input: String,
|
||||
): Boolean {
|
||||
return patten
|
||||
.split("*")
|
||||
.joinToString(".*") { Regex.escape(it) }
|
||||
.toRegex()
|
||||
.containsMatchIn(input)
|
||||
}
|
||||
|
||||
fun Category.iconSource(): IconSource? {
|
||||
return IconSource.fromUri(icon)
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package com.abdownloadmanager.utils.proxy
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
interface IProxyStorage {
|
||||
val proxyDataFlow: MutableStateFlow<ProxyData>
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.abdownloadmanager.utils.proxy
|
||||
|
||||
import ir.amirab.downloader.connection.proxy.Proxy
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ProxyRules(
|
||||
val excludeURLPatterns: List<String>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ProxyWithRules(
|
||||
val proxy: Proxy,
|
||||
val rules: ProxyRules,
|
||||
)
|
||||
|
||||
enum class ProxyMode {
|
||||
@SerialName("direct")
|
||||
Direct,
|
||||
|
||||
@SerialName("system")
|
||||
UseSystem,
|
||||
|
||||
@SerialName("manual")
|
||||
Manual;
|
||||
|
||||
companion object {
|
||||
fun usableValues(): List<ProxyMode> {
|
||||
// UseSystem not works as expected
|
||||
// so we filter it for now.
|
||||
return listOf(
|
||||
Direct,
|
||||
Manual,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for persisting in storage
|
||||
@Serializable
|
||||
data class ProxyData(
|
||||
val proxyMode: ProxyMode,
|
||||
val proxyWithRules: ProxyWithRules,
|
||||
) {
|
||||
companion object {
|
||||
fun default() = ProxyData(
|
||||
proxyMode = ProxyMode.Direct,
|
||||
proxyWithRules = ProxyWithRules(
|
||||
proxy = Proxy.default(),
|
||||
rules = ProxyRules(emptyList())
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package com.abdownloadmanager.utils.proxy
|
||||
|
||||
import ir.amirab.downloader.connection.proxy.Proxy
|
||||
import ir.amirab.downloader.connection.proxy.ProxyStrategy
|
||||
import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider
|
||||
import ir.amirab.downloader.connection.proxy.ProxyType
|
||||
import ir.amirab.util.wildcardMatch
|
||||
import java.net.Authenticator
|
||||
import java.net.PasswordAuthentication
|
||||
|
||||
class ProxyManager(
|
||||
val storage: IProxyStorage,
|
||||
) : ProxyStrategyProvider {
|
||||
val proxyData = storage.proxyDataFlow
|
||||
|
||||
init {
|
||||
val mySocksProxyAuthenticator = MySocksProxyAuthenticator { proxyData.value.proxyWithRules.proxy }
|
||||
Authenticator.setDefault(mySocksProxyAuthenticator)
|
||||
}
|
||||
|
||||
private fun getProxyModeForThisURL(url: String): ProxyStrategy {
|
||||
val usingProxy = proxyData.value
|
||||
return when (usingProxy.proxyMode) {
|
||||
ProxyMode.Direct -> ProxyStrategy.Direct
|
||||
ProxyMode.UseSystem -> ProxyStrategy.UseSystem
|
||||
ProxyMode.Manual -> {
|
||||
val proxyWithRules = usingProxy.proxyWithRules
|
||||
if (shouldUseProxyFor(url, proxyWithRules.rules)) {
|
||||
ProxyStrategy.ManualProxy(proxyWithRules.proxy)
|
||||
} else {
|
||||
ProxyStrategy.Direct
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldUseProxyFor(
|
||||
url: String,
|
||||
rules: ProxyRules,
|
||||
): Boolean {
|
||||
val isInExcludeList = rules.excludeURLPatterns.any {
|
||||
wildcardMatch(it, url)
|
||||
}
|
||||
return !isInExcludeList
|
||||
}
|
||||
|
||||
override fun getProxyStrategyFor(url: String): ProxyStrategy {
|
||||
return getProxyModeForThisURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* this is used for socks proxy authentication
|
||||
*/
|
||||
private class MySocksProxyAuthenticator(
|
||||
val currentProxy: () -> Proxy,
|
||||
) : Authenticator() {
|
||||
override fun getPasswordAuthentication(): PasswordAuthentication? {
|
||||
val proxy = currentProxy()
|
||||
if (proxy.type == ProxyType.SOCKS && requestingPrompt == "SOCKS authentication") {
|
||||
if (proxy.host == requestingHost && proxy.port == requestingPort) {
|
||||
if (proxy.username != null) {
|
||||
return PasswordAuthentication(
|
||||
proxy.username,
|
||||
proxy.password.orEmpty().toCharArray(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.abdownloadmanager.desktop.storage.base
|
||||
package ir.amirab.util.config.datastore
|
||||
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.DataStore
|
@ -46,7 +46,7 @@ class MyConfigSerializer(
|
||||
}
|
||||
}
|
||||
|
||||
fun createMyConfigPreferences(
|
||||
fun createMapConfigDatastore(
|
||||
file: File,
|
||||
json: Json,
|
||||
): DataStore<MapConfig> {
|
@ -1,4 +1,4 @@
|
||||
package ir.amirab.util.osfileutil
|
||||
package ir.amirab.util
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@ -8,7 +8,7 @@ import java.util.concurrent.TimeUnit
|
||||
* @param waitFor maximum time allowed process finish ( in milliseconds )
|
||||
* @return `true` when process exits with `0` exit code, `false` if the process fails with non-zero exit code or execution time exceeds the [waitFor]
|
||||
*/
|
||||
internal fun execAndWait(
|
||||
fun execAndWait(
|
||||
command: Array<String>,
|
||||
waitFor: Long = 2_000,
|
||||
): Boolean {
|
12
shared/utils/src/main/kotlin/ir/amirab/util/StringUtil.kt
Normal file
12
shared/utils/src/main/kotlin/ir/amirab/util/StringUtil.kt
Normal file
@ -0,0 +1,12 @@
|
||||
package ir.amirab.util
|
||||
|
||||
fun wildcardMatch(
|
||||
pattern: String,
|
||||
input: String,
|
||||
): Boolean {
|
||||
return pattern
|
||||
.split("*")
|
||||
.joinToString(".*") { Regex.escape(it) }
|
||||
.toRegex()
|
||||
.containsMatchIn(input)
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package ir.amirab.util.osfileutil
|
||||
|
||||
import ir.amirab.util.execAndWait
|
||||
import java.io.File
|
||||
|
||||
internal class LinuxFileUtils : FileUtilsBase() {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package ir.amirab.util.osfileutil
|
||||
|
||||
import ir.amirab.util.execAndWait
|
||||
import java.io.File
|
||||
|
||||
internal class MacOsFileUtils : FileUtilsBase() {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package ir.amirab.util.osfileutil
|
||||
|
||||
import ir.amirab.util.execAndWait
|
||||
import java.io.File
|
||||
|
||||
internal class WindowsFileUtils : FileUtilsBase() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user