android: SOCKS proxy settings (#2158)
* android: draft UI for SOCKS proxy settings * footer comment * UI for proxy host-port selection * keyboard type in port text field * footer * better text validation logic * use italic in footer --------- Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
747cbb8e09
commit
04c90b4f07
@@ -108,6 +108,7 @@ class AppPreferences(val context: Context) {
|
||||
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
|
||||
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
|
||||
val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
|
||||
val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050")
|
||||
private val _networkSessionMode = mkStrPreference(SHARED_PREFS_NETWORK_SESSION_MODE, TransportSessionMode.default.name)
|
||||
val networkSessionMode: SharedPreference<TransportSessionMode> = SharedPreference(
|
||||
get = fun(): TransportSessionMode {
|
||||
@@ -224,6 +225,7 @@ class AppPreferences(val context: Context) {
|
||||
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
|
||||
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
|
||||
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
|
||||
private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort"
|
||||
private const val SHARED_PREFS_NETWORK_SESSION_MODE = "NetworkSessionMode"
|
||||
private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode"
|
||||
private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode"
|
||||
@@ -1765,7 +1767,16 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
|
||||
fun getNetCfg(): NetCfg {
|
||||
val useSocksProxy = appPrefs.networkUseSocksProxy.get()
|
||||
val socksProxy = if (useSocksProxy) ":9050" else null
|
||||
val proxyHostPort = appPrefs.networkProxyHostPort.get()
|
||||
val socksProxy = if (useSocksProxy) {
|
||||
if (proxyHostPort?.startsWith("localhost:") == true) {
|
||||
proxyHostPort.removePrefix("localhost")
|
||||
} else {
|
||||
proxyHostPort ?: ":9050"
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val hostMode = HostMode.valueOf(appPrefs.networkHostMode.get()!!)
|
||||
val requiredHostMode = appPrefs.networkRequiredHostMode.get()
|
||||
val sessionMode = appPrefs.networkSessionMode.get()
|
||||
|
||||
@@ -366,7 +366,7 @@ fun PassphraseField(
|
||||
showStrength: Boolean = false,
|
||||
isValid: (String) -> Boolean,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
dependsOn: MutableState<String>? = null,
|
||||
dependsOn: State<Any?>? = null,
|
||||
) {
|
||||
var valid by remember { mutableStateOf(validKey(key.value)) }
|
||||
var showKey by remember { mutableStateOf(false) }
|
||||
@@ -479,7 +479,7 @@ private fun passphraseEntropy(s: String): Double {
|
||||
return s.length * log2(poolSize.toDouble())
|
||||
}
|
||||
|
||||
private enum class PassphraseStrength(val color: Color) {
|
||||
enum class PassphraseStrength(val color: Color) {
|
||||
VERY_WEAK(Color.Red), WEAK(WarningOrange), REASONABLE(WarningYellow), STRONG(SimplexGreen);
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -3,10 +3,15 @@ package chat.simplex.app.views.helpers
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||
import androidx.compose.foundation.text.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.TextFieldDefaults.indicatorLine
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.outlined.Error
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -14,13 +19,18 @@ import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.views.database.PassphraseStrength
|
||||
import chat.simplex.app.views.database.validKey
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
@@ -110,3 +120,109 @@ fun DefaultBasicTextField(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun DefaultConfigurableTextField(
|
||||
state: MutableState<TextFieldValue>,
|
||||
placeholder: String,
|
||||
modifier: Modifier = Modifier,
|
||||
showPasswordStrength: Boolean = false,
|
||||
isValid: (String) -> Boolean,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
keyboardType: KeyboardType = KeyboardType.Text,
|
||||
dependsOn: State<Any?>? = null,
|
||||
) {
|
||||
var valid by remember { mutableStateOf(validKey(state.value.text)) }
|
||||
var showKey by remember { mutableStateOf(false) }
|
||||
val icon = if (valid) {
|
||||
if (showKey) Icons.Filled.VisibilityOff else Icons.Filled.Visibility
|
||||
} else Icons.Outlined.Error
|
||||
val iconColor = if (valid) {
|
||||
if (showPasswordStrength && state.value.text.isNotEmpty()) PassphraseStrength.check(state.value.text).color else HighOrLowlight
|
||||
} else Color.Red
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
val keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (keyboardActions.onNext != null) ImeAction.Next else ImeAction.Done,
|
||||
autoCorrect = keyboardType != KeyboardType.Password,
|
||||
keyboardType = keyboardType
|
||||
)
|
||||
val enabled = true
|
||||
val colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = Color.Unspecified,
|
||||
textColor = MaterialTheme.colors.onBackground,
|
||||
focusedIndicatorColor = Color.Unspecified,
|
||||
unfocusedIndicatorColor = Color.Unspecified,
|
||||
)
|
||||
val color = MaterialTheme.colors.onBackground
|
||||
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
BasicTextField(
|
||||
value = state.value,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(colors.backgroundColor(enabled).value, shape)
|
||||
.indicatorLine(enabled, false, interactionSource, colors)
|
||||
.defaultMinSize(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
minHeight = TextFieldDefaults.MinHeight
|
||||
),
|
||||
onValueChange = {
|
||||
state.value = it
|
||||
},
|
||||
cursorBrush = SolidColor(colors.cursorColor(false).value),
|
||||
visualTransformation = if (showKey || keyboardType != KeyboardType.Password)
|
||||
VisualTransformation.None
|
||||
else
|
||||
VisualTransformation { TransformedText(AnnotatedString(it.text.map { "*" }.joinToString(separator = "")), OffsetMapping.Identity) },
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
keyboard?.hide()
|
||||
keyboardActions.onDone?.invoke(this)
|
||||
}),
|
||||
singleLine = true,
|
||||
textStyle = TextStyle.Default.copy(
|
||||
color = color,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
TextFieldDefaults.TextFieldDecorationBox(
|
||||
value = state.value.text,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = { Text(placeholder, color = HighOrLowlight) },
|
||||
singleLine = true,
|
||||
enabled = enabled,
|
||||
isError = !valid,
|
||||
trailingIcon = {
|
||||
if (keyboardType == KeyboardType.Password || !valid) {
|
||||
IconButton({ showKey = !showKey }) {
|
||||
Icon(icon, null, tint = iconColor)
|
||||
}
|
||||
}
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
contentPadding = TextFieldDefaults.textFieldWithLabelPadding(start = 0.dp, end = 0.dp),
|
||||
visualTransformation = VisualTransformation.None,
|
||||
colors = colors
|
||||
)
|
||||
}
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
snapshotFlow { state.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
valid = isValid(it.text)
|
||||
}
|
||||
}
|
||||
launch {
|
||||
snapshotFlow { dependsOn?.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
valid = isValid(state.value.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,15 +102,6 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
saveCfg(newCfg)
|
||||
}
|
||||
|
||||
fun updateSettingsDialog(action: () -> Unit) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.update_network_settings_question),
|
||||
text = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
|
||||
confirmText = generalGetString(R.string.update_network_settings_confirmation),
|
||||
onConfirm = action
|
||||
)
|
||||
}
|
||||
|
||||
AdvancedNetworkSettingsLayout(
|
||||
networkTCPConnectTimeout,
|
||||
networkTCPTimeout,
|
||||
@@ -121,10 +112,10 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
networkTCPKeepIntvl,
|
||||
networkTCPKeepCnt,
|
||||
resetDisabled = if (currentCfg.value.useSocksProxy) currentCfg.value == NetCfg.proxyDefaults else currentCfg.value == NetCfg.defaults,
|
||||
reset = { updateSettingsDialog(::reset) },
|
||||
reset = { showUpdateNetworkSettingsDialog(::reset) },
|
||||
footerDisabled = buildCfg() == currentCfg.value,
|
||||
revert = { updateView(currentCfg.value) },
|
||||
save = { updateSettingsDialog { saveCfg(buildCfg()) } }
|
||||
save = { showUpdateNetworkSettingsDialog { saveCfg(buildCfg()) } }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -415,6 +406,15 @@ fun FooterButton(icon: ImageVector, title: String, action: () -> Unit, disabled:
|
||||
}
|
||||
}
|
||||
|
||||
fun showUpdateNetworkSettingsDialog(action: () -> Unit) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.update_network_settings_question),
|
||||
text = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
|
||||
confirmText = generalGetString(R.string.update_network_settings_confirmation),
|
||||
onConfirm = action
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PreviewAdvancedNetworkSettingsLayout() {
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionCustomFooter
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionItemWithValue
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import SectionViewSelectable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
@@ -44,6 +52,7 @@ fun NetworkAndServersView(
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
onionHosts = onionHosts,
|
||||
sessionMode = sessionMode,
|
||||
proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } },
|
||||
showModal = showModal,
|
||||
showSettingsModal = showSettingsModal,
|
||||
showCustomModal = showCustomModal,
|
||||
@@ -87,7 +96,7 @@ fun NetworkAndServersView(
|
||||
OnionHosts.PREFER -> generalGetString(R.string.network_use_onion_hosts_prefer_desc_in_alert)
|
||||
OnionHosts.REQUIRED -> generalGetString(R.string.network_use_onion_hosts_required_desc_in_alert)
|
||||
}
|
||||
updateNetworkSettingsDialog(
|
||||
showUpdateNetworkSettingsDialog(
|
||||
title = generalGetString(R.string.update_onion_hosts_settings_question),
|
||||
startsWith,
|
||||
onDismiss = {
|
||||
@@ -114,7 +123,7 @@ fun NetworkAndServersView(
|
||||
TransportSessionMode.User -> generalGetString(R.string.network_session_mode_user_description)
|
||||
TransportSessionMode.Entity -> generalGetString(R.string.network_session_mode_entity_description)
|
||||
}
|
||||
updateNetworkSettingsDialog(
|
||||
showUpdateNetworkSettingsDialog(
|
||||
title = generalGetString(R.string.update_network_session_mode_question),
|
||||
startsWith,
|
||||
onDismiss = { sessionMode.value = prevValue }
|
||||
@@ -140,6 +149,7 @@ fun NetworkAndServersView(
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
proxyPort: State<Int>,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
@@ -163,7 +173,7 @@ fun NetworkAndServersView(
|
||||
}
|
||||
|
||||
SectionItemView {
|
||||
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
|
||||
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal)
|
||||
}
|
||||
SectionDivider()
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
|
||||
@@ -174,7 +184,10 @@ fun NetworkAndServersView(
|
||||
}
|
||||
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
if (networkUseSocksProxy.value) {
|
||||
SectionCustomFooter { Text(annotatedStringResource(R.string.disable_onion_hosts_when_not_supported)) }
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
SectionView(generalGetString(R.string.settings_section_title_calls)) {
|
||||
SettingsActionItem(Icons.Outlined.ElectricalServices, stringResource(R.string.webrtc_ice_servers), showModal { RTCServersView(it) })
|
||||
}
|
||||
@@ -184,7 +197,9 @@ fun NetworkAndServersView(
|
||||
@Composable
|
||||
fun UseSocksProxySwitch(
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
toggleSocksProxy: (Boolean) -> Unit
|
||||
proxyPort: State<Int>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -201,7 +216,19 @@ fun UseSocksProxySwitch(
|
||||
stringResource(R.string.network_socks_toggle),
|
||||
tint = HighOrLowlight
|
||||
)
|
||||
Text(stringResource(R.string.network_socks_toggle))
|
||||
if (networkUseSocksProxy.value) {
|
||||
Row {
|
||||
Text(generalGetString(R.string.network_socks_toggle_use_socks_proxy) + " (")
|
||||
Text(
|
||||
generalGetString(R.string.network_proxy_port).format(proxyPort.value),
|
||||
Modifier.clickable { showSettingsModal { SockProxySettings(it) }() },
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
Text(")")
|
||||
}
|
||||
} else {
|
||||
Text(stringResource(R.string.network_socks_toggle))
|
||||
}
|
||||
}
|
||||
Switch(
|
||||
checked = networkUseSocksProxy.value,
|
||||
@@ -214,6 +241,83 @@ fun UseSocksProxySwitch(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SockProxySettings(m: ChatModel) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = DEFAULT_BOTTOM_PADDING),
|
||||
) {
|
||||
val defaultHostPort = remember { "localhost:9050" }
|
||||
AppBarTitle(generalGetString(R.string.network_socks_proxy_settings))
|
||||
val hostPort by remember { m.controller.appPrefs.networkProxyHostPort.state }
|
||||
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(hostPort?.split(":")?.firstOrNull() ?: "localhost"))
|
||||
}
|
||||
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(hostPort?.split(":")?.lastOrNull() ?: "9050"))
|
||||
}
|
||||
val save = {
|
||||
withBGApi {
|
||||
m.controller.appPrefs.networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
|
||||
m.controller.apiSetNetworkConfig(m.controller.getNetCfg())
|
||||
}
|
||||
}
|
||||
SectionView {
|
||||
SectionItemView {
|
||||
ResetToDefaultsButton({
|
||||
showUpdateNetworkSettingsDialog {
|
||||
m.controller.appPrefs.networkProxyHostPort.set(defaultHostPort)
|
||||
val newHost = defaultHostPort.split(":").first()
|
||||
val newPort = defaultHostPort.split(":").last()
|
||||
hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length))
|
||||
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
|
||||
save()
|
||||
}
|
||||
}, disabled = hostPort == defaultHostPort)
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
DefaultConfigurableTextField(
|
||||
hostUnsaved,
|
||||
stringResource(R.string.host_verb),
|
||||
modifier = Modifier,
|
||||
isValid = ::validHost,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
DefaultConfigurableTextField(
|
||||
portUnsaved,
|
||||
stringResource(R.string.port_verb),
|
||||
modifier = Modifier,
|
||||
isValid = ::validPort,
|
||||
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
|
||||
keyboardType = KeyboardType.Number,
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionCustomFooter {
|
||||
NetworkSectionFooter(
|
||||
revert = {
|
||||
val prevHost = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.firstOrNull() ?: "localhost"
|
||||
val prevPort = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.lastOrNull() ?: "9050"
|
||||
hostUnsaved.value = hostUnsaved.value.copy(prevHost, TextRange(prevHost.length))
|
||||
portUnsaved.value = portUnsaved.value.copy(prevPort, TextRange(prevPort.length))
|
||||
},
|
||||
save = { showUpdateNetworkSettingsDialog { save() } },
|
||||
revertDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text),
|
||||
saveDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
|
||||
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
|
||||
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UseOnionHosts(
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
@@ -282,7 +386,32 @@ private fun SessionModePicker(
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateNetworkSettingsDialog(
|
||||
@Composable
|
||||
private fun NetworkSectionFooter(revert: () -> Unit, save: () -> Unit, revertDisabled: Boolean, saveDisabled: Boolean) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
FooterButton(Icons.Outlined.Replay, stringResource(R.string.network_options_revert), revert, revertDisabled)
|
||||
FooterButton(Icons.Outlined.Check, stringResource(R.string.network_options_save), save, saveDisabled)
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/106223
|
||||
private fun validHost(s: String): Boolean {
|
||||
val validIp = Regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
|
||||
val validHostname = Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])[.])*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$");
|
||||
return s.matches(validIp) || s.matches(validHostname)
|
||||
}
|
||||
|
||||
// https://ihateregex.io/expr/port/
|
||||
private fun validPort(s: String): Boolean {
|
||||
val validPort = Regex("^(6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4})$")
|
||||
return s.isNotBlank() && s.matches(validPort)
|
||||
}
|
||||
|
||||
private fun showUpdateNetworkSettingsDialog(
|
||||
title: String,
|
||||
startsWith: String = "",
|
||||
message: String = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
|
||||
@@ -307,6 +436,7 @@ fun PreviewNetworkAndServersLayout() {
|
||||
developerTools = true,
|
||||
xftpSendEnabled = remember { mutableStateOf(true) },
|
||||
networkUseSocksProxy = remember { mutableStateOf(true) },
|
||||
proxyPort = remember { mutableStateOf(9050) },
|
||||
showModal = { {} },
|
||||
showSettingsModal = { {} },
|
||||
showCustomModal = { {} },
|
||||
|
||||
@@ -508,6 +508,11 @@
|
||||
<string name="network_settings">Advanced network settings</string>
|
||||
<string name="network_settings_title">Network settings</string>
|
||||
<string name="network_socks_toggle">Use SOCKS proxy (port 9050)</string>
|
||||
<string name="network_socks_proxy_settings">SOCKS proxy settings</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">Use SOCKS proxy</string>
|
||||
<string name="network_proxy_port">port %d</string>
|
||||
<string name="host_verb">Host</string>
|
||||
<string name="port_verb">Port</string>
|
||||
<string name="network_enable_socks">Use SOCKS proxy?</string>
|
||||
<string name="network_enable_socks_info">Access the servers via SOCKS proxy on port 9050? Proxy must be started before enabling this option.</string>
|
||||
<string name="network_disable_socks">Use direct Internet connection?</string>
|
||||
@@ -529,6 +534,7 @@
|
||||
<string name="network_session_mode_user_description">A separate TCP connection (and SOCKS credential) will be used <b>for each chat profile you have in the app</b>.</string>
|
||||
<string name="network_session_mode_entity_description">A separate TCP connection (and SOCKS credential) will be used <b>for each contact and group member</b>.\n<b>Please note</b>: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</string>
|
||||
<string name="update_network_session_mode_question">Update transport isolation mode?</string>
|
||||
<string name="disable_onion_hosts_when_not_supported">Set <i>Use .onion hosts</i> to No if SOCKS proxy does not support them.</string>
|
||||
<string name="appearance_settings">Appearance</string>
|
||||
<string name="app_version_title">App version</string>
|
||||
<string name="app_version_name">App version: v%s</string>
|
||||
|
||||
Reference in New Issue
Block a user