feature/system proxy and pac support (#371)

* add system proxy support

* add proxy pac support
This commit is contained in:
AmirHossein Abdolmotallebi 2025-01-17 00:18:02 +03:30 committed by GitHub
parent f50e3e10e8
commit 3db6306177
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 604 additions and 230 deletions

View File

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

View File

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

View File

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

View File

@ -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,91 +44,62 @@ 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
)
RenderChangeProxyConfig(
proxyWithRules = value,
setProxyWithRules = { setValue(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
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 {
when (proxyMode.value) {
ProxyMode.Direct -> true
ProxyMode.UseSystem -> true
ProxyMode.Manual -> {
val hostValid = proxyHost.value.isNotBlank()
hostValid
}
ProxyMode.Pac -> {
isValidUrl(pacURL.value)
}
}
}
fun save() {
val useAuth = useAuth.value
if (!canSave) {
return
}
setProxyWithRules(
proxyWithRules.copy(
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(),
@ -143,31 +115,32 @@ 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,6 +159,124 @@ private fun ProxyEditDialog(
Dialog(
onDismissRequest = (onDismiss),
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 (host, setHost) = state.proxyHost
val (port, setPort) = state.proxyPort
@ -192,18 +284,6 @@ private fun ProxyEditDialog(
val (username, setUsername) = state.proxyUsername
val (password, setPassword) = state.proxyPassword
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(
modifier = Modifier,
title = {
@ -226,7 +306,7 @@ private fun ProxyEditDialog(
)
}
)
spacer()
ProxyConfigSpacer()
DialogConfigItem(
modifier = Modifier,
title = {
@ -256,7 +336,7 @@ private fun ProxyEditDialog(
}
}
)
spacer()
ProxyConfigSpacer()
DialogConfigItem(
modifier = Modifier,
title = {
@ -296,7 +376,7 @@ private fun ProxyEditDialog(
}
}
)
spacer()
ProxyConfigSpacer()
DialogConfigItem(
modifier = Modifier,
title = {
@ -322,23 +402,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
private fun SettingsDialog(
@ -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))
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,
@ -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)
},
)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
package ir.amirab.downloader.connection.proxy
import java.net.ProxySelector
interface SystemProxySelectorProvider {
fun getSystemProxySelector(): ProxySelector?
}

View File

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

View File

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

View File

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

View File

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

View File

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