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) {
|
implementation(libs.osThemeDetector) {
|
||||||
exclude(group = "net.java.dev.jna")
|
exclude(group = "net.java.dev.jna")
|
||||||
}
|
}
|
||||||
|
implementation(libs.proxyVole) {
|
||||||
// at the moment I don't use jna but some libraries does
|
exclude(group = "net.java.dev.jna")
|
||||||
// filekit and osThemeDetector both use jna but with different versions
|
}
|
||||||
// I excluded jna from both of them and add it here!
|
|
||||||
implementation(libs.jna.core)
|
implementation(libs.jna.core)
|
||||||
implementation(libs.jna.platform)
|
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.*
|
||||||
import com.abdownloadmanager.desktop.utils.native_messaging.NativeMessaging
|
import com.abdownloadmanager.desktop.utils.native_messaging.NativeMessaging
|
||||||
import com.abdownloadmanager.desktop.utils.native_messaging.NativeMessagingManifestApplier
|
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.decompose.DefaultComponentContext
|
||||||
import com.arkivanov.essenty.lifecycle.LifecycleRegistry
|
import com.arkivanov.essenty.lifecycle.LifecycleRegistry
|
||||||
import ir.amirab.downloader.DownloadManagerMinimalControl
|
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.IProxyStorage
|
||||||
import com.abdownloadmanager.shared.utils.proxy.ProxyData
|
import com.abdownloadmanager.shared.utils.proxy.ProxyData
|
||||||
import com.abdownloadmanager.shared.utils.proxy.ProxyManager
|
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.ProxyStrategyProvider
|
||||||
|
import ir.amirab.downloader.connection.proxy.SystemProxySelectorProvider
|
||||||
import ir.amirab.downloader.monitor.IDownloadMonitor
|
import ir.amirab.downloader.monitor.IDownloadMonitor
|
||||||
import ir.amirab.downloader.utils.EmptyFileCreator
|
import ir.amirab.downloader.utils.EmptyFileCreator
|
||||||
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
||||||
@ -112,6 +117,15 @@ val downloaderModule = module {
|
|||||||
get()
|
get()
|
||||||
)
|
)
|
||||||
}.bind<ProxyStrategyProvider>()
|
}.bind<ProxyStrategyProvider>()
|
||||||
|
single {
|
||||||
|
ProxyCachingConfig.default()
|
||||||
|
}
|
||||||
|
single<AutoConfigurableProxyProvider> {
|
||||||
|
AutoConfigurableProxyProviderForDesktop(get())
|
||||||
|
}
|
||||||
|
single<SystemProxySelectorProvider> {
|
||||||
|
DesktopSystemProxySelectorProvider(get())
|
||||||
|
}
|
||||||
single<DownloaderClient> {
|
single<DownloaderClient> {
|
||||||
OkHttpDownloaderClient(
|
OkHttpDownloaderClient(
|
||||||
OkHttpClient
|
OkHttpClient
|
||||||
@ -121,7 +135,9 @@ val downloaderModule = module {
|
|||||||
maxRequests = Int.MAX_VALUE
|
maxRequests = Int.MAX_VALUE
|
||||||
maxRequestsPerHost = Int.MAX_VALUE
|
maxRequestsPerHost = Int.MAX_VALUE
|
||||||
}).build(),
|
}).build(),
|
||||||
get()
|
get(),
|
||||||
|
get(),
|
||||||
|
get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
single {
|
single {
|
||||||
|
@ -231,6 +231,15 @@ fun proxyConfig(proxyManager: ProxyManager, scope: CoroutineScope): ProxyConfigu
|
|||||||
value = it.proxyWithRules.proxy.run { "$type $host:$port" }
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -21,13 +21,14 @@ import com.abdownloadmanager.shared.utils.ui.myColors
|
|||||||
import com.abdownloadmanager.shared.utils.ui.theme.myTextSizes
|
import com.abdownloadmanager.shared.utils.ui.theme.myTextSizes
|
||||||
import com.abdownloadmanager.shared.utils.div
|
import com.abdownloadmanager.shared.utils.div
|
||||||
import com.abdownloadmanager.resources.Res
|
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.LocalContentColor
|
||||||
import com.abdownloadmanager.shared.utils.ui.widget.MyIcon
|
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.Proxy
|
||||||
import ir.amirab.downloader.connection.proxy.ProxyType
|
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.compose.resources.myStringResource
|
||||||
import ir.amirab.util.desktop.DesktopUtils
|
import ir.amirab.util.desktop.DesktopUtils
|
||||||
|
|
||||||
@ -43,91 +44,62 @@ fun RenderProxyConfig(cfg: ProxyConfigurable, modifier: Modifier) {
|
|||||||
TitleAndDescription(cfg, true)
|
TitleAndDescription(cfg, true)
|
||||||
},
|
},
|
||||||
value = {
|
value = {
|
||||||
RenderSpinner(
|
RenderChangeProxyConfig(
|
||||||
enabled = enabled,
|
proxyWithRules = value,
|
||||||
possibleValues = ProxyMode.usableValues(),
|
setProxyWithRules = { setValue(it) }
|
||||||
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)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
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
|
@Stable
|
||||||
private class ProxyEditState(
|
private class ProxyEditState(
|
||||||
private val proxyWithRules: ProxyWithRules,
|
private val proxyData: ProxyData,
|
||||||
private val setProxyWithRules: (ProxyWithRules) -> Unit,
|
private val setProxyData: (ProxyData) -> Unit,
|
||||||
) {
|
) {
|
||||||
var proxyType = mutableStateOf(proxyWithRules.proxy.type)
|
var proxyMode = mutableStateOf(proxyData.proxyMode)
|
||||||
|
|
||||||
var proxyHost = mutableStateOf(proxyWithRules.proxy.host)
|
//pac
|
||||||
var proxyPort = mutableStateOf(proxyWithRules.proxy.port)
|
var pacURL = mutableStateOf(proxyData.pac.uri)
|
||||||
|
|
||||||
var useAuth = mutableStateOf(proxyWithRules.proxy.username != null)
|
//manual
|
||||||
var proxyUsername = mutableStateOf(proxyWithRules.proxy.username.orEmpty())
|
var proxyType = mutableStateOf(proxyData.proxyWithRules.proxy.type)
|
||||||
var proxyPassword = mutableStateOf(proxyWithRules.proxy.password.orEmpty())
|
|
||||||
|
|
||||||
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 canSave: Boolean by derivedStateOf {
|
||||||
|
when (proxyMode.value) {
|
||||||
|
ProxyMode.Direct -> true
|
||||||
|
ProxyMode.UseSystem -> true
|
||||||
|
ProxyMode.Manual -> {
|
||||||
val hostValid = proxyHost.value.isNotBlank()
|
val hostValid = proxyHost.value.isNotBlank()
|
||||||
hostValid
|
hostValid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProxyMode.Pac -> {
|
||||||
|
isValidUrl(pacURL.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
fun save() {
|
fun save() {
|
||||||
val useAuth = useAuth.value
|
val useAuth = useAuth.value
|
||||||
if (!canSave) {
|
if (!canSave) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setProxyWithRules(
|
setProxyData(
|
||||||
proxyWithRules.copy(
|
proxyData.copy(
|
||||||
|
proxyMode = proxyMode.value,
|
||||||
|
pac = proxyData.pac.copy(pacURL.value),
|
||||||
|
proxyWithRules = proxyData.proxyWithRules.copy(
|
||||||
proxy = Proxy(
|
proxy = Proxy(
|
||||||
type = proxyType.value,
|
type = proxyType.value,
|
||||||
host = proxyHost.value.trim(),
|
host = proxyHost.value.trim(),
|
||||||
@ -143,31 +115,32 @@ private class ProxyEditState(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderManualProxyConfig(
|
fun RenderChangeProxyConfig(
|
||||||
proxyWithRules: ProxyWithRules,
|
proxyWithRules: ProxyData,
|
||||||
setProxyWithRules: (ProxyWithRules) -> Unit,
|
setProxyWithRules: (ProxyData) -> Unit,
|
||||||
) {
|
) {
|
||||||
var showManualProxyConfig by remember {
|
var showProxyConfig by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
ActionButton(
|
ActionButton(
|
||||||
myStringResource(Res.string.change_proxy),
|
myStringResource(Res.string.change_proxy),
|
||||||
onClick = {
|
onClick = {
|
||||||
showManualProxyConfig = true
|
showProxyConfig = true
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (showManualProxyConfig) {
|
if (showProxyConfig) {
|
||||||
val dismiss = {
|
val dismiss = {
|
||||||
showManualProxyConfig = false
|
showProxyConfig = false
|
||||||
}
|
}
|
||||||
val state = remember(setProxyWithRules) {
|
val state = remember(setProxyWithRules) {
|
||||||
ProxyEditState(
|
ProxyEditState(
|
||||||
proxyWithRules = proxyWithRules,
|
proxyData = proxyWithRules,
|
||||||
setProxyWithRules = {
|
setProxyData = {
|
||||||
setProxyWithRules(it)
|
setProxyWithRules(it)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
@ -177,6 +150,7 @@ fun RenderManualProxyConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProxyEditDialog(
|
private fun ProxyEditDialog(
|
||||||
state: ProxyEditState,
|
state: ProxyEditState,
|
||||||
@ -185,6 +159,124 @@ private fun ProxyEditDialog(
|
|||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = (onDismiss),
|
onDismissRequest = (onDismiss),
|
||||||
content = {
|
content = {
|
||||||
|
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())
|
||||||
|
) {
|
||||||
|
Accordion(
|
||||||
|
possibleValues = ProxyMode.usableValues(),
|
||||||
|
selectedItem = mode,
|
||||||
|
renderHeader = {
|
||||||
|
val selected = it == mode
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(shape)
|
||||||
|
.clickable { setMode(it) }
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
value = selected,
|
||||||
|
onValueChange = {},
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(it.asStringSource().rememberString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ProxyConfigSpacer()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
ActionButton(
|
||||||
|
myStringResource(Res.string.change),
|
||||||
|
enabled = state.canSave,
|
||||||
|
onClick = {
|
||||||
|
state.save()
|
||||||
|
})
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
ActionButton(myStringResource(Res.string.cancel), onClick = {
|
||||||
|
onDismiss()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 (type, setType) = state.proxyType
|
||||||
val (host, setHost) = state.proxyHost
|
val (host, setHost) = state.proxyHost
|
||||||
val (port, setPort) = state.proxyPort
|
val (port, setPort) = state.proxyPort
|
||||||
@ -192,18 +284,6 @@ private fun ProxyEditDialog(
|
|||||||
val (username, setUsername) = state.proxyUsername
|
val (username, setUsername) = state.proxyUsername
|
||||||
val (password, setPassword) = state.proxyPassword
|
val (password, setPassword) = state.proxyPassword
|
||||||
val (excludeURLPatterns, setExcludeURLPatterns) = state.excludeURLPatterns
|
val (excludeURLPatterns, setExcludeURLPatterns) = state.excludeURLPatterns
|
||||||
|
|
||||||
SettingsDialog(
|
|
||||||
headerTitle = myStringResource(Res.string.proxy_change_title),
|
|
||||||
onDismiss = onDismiss,
|
|
||||||
content = {
|
|
||||||
Column(
|
|
||||||
Modifier
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
val spacer = @Composable {
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
DialogConfigItem(
|
DialogConfigItem(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
title = {
|
title = {
|
||||||
@ -226,7 +306,7 @@ private fun ProxyEditDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
spacer()
|
ProxyConfigSpacer()
|
||||||
DialogConfigItem(
|
DialogConfigItem(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
title = {
|
title = {
|
||||||
@ -256,7 +336,7 @@ private fun ProxyEditDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
spacer()
|
ProxyConfigSpacer()
|
||||||
DialogConfigItem(
|
DialogConfigItem(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
title = {
|
title = {
|
||||||
@ -296,7 +376,7 @@ private fun ProxyEditDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
spacer()
|
ProxyConfigSpacer()
|
||||||
DialogConfigItem(
|
DialogConfigItem(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
title = {
|
title = {
|
||||||
@ -321,23 +401,6 @@ private fun ProxyEditDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
ActionButton(
|
|
||||||
myStringResource(Res.string.change),
|
|
||||||
enabled = state.canSave,
|
|
||||||
onClick = {
|
|
||||||
state.save()
|
|
||||||
})
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
ActionButton(myStringResource(Res.string.cancel), onClick = {
|
|
||||||
onDismiss()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -362,7 +425,6 @@ private fun SettingsDialog(
|
|||||||
)
|
)
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.width(450.dp),
|
.width(450.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@ -385,7 +447,9 @@ private fun SettingsDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Box(Modifier.weight(1f, false)) {
|
||||||
content()
|
content()
|
||||||
|
}
|
||||||
actions?.let {
|
actions?.let {
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Row(
|
Row(
|
||||||
@ -398,6 +462,11 @@ private fun SettingsDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProxyConfigSpacer() {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DialogConfigItem(
|
private fun DialogConfigItem(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
@ -427,3 +496,35 @@ 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
|
package ir.amirab.downloader.connection
|
||||||
|
|
||||||
import ir.amirab.downloader.connection.proxy.ProxyStrategy
|
import ir.amirab.downloader.connection.proxy.*
|
||||||
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.connection.response.ResponseInfo
|
||||||
import ir.amirab.downloader.downloaditem.IDownloadCredentials
|
import ir.amirab.downloader.downloaditem.IDownloadCredentials
|
||||||
import ir.amirab.downloader.utils.await
|
import ir.amirab.downloader.utils.await
|
||||||
@ -14,6 +12,8 @@ import java.net.ProxySelector
|
|||||||
class OkHttpDownloaderClient(
|
class OkHttpDownloaderClient(
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val proxyStrategyProvider: ProxyStrategyProvider,
|
private val proxyStrategyProvider: ProxyStrategyProvider,
|
||||||
|
private val systemProxySelectorProvider: SystemProxySelectorProvider,
|
||||||
|
private val autoConfigurableProxyProvider: AutoConfigurableProxyProvider,
|
||||||
) : DownloaderClient() {
|
) : DownloaderClient() {
|
||||||
private fun newCall(
|
private fun newCall(
|
||||||
downloadCredentials: IDownloadCredentials,
|
downloadCredentials: IDownloadCredentials,
|
||||||
@ -67,10 +67,22 @@ class OkHttpDownloaderClient(
|
|||||||
ProxyStrategy.Direct -> return this
|
ProxyStrategy.Direct -> return this
|
||||||
ProxyStrategy.UseSystem -> {
|
ProxyStrategy.UseSystem -> {
|
||||||
newBuilder()
|
newBuilder()
|
||||||
.proxySelector(ProxySelector.getDefault())
|
.proxySelector(
|
||||||
|
systemProxySelectorProvider.getSystemProxySelector()
|
||||||
|
?: ProxySelector.getDefault()
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
is ProxyStrategy.ByScript -> {
|
||||||
|
val proxySelector = autoConfigurableProxyProvider.getAutoConfigurableProxy(strategy.scriptPath)
|
||||||
|
if (proxySelector != null) {
|
||||||
|
newBuilder()
|
||||||
|
.proxySelector(proxySelector)
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
is ProxyStrategy.ManualProxy -> {
|
is ProxyStrategy.ManualProxy -> {
|
||||||
val proxy = strategy.proxy
|
val proxy = strategy.proxy
|
||||||
return newBuilder()
|
return newBuilder()
|
||||||
|
@ -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 Direct : ProxyStrategy
|
||||||
data object UseSystem : ProxyStrategy
|
data object UseSystem : ProxyStrategy
|
||||||
data class ManualProxy(val proxy: Proxy) : 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"
|
markdownRenderer = "0.27.0"
|
||||||
autoServiceKsp = "1.2.0"
|
autoServiceKsp = "1.2.0"
|
||||||
autoService = "1.1.1"
|
autoService = "1.1.1"
|
||||||
|
proxyVole = "1.1.6"
|
||||||
|
|
||||||
[libraries]
|
[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" }
|
autoService-annoations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" }
|
||||||
|
|
||||||
systemTray = "com.dorkbox:SystemTray:4.4"
|
systemTray = "com.dorkbox:SystemTray:4.4"
|
||||||
|
proxyVole = { module = "org.bidib.com.github.markusbernhardt:proxy-vole", version.ref = "proxyVole" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
|
@ -15,6 +15,15 @@ data class ProxyWithRules(
|
|||||||
val rules: ProxyRules,
|
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 {
|
enum class ProxyMode {
|
||||||
@SerialName("direct")
|
@SerialName("direct")
|
||||||
Direct,
|
Direct,
|
||||||
@ -23,7 +32,10 @@ enum class ProxyMode {
|
|||||||
UseSystem,
|
UseSystem,
|
||||||
|
|
||||||
@SerialName("manual")
|
@SerialName("manual")
|
||||||
Manual;
|
Manual,
|
||||||
|
|
||||||
|
@SerialName("pac")
|
||||||
|
Pac;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun usableValues(): List<ProxyMode> {
|
fun usableValues(): List<ProxyMode> {
|
||||||
@ -31,6 +43,8 @@ enum class ProxyMode {
|
|||||||
// so we filter it for now.
|
// so we filter it for now.
|
||||||
return listOf(
|
return listOf(
|
||||||
Direct,
|
Direct,
|
||||||
|
UseSystem,
|
||||||
|
Pac,
|
||||||
Manual,
|
Manual,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -41,7 +55,10 @@ enum class ProxyMode {
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class ProxyData(
|
data class ProxyData(
|
||||||
val proxyMode: ProxyMode,
|
val proxyMode: ProxyMode,
|
||||||
|
//manual proxy config
|
||||||
val proxyWithRules: ProxyWithRules,
|
val proxyWithRules: ProxyWithRules,
|
||||||
|
//configuration script config
|
||||||
|
val pac: PACProxy,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun default() = ProxyData(
|
fun default() = ProxyData(
|
||||||
@ -49,7 +66,8 @@ data class ProxyData(
|
|||||||
proxyWithRules = ProxyWithRules(
|
proxyWithRules = ProxyWithRules(
|
||||||
proxy = Proxy.default(),
|
proxy = Proxy.default(),
|
||||||
rules = ProxyRules(emptyList())
|
rules = ProxyRules(emptyList())
|
||||||
)
|
),
|
||||||
|
pac = PACProxy.default()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package com.abdownloadmanager.shared.utils.proxy
|
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.Proxy
|
||||||
import ir.amirab.downloader.connection.proxy.ProxyStrategy
|
import ir.amirab.downloader.connection.proxy.ProxyStrategy
|
||||||
import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider
|
import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider
|
||||||
@ -18,6 +19,9 @@ class ProxyManager(
|
|||||||
Authenticator.setDefault(mySocksProxyAuthenticator)
|
Authenticator.setDefault(mySocksProxyAuthenticator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* I don't like this it's better to improve this later
|
||||||
|
*/
|
||||||
private fun getProxyModeForThisURL(url: String): ProxyStrategy {
|
private fun getProxyModeForThisURL(url: String): ProxyStrategy {
|
||||||
val usingProxy = proxyData.value
|
val usingProxy = proxyData.value
|
||||||
return when (usingProxy.proxyMode) {
|
return when (usingProxy.proxyMode) {
|
||||||
@ -31,6 +35,14 @@ class ProxyManager(
|
|||||||
ProxyStrategy.Direct
|
ProxyStrategy.Direct
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ProxyMode.Pac -> {
|
||||||
|
val pacURI = usingProxy.pac.uri
|
||||||
|
if (isValidUrl(pacURI)) {
|
||||||
|
ProxyStrategy.ByScript(pacURI)
|
||||||
|
} else {
|
||||||
|
ProxyStrategy.Direct
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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_no_proxy=No Proxy will be used
|
||||||
settings_use_proxy_describe_system_proxy=System 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_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=Track Deleted Files On Disk
|
||||||
settings_track_deleted_files_on_disk_description=Automatically remove files from the list when they are deleted or moved from the download directory.
|
settings_track_deleted_files_on_disk_description=Automatically remove files from the list when they are deleted or moved from the download directory.
|
||||||
settings_download_speed_unit=Download Speed Unit
|
settings_download_speed_unit=Download Speed Unit
|
||||||
@ -317,6 +318,8 @@ change_proxy=Change Proxy
|
|||||||
proxy_no=No Proxy
|
proxy_no=No Proxy
|
||||||
proxy_system=System Proxy
|
proxy_system=System Proxy
|
||||||
proxy_manual=Manual Proxy
|
proxy_manual=Manual Proxy
|
||||||
|
proxy_pac=Proxy Auto Configuration
|
||||||
|
proxy_pac_url=Proxy Auto Configuration URL
|
||||||
address=Address
|
address=Address
|
||||||
port=Port
|
port=Port
|
||||||
address_and_port=Address & Port
|
address_and_port=Address & Port
|
||||||
|
Loading…
x
Reference in New Issue
Block a user