mirror of
https://github.com/amir1376/ab-download-manager.git
synced 2025-02-20 11:43:24 +08:00
feature/system proxy and pac support (#371)
* add system proxy support * add proxy pac support
This commit is contained in:
parent
f50e3e10e8
commit
3db6306177
@ -49,10 +49,9 @@ dependencies {
|
||||
implementation(libs.osThemeDetector) {
|
||||
exclude(group = "net.java.dev.jna")
|
||||
}
|
||||
|
||||
// at the moment I don't use jna but some libraries does
|
||||
// filekit and osThemeDetector both use jna but with different versions
|
||||
// I excluded jna from both of them and add it here!
|
||||
implementation(libs.proxyVole) {
|
||||
exclude(group = "net.java.dev.jna")
|
||||
}
|
||||
implementation(libs.jna.core)
|
||||
implementation(libs.jna.platform)
|
||||
|
||||
|
@ -18,6 +18,9 @@ import com.abdownloadmanager.shared.utils.ui.theme.ISystemThemeDetector
|
||||
import com.abdownloadmanager.desktop.utils.*
|
||||
import com.abdownloadmanager.desktop.utils.native_messaging.NativeMessaging
|
||||
import com.abdownloadmanager.desktop.utils.native_messaging.NativeMessagingManifestApplier
|
||||
import com.abdownloadmanager.desktop.utils.proxy.AutoConfigurableProxyProviderForDesktop
|
||||
import com.abdownloadmanager.desktop.utils.proxy.DesktopSystemProxySelectorProvider
|
||||
import com.abdownloadmanager.desktop.utils.proxy.ProxyCachingConfig
|
||||
import com.arkivanov.decompose.DefaultComponentContext
|
||||
import com.arkivanov.essenty.lifecycle.LifecycleRegistry
|
||||
import ir.amirab.downloader.DownloadManagerMinimalControl
|
||||
@ -55,7 +58,9 @@ import com.abdownloadmanager.shared.utils.ui.IMyIcons
|
||||
import com.abdownloadmanager.shared.utils.proxy.IProxyStorage
|
||||
import com.abdownloadmanager.shared.utils.proxy.ProxyData
|
||||
import com.abdownloadmanager.shared.utils.proxy.ProxyManager
|
||||
import ir.amirab.downloader.connection.proxy.AutoConfigurableProxyProvider
|
||||
import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider
|
||||
import ir.amirab.downloader.connection.proxy.SystemProxySelectorProvider
|
||||
import ir.amirab.downloader.monitor.IDownloadMonitor
|
||||
import ir.amirab.downloader.utils.EmptyFileCreator
|
||||
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
||||
@ -112,6 +117,15 @@ val downloaderModule = module {
|
||||
get()
|
||||
)
|
||||
}.bind<ProxyStrategyProvider>()
|
||||
single {
|
||||
ProxyCachingConfig.default()
|
||||
}
|
||||
single<AutoConfigurableProxyProvider> {
|
||||
AutoConfigurableProxyProviderForDesktop(get())
|
||||
}
|
||||
single<SystemProxySelectorProvider> {
|
||||
DesktopSystemProxySelectorProvider(get())
|
||||
}
|
||||
single<DownloaderClient> {
|
||||
OkHttpDownloaderClient(
|
||||
OkHttpClient
|
||||
@ -121,7 +135,9 @@ val downloaderModule = module {
|
||||
maxRequests = Int.MAX_VALUE
|
||||
maxRequestsPerHost = Int.MAX_VALUE
|
||||
}).build(),
|
||||
get()
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
)
|
||||
}
|
||||
single {
|
||||
@ -341,4 +357,4 @@ object Di : KoinComponent {
|
||||
modules(appModule)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -231,6 +231,15 @@ fun proxyConfig(proxyManager: ProxyManager, scope: CoroutineScope): ProxyConfigu
|
||||
value = it.proxyWithRules.proxy.run { "$type $host:$port" }
|
||||
)
|
||||
)
|
||||
|
||||
ProxyMode.Pac -> {
|
||||
Res.string.settings_use_proxy_describe_pac_proxy
|
||||
.asStringSourceWithARgs(
|
||||
Res.string.settings_use_proxy_describe_pac_proxy_createArgs(
|
||||
value = it.pac.uri
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -461,4 +470,4 @@ class SettingsComponent(
|
||||
val configurables by derivedStateOf {
|
||||
allConfigs[currentPage]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,13 +21,14 @@ import com.abdownloadmanager.shared.utils.ui.myColors
|
||||
import com.abdownloadmanager.shared.utils.ui.theme.myTextSizes
|
||||
import com.abdownloadmanager.shared.utils.div
|
||||
import com.abdownloadmanager.resources.Res
|
||||
import com.abdownloadmanager.shared.utils.isValidUrl
|
||||
import com.abdownloadmanager.shared.utils.proxy.*
|
||||
import com.abdownloadmanager.shared.utils.ui.LocalContentColor
|
||||
import com.abdownloadmanager.shared.utils.ui.widget.MyIcon
|
||||
import com.abdownloadmanager.shared.utils.proxy.ProxyMode
|
||||
import com.abdownloadmanager.shared.utils.proxy.ProxyRules
|
||||
import com.abdownloadmanager.shared.utils.proxy.ProxyWithRules
|
||||
import ir.amirab.downloader.connection.proxy.Proxy
|
||||
import ir.amirab.downloader.connection.proxy.ProxyType
|
||||
import ir.amirab.util.compose.StringSource
|
||||
import ir.amirab.util.compose.asStringSource
|
||||
import ir.amirab.util.compose.resources.myStringResource
|
||||
import ir.amirab.util.desktop.DesktopUtils
|
||||
|
||||
@ -43,82 +44,50 @@ fun RenderProxyConfig(cfg: ProxyConfigurable, modifier: Modifier) {
|
||||
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 = myStringResource(
|
||||
when (it) {
|
||||
ProxyMode.Direct -> Res.string.proxy_no
|
||||
ProxyMode.UseSystem -> Res.string.proxy_system
|
||||
ProxyMode.Manual -> Res.string.proxy_manual
|
||||
}
|
||||
)
|
||||
Text(text)
|
||||
},
|
||||
RenderChangeProxyConfig(
|
||||
proxyWithRules = value,
|
||||
setProxyWithRules = { setValue(it) }
|
||||
)
|
||||
},
|
||||
nestedContent = {
|
||||
AnimatedContent(value.proxyMode.takeIf { enabled }) {
|
||||
when (it) {
|
||||
ProxyMode.Direct -> {}
|
||||
ProxyMode.UseSystem -> {
|
||||
ActionButton(
|
||||
myStringResource(Res.string.proxy_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,
|
||||
private val proxyData: ProxyData,
|
||||
private val setProxyData: (ProxyData) -> Unit,
|
||||
) {
|
||||
var proxyType = mutableStateOf(proxyWithRules.proxy.type)
|
||||
var proxyMode = mutableStateOf(proxyData.proxyMode)
|
||||
|
||||
var proxyHost = mutableStateOf(proxyWithRules.proxy.host)
|
||||
var proxyPort = mutableStateOf(proxyWithRules.proxy.port)
|
||||
//pac
|
||||
var pacURL = mutableStateOf(proxyData.pac.uri)
|
||||
|
||||
var useAuth = mutableStateOf(proxyWithRules.proxy.username != null)
|
||||
var proxyUsername = mutableStateOf(proxyWithRules.proxy.username.orEmpty())
|
||||
var proxyPassword = mutableStateOf(proxyWithRules.proxy.password.orEmpty())
|
||||
//manual
|
||||
var proxyType = mutableStateOf(proxyData.proxyWithRules.proxy.type)
|
||||
|
||||
var excludeURLPatterns = mutableStateOf(proxyWithRules.rules.excludeURLPatterns.joinToString(" "))
|
||||
var proxyHost = mutableStateOf(proxyData.proxyWithRules.proxy.host)
|
||||
var proxyPort = mutableStateOf(proxyData.proxyWithRules.proxy.port)
|
||||
|
||||
var useAuth = mutableStateOf(proxyData.proxyWithRules.proxy.username != null)
|
||||
var proxyUsername = mutableStateOf(proxyData.proxyWithRules.proxy.username.orEmpty())
|
||||
var proxyPassword = mutableStateOf(proxyData.proxyWithRules.proxy.password.orEmpty())
|
||||
|
||||
var excludeURLPatterns = mutableStateOf(proxyData.proxyWithRules.rules.excludeURLPatterns.joinToString(" "))
|
||||
|
||||
val canSave: Boolean by derivedStateOf {
|
||||
val hostValid = proxyHost.value.isNotBlank()
|
||||
hostValid
|
||||
when (proxyMode.value) {
|
||||
ProxyMode.Direct -> true
|
||||
ProxyMode.UseSystem -> true
|
||||
ProxyMode.Manual -> {
|
||||
val hostValid = proxyHost.value.isNotBlank()
|
||||
hostValid
|
||||
}
|
||||
|
||||
ProxyMode.Pac -> {
|
||||
isValidUrl(pacURL.value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun save() {
|
||||
@ -126,20 +95,24 @@ private class ProxyEditState(
|
||||
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() },
|
||||
setProxyData(
|
||||
proxyData.copy(
|
||||
proxyMode = proxyMode.value,
|
||||
pac = proxyData.pac.copy(pacURL.value),
|
||||
proxyWithRules = proxyData.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() },
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -147,27 +120,27 @@ private class ProxyEditState(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderManualProxyConfig(
|
||||
proxyWithRules: ProxyWithRules,
|
||||
setProxyWithRules: (ProxyWithRules) -> Unit,
|
||||
fun RenderChangeProxyConfig(
|
||||
proxyWithRules: ProxyData,
|
||||
setProxyWithRules: (ProxyData) -> Unit,
|
||||
) {
|
||||
var showManualProxyConfig by remember {
|
||||
var showProxyConfig by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
ActionButton(
|
||||
myStringResource(Res.string.change_proxy),
|
||||
onClick = {
|
||||
showManualProxyConfig = true
|
||||
showProxyConfig = true
|
||||
},
|
||||
)
|
||||
if (showManualProxyConfig) {
|
||||
if (showProxyConfig) {
|
||||
val dismiss = {
|
||||
showManualProxyConfig = false
|
||||
showProxyConfig = false
|
||||
}
|
||||
val state = remember(setProxyWithRules) {
|
||||
ProxyEditState(
|
||||
proxyWithRules = proxyWithRules,
|
||||
setProxyWithRules = {
|
||||
proxyData = proxyWithRules,
|
||||
setProxyData = {
|
||||
setProxyWithRules(it)
|
||||
dismiss()
|
||||
}
|
||||
@ -177,6 +150,7 @@ fun RenderManualProxyConfig(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun ProxyEditDialog(
|
||||
state: ProxyEditState,
|
||||
@ -185,142 +159,74 @@ private fun ProxyEditDialog(
|
||||
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
|
||||
|
||||
val (mode, setMode) = state.proxyMode
|
||||
SettingsDialog(
|
||||
headerTitle = myStringResource(Res.string.proxy_change_title),
|
||||
onDismiss = onDismiss,
|
||||
content = {
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
val spacer = @Composable {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
DialogConfigItem(
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
Text(myStringResource(Res.string.proxy_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(myStringResource(Res.string.address_and_port))
|
||||
},
|
||||
value = {
|
||||
Accordion(
|
||||
possibleValues = ProxyMode.usableValues(),
|
||||
selectedItem = mode,
|
||||
renderHeader = {
|
||||
val selected = it == mode
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape)
|
||||
.clickable { setMode(it) }
|
||||
.padding(8.dp)
|
||||
) {
|
||||
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 = myStringResource(Res.string.port),
|
||||
range = 1..65535,
|
||||
modifier = Modifier.width(96.dp),
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
textPadding = PaddingValues(8.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
RadioButton(
|
||||
value = selected,
|
||||
onValueChange = {},
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(it.asStringSource().rememberString())
|
||||
}
|
||||
}
|
||||
)
|
||||
spacer()
|
||||
DialogConfigItem(
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier.onClick {
|
||||
setUseAuth(!useAuth)
|
||||
},
|
||||
renderContent = {
|
||||
val cm = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape)
|
||||
.border(1.dp, myColors.onBackground / 0.15f, shape)
|
||||
.background(myColors.background / 25)
|
||||
.padding(8.dp)
|
||||
when (it) {
|
||||
ProxyMode.Direct -> {
|
||||
|
||||
}
|
||||
|
||||
ProxyMode.UseSystem -> {
|
||||
Column(cm) {
|
||||
ActionButton(
|
||||
myStringResource(Res.string.proxy_open_system_proxy_settings),
|
||||
onClick = {
|
||||
DesktopUtils.openSystemProxySettings()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ProxyMode.Manual -> {
|
||||
Column(cm) {
|
||||
RenderManualConfig(state)
|
||||
}
|
||||
}
|
||||
|
||||
ProxyMode.Pac -> {
|
||||
Column(cm) {
|
||||
RenderPACConfig(state)
|
||||
}
|
||||
}
|
||||
) {
|
||||
CheckBox(
|
||||
value = useAuth,
|
||||
onValueChange = setUseAuth,
|
||||
size = 16.dp
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(myStringResource(Res.string.use_authentication))
|
||||
}
|
||||
},
|
||||
value = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MyTextField(
|
||||
text = username,
|
||||
onTextChange = setUsername,
|
||||
placeholder = myStringResource(Res.string.username),
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = useAuth,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
MyTextField(
|
||||
text = password,
|
||||
onTextChange = setPassword,
|
||||
placeholder = myStringResource(Res.string.password),
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = useAuth,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
spacer()
|
||||
DialogConfigItem(
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
Row {
|
||||
Text(myStringResource(Res.string.proxy_do_not_use_proxy_for))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
com.abdownloadmanager.shared.ui.widget.Help(
|
||||
myStringResource(Res.string.proxy_do_not_use_proxy_for_description)
|
||||
)
|
||||
}
|
||||
},
|
||||
value = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MyTextField(
|
||||
text = excludeURLPatterns,
|
||||
onTextChange = setExcludeURLPatterns,
|
||||
placeholder = "example.com 192.168.1.*",
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
ProxyConfigSpacer()
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
@ -340,6 +246,163 @@ private fun ProxyEditDialog(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderPACConfig(
|
||||
state: ProxyEditState,
|
||||
) {
|
||||
Column {
|
||||
val (url, setPacUrl) = state.pacURL
|
||||
DialogConfigItem(
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
Text(myStringResource(Res.string.proxy_pac_url))
|
||||
},
|
||||
value = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MyTextField(
|
||||
text = url,
|
||||
onTextChange = setPacUrl,
|
||||
placeholder = "http://path/to/file.pac",
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderManualConfig(
|
||||
state: ProxyEditState,
|
||||
) {
|
||||
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
|
||||
DialogConfigItem(
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
Text(myStringResource(Res.string.proxy_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,
|
||||
)
|
||||
}
|
||||
)
|
||||
ProxyConfigSpacer()
|
||||
DialogConfigItem(
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
Text(myStringResource(Res.string.address_and_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 = myStringResource(Res.string.port),
|
||||
range = 1..65535,
|
||||
modifier = Modifier.width(96.dp),
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
textPadding = PaddingValues(8.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
ProxyConfigSpacer()
|
||||
DialogConfigItem(
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier.onClick {
|
||||
setUseAuth(!useAuth)
|
||||
}
|
||||
) {
|
||||
CheckBox(
|
||||
value = useAuth,
|
||||
onValueChange = setUseAuth,
|
||||
size = 16.dp
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(myStringResource(Res.string.use_authentication))
|
||||
}
|
||||
},
|
||||
value = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MyTextField(
|
||||
text = username,
|
||||
onTextChange = setUsername,
|
||||
placeholder = myStringResource(Res.string.username),
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = useAuth,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
MyTextField(
|
||||
text = password,
|
||||
onTextChange = setPassword,
|
||||
placeholder = myStringResource(Res.string.password),
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = useAuth,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
ProxyConfigSpacer()
|
||||
DialogConfigItem(
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
Row {
|
||||
Text(myStringResource(Res.string.proxy_do_not_use_proxy_for))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
com.abdownloadmanager.shared.ui.widget.Help(
|
||||
myStringResource(Res.string.proxy_do_not_use_proxy_for_description)
|
||||
)
|
||||
}
|
||||
},
|
||||
value = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MyTextField(
|
||||
text = excludeURLPatterns,
|
||||
onTextChange = setExcludeURLPatterns,
|
||||
placeholder = "example.com 192.168.1.*",
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsDialog(
|
||||
headerTitle: String,
|
||||
@ -362,7 +425,6 @@ private fun SettingsDialog(
|
||||
)
|
||||
.padding(16.dp)
|
||||
.width(450.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@ -385,7 +447,9 @@ private fun SettingsDialog(
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
content()
|
||||
Box(Modifier.weight(1f, false)) {
|
||||
content()
|
||||
}
|
||||
actions?.let {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(
|
||||
@ -398,6 +462,11 @@ private fun SettingsDialog(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProxyConfigSpacer() {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DialogConfigItem(
|
||||
modifier: Modifier,
|
||||
@ -426,4 +495,36 @@ private fun DialogConfigItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ProxyMode.asStringSource(): StringSource {
|
||||
return when (this) {
|
||||
ProxyMode.Direct -> Res.string.proxy_no
|
||||
ProxyMode.UseSystem -> Res.string.proxy_system
|
||||
ProxyMode.Manual -> Res.string.proxy_manual
|
||||
ProxyMode.Pac -> Res.string.proxy_pac
|
||||
}.asStringSource()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T> Accordion(
|
||||
possibleValues: List<T>,
|
||||
selectedItem: T,
|
||||
renderHeader: @Composable (T) -> Unit,
|
||||
renderContent: @Composable (T) -> Unit,
|
||||
) {
|
||||
Column {
|
||||
possibleValues.forEach {
|
||||
ExpandableItem(
|
||||
modifier = Modifier,
|
||||
isExpanded = selectedItem == it,
|
||||
header = {
|
||||
renderHeader(it)
|
||||
},
|
||||
body = {
|
||||
renderContent(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
package com.abdownloadmanager.desktop.utils.proxy
|
||||
|
||||
import com.github.markusbernhardt.proxy.selector.misc.BufferedProxySelector
|
||||
import com.github.markusbernhardt.proxy.selector.misc.ProxyListFallbackSelector
|
||||
import com.github.markusbernhardt.proxy.selector.pac.PacProxySelector
|
||||
import com.github.markusbernhardt.proxy.selector.pac.UrlPacScriptSource
|
||||
import ir.amirab.downloader.connection.proxy.AutoConfigurableProxyProvider
|
||||
import java.net.ProxySelector
|
||||
|
||||
class AutoConfigurableProxyProviderForDesktop(
|
||||
private val proxyCachingConfig: ProxyCachingConfig
|
||||
) : AutoConfigurableProxyProvider {
|
||||
@Volatile
|
||||
private var packProxySelector: ProxySelector? = null
|
||||
|
||||
@Volatile
|
||||
private var lastUsedUri: String? = null
|
||||
override fun getAutoConfigurableProxy(uri: String): ProxySelector? {
|
||||
if (lastUsedUri == uri) {
|
||||
val o = packProxySelector
|
||||
return o ?: createAndInitializePacProxySelector(uri)
|
||||
} else {
|
||||
return createAndInitializePacProxySelector(uri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAndInitializePacProxySelector(uri: String): ProxySelector {
|
||||
synchronized(this) {
|
||||
val s = installBufferingAndFallbackBehaviour(PacProxySelector(UrlPacScriptSource(uri)))
|
||||
lastUsedUri = uri
|
||||
packProxySelector = s
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
private fun installBufferingAndFallbackBehaviour(selector: ProxySelector): ProxySelector {
|
||||
var selector = selector
|
||||
if (selector is PacProxySelector) {
|
||||
if (proxyCachingConfig.pacCacheSize > 0) {
|
||||
selector = BufferedProxySelector(
|
||||
proxyCachingConfig.pacCacheSize,
|
||||
proxyCachingConfig.pacCacheTTL,
|
||||
selector,
|
||||
proxyCachingConfig.pacCacheScope
|
||||
)
|
||||
}
|
||||
selector = ProxyListFallbackSelector(selector)
|
||||
}
|
||||
return selector
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package com.abdownloadmanager.desktop.utils.proxy
|
||||
|
||||
import com.github.markusbernhardt.proxy.ProxySearch
|
||||
import ir.amirab.downloader.connection.proxy.SystemProxySelectorProvider
|
||||
import java.net.ProxySelector
|
||||
|
||||
class DesktopSystemProxySelectorProvider(
|
||||
private val proxyCachingConfig: ProxyCachingConfig
|
||||
) : SystemProxySelectorProvider {
|
||||
private val proxySearch by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
createProxySearch()
|
||||
}
|
||||
|
||||
private fun createProxySearch(): ProxySearch {
|
||||
return ProxySearch.getDefaultProxySearch().apply {
|
||||
setPacCacheSettings(
|
||||
proxyCachingConfig.pacCacheSize,
|
||||
proxyCachingConfig.pacCacheTTL,
|
||||
proxyCachingConfig.pacCacheScope
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSystemProxySelector(): ProxySelector? {
|
||||
return proxySearch.proxySelector
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.abdownloadmanager.desktop.utils.proxy
|
||||
|
||||
import com.github.markusbernhardt.proxy.selector.misc.BufferedProxySelector
|
||||
|
||||
class ProxyCachingConfig(
|
||||
val pacCacheSize: Int,
|
||||
val pacCacheTTL: Long,
|
||||
val pacCacheScope: BufferedProxySelector.CacheScope
|
||||
) {
|
||||
companion object {
|
||||
fun default() = ProxyCachingConfig(
|
||||
pacCacheSize = 10,
|
||||
pacCacheTTL = 60 * 60 * 1000,
|
||||
pacCacheScope = BufferedProxySelector.CacheScope.CACHE_SCOPE_URL
|
||||
)
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
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.proxy.*
|
||||
import ir.amirab.downloader.connection.response.ResponseInfo
|
||||
import ir.amirab.downloader.downloaditem.IDownloadCredentials
|
||||
import ir.amirab.downloader.utils.await
|
||||
@ -14,6 +12,8 @@ import java.net.ProxySelector
|
||||
class OkHttpDownloaderClient(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val proxyStrategyProvider: ProxyStrategyProvider,
|
||||
private val systemProxySelectorProvider: SystemProxySelectorProvider,
|
||||
private val autoConfigurableProxyProvider: AutoConfigurableProxyProvider,
|
||||
) : DownloaderClient() {
|
||||
private fun newCall(
|
||||
downloadCredentials: IDownloadCredentials,
|
||||
@ -67,10 +67,22 @@ class OkHttpDownloaderClient(
|
||||
ProxyStrategy.Direct -> return this
|
||||
ProxyStrategy.UseSystem -> {
|
||||
newBuilder()
|
||||
.proxySelector(ProxySelector.getDefault())
|
||||
.proxySelector(
|
||||
systemProxySelectorProvider.getSystemProxySelector()
|
||||
?: ProxySelector.getDefault()
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
is ProxyStrategy.ByScript -> {
|
||||
val proxySelector = autoConfigurableProxyProvider.getAutoConfigurableProxy(strategy.scriptPath)
|
||||
if (proxySelector != null) {
|
||||
newBuilder()
|
||||
.proxySelector(proxySelector)
|
||||
.build()
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
is ProxyStrategy.ManualProxy -> {
|
||||
val proxy = strategy.proxy
|
||||
return newBuilder()
|
||||
@ -158,4 +170,4 @@ class OkHttpDownloaderClient(
|
||||
responseInfo = createFileInfo(response)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
package ir.amirab.downloader.connection.proxy
|
||||
|
||||
import java.net.ProxySelector
|
||||
import java.net.URI
|
||||
|
||||
interface AutoConfigurableProxyProvider {
|
||||
fun getAutoConfigurableProxy(
|
||||
uri: String
|
||||
): ProxySelector?
|
||||
}
|
@ -4,4 +4,5 @@ sealed interface ProxyStrategy {
|
||||
data object Direct : ProxyStrategy
|
||||
data object UseSystem : ProxyStrategy
|
||||
data class ManualProxy(val proxy: Proxy) : ProxyStrategy
|
||||
}
|
||||
data class ByScript(val scriptPath: String) : ProxyStrategy
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package ir.amirab.downloader.connection.proxy
|
||||
|
||||
import java.net.ProxySelector
|
||||
|
||||
interface SystemProxySelectorProvider {
|
||||
fun getSystemProxySelector(): ProxySelector?
|
||||
}
|
@ -29,6 +29,7 @@ kotlinFileWatcher = "1.3.0"
|
||||
markdownRenderer = "0.27.0"
|
||||
autoServiceKsp = "1.2.0"
|
||||
autoService = "1.1.1"
|
||||
proxyVole = "1.1.6"
|
||||
|
||||
[libraries]
|
||||
|
||||
@ -129,6 +130,7 @@ autoService-ksp = { module = "dev.zacsweers.autoservice:auto-service-ksp", versi
|
||||
autoService-annoations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" }
|
||||
|
||||
systemTray = "com.dorkbox:SystemTray:4.4"
|
||||
proxyVole = { module = "org.bidib.com.github.markusbernhardt:proxy-vole", version.ref = "proxyVole" }
|
||||
|
||||
[plugins]
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
|
@ -15,6 +15,15 @@ data class ProxyWithRules(
|
||||
val rules: ProxyRules,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PACProxy(
|
||||
val uri: String,// an uri to get script path of the PAC
|
||||
) {
|
||||
companion object {
|
||||
fun default() = PACProxy("http://localhost/some.pac")
|
||||
}
|
||||
}
|
||||
|
||||
enum class ProxyMode {
|
||||
@SerialName("direct")
|
||||
Direct,
|
||||
@ -23,7 +32,10 @@ enum class ProxyMode {
|
||||
UseSystem,
|
||||
|
||||
@SerialName("manual")
|
||||
Manual;
|
||||
Manual,
|
||||
|
||||
@SerialName("pac")
|
||||
Pac;
|
||||
|
||||
companion object {
|
||||
fun usableValues(): List<ProxyMode> {
|
||||
@ -31,6 +43,8 @@ enum class ProxyMode {
|
||||
// so we filter it for now.
|
||||
return listOf(
|
||||
Direct,
|
||||
UseSystem,
|
||||
Pac,
|
||||
Manual,
|
||||
)
|
||||
}
|
||||
@ -41,7 +55,10 @@ enum class ProxyMode {
|
||||
@Serializable
|
||||
data class ProxyData(
|
||||
val proxyMode: ProxyMode,
|
||||
//manual proxy config
|
||||
val proxyWithRules: ProxyWithRules,
|
||||
//configuration script config
|
||||
val pac: PACProxy,
|
||||
) {
|
||||
companion object {
|
||||
fun default() = ProxyData(
|
||||
@ -49,7 +66,8 @@ data class ProxyData(
|
||||
proxyWithRules = ProxyWithRules(
|
||||
proxy = Proxy.default(),
|
||||
rules = ProxyRules(emptyList())
|
||||
)
|
||||
),
|
||||
pac = PACProxy.default()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.abdownloadmanager.shared.utils.proxy
|
||||
|
||||
import com.abdownloadmanager.shared.utils.isValidUrl
|
||||
import ir.amirab.downloader.connection.proxy.Proxy
|
||||
import ir.amirab.downloader.connection.proxy.ProxyStrategy
|
||||
import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider
|
||||
@ -18,6 +19,9 @@ class ProxyManager(
|
||||
Authenticator.setDefault(mySocksProxyAuthenticator)
|
||||
}
|
||||
|
||||
/**
|
||||
* I don't like this it's better to improve this later
|
||||
*/
|
||||
private fun getProxyModeForThisURL(url: String): ProxyStrategy {
|
||||
val usingProxy = proxyData.value
|
||||
return when (usingProxy.proxyMode) {
|
||||
@ -31,6 +35,14 @@ class ProxyManager(
|
||||
ProxyStrategy.Direct
|
||||
}
|
||||
}
|
||||
ProxyMode.Pac -> {
|
||||
val pacURI = usingProxy.pac.uri
|
||||
if (isValidUrl(pacURI)) {
|
||||
ProxyStrategy.ByScript(pacURI)
|
||||
} else {
|
||||
ProxyStrategy.Direct
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,4 +81,4 @@ private class MySocksProxyAuthenticator(
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,89 @@
|
||||
package com.abdownloadmanager.shared.ui.widget
|
||||
|
||||
import com.abdownloadmanager.shared.utils.ui.LocalContentColor
|
||||
import com.abdownloadmanager.shared.utils.ui.widget.MyIcon
|
||||
import com.abdownloadmanager.shared.utils.ui.icon.MyIcons
|
||||
import com.abdownloadmanager.shared.utils.ui.myColors
|
||||
import ir.amirab.util.ifThen
|
||||
import com.abdownloadmanager.shared.utils.div
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.selection.triStateToggleable
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun RadioButton(
|
||||
value: Boolean,
|
||||
onValueChange: (Boolean) -> Unit,
|
||||
enabled: Boolean = true,
|
||||
modifier: Modifier = Modifier,
|
||||
size: Dp = 18.dp,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
uncheckedAlpha: Float = 0.25f,
|
||||
) {
|
||||
val shape = CircleShape
|
||||
Box(
|
||||
modifier
|
||||
.ifThen(!enabled) {
|
||||
alpha(0.5f)
|
||||
}
|
||||
.size(size)
|
||||
.clip(shape)
|
||||
.triStateToggleable(
|
||||
state = ToggleableState(value),
|
||||
enabled = enabled,
|
||||
role = Role.RadioButton,
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
onClick = { onValueChange(!value) },
|
||||
)
|
||||
) {
|
||||
Spacer(
|
||||
Modifier.matchParentSize()
|
||||
.border(
|
||||
1.dp,
|
||||
if (value) {
|
||||
myColors.primaryGradient
|
||||
} else {
|
||||
SolidColor(LocalContentColor.current / uncheckedAlpha)
|
||||
},
|
||||
shape
|
||||
)
|
||||
)
|
||||
AnimatedContent(
|
||||
value,
|
||||
transitionSpec = {
|
||||
val tween = tween<Float>(220)
|
||||
fadeIn(tween) togetherWith fadeOut(tween)
|
||||
}
|
||||
) {
|
||||
val m = Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(animateFloatAsState(if (value) 1f else 0f).value)
|
||||
.padding(4.dp)
|
||||
.clip(shape)
|
||||
.background(myColors.primaryGradient)
|
||||
Spacer(m)
|
||||
}
|
||||
}
|
||||
}
|
@ -195,6 +195,7 @@ settings_use_proxy_description=Use proxy for downloading files
|
||||
settings_use_proxy_describe_no_proxy=No Proxy will be used
|
||||
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_pac_proxy=pac file "{{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
|
||||
@ -317,6 +318,8 @@ change_proxy=Change Proxy
|
||||
proxy_no=No Proxy
|
||||
proxy_system=System Proxy
|
||||
proxy_manual=Manual Proxy
|
||||
proxy_pac=Proxy Auto Configuration
|
||||
proxy_pac_url=Proxy Auto Configuration URL
|
||||
address=Address
|
||||
port=Port
|
||||
address_and_port=Address & Port
|
||||
|
Loading…
x
Reference in New Issue
Block a user