feat(tor): desktop Tor settings UI — indicator, toggle, dialog
Phase 5: Desktop settings UI for Tor. TorStatusIndicator: - Shield icon in sidebar footer (gray/yellow/green/red) - Tooltip shows status (no port number for security) - Only visible when Tor is not Off TorSettingsSection: - Inline in settings screen between Media Server and Dev Settings - Mode selector (Off/Internal/External) with segmented buttons - External SOCKS port input with validation (1-65535) - "Advanced..." button opens full dialog TorSettingsDialog: - Full DialogWindow with preset selector (radio buttons) - 4 relay routing toggles + 7 content routing toggles - Preset auto-detection (whichPreset) - Save/Cancel with DesktopTorPreferences persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -573,6 +573,7 @@ fun App(
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
appScope = scope,
|
||||
torStatus = torManager.status.collectAsState().value,
|
||||
onShowComposeDialog = onShowComposeDialog,
|
||||
onShowReplyDialog = onShowReplyDialog,
|
||||
onShowAddColumnDialog = onShowAddColumnDialog,
|
||||
@@ -624,6 +625,7 @@ fun MainContent(
|
||||
nwcConnection: Nip47WalletConnect.Nip47URINorm?,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator,
|
||||
appScope: CoroutineScope,
|
||||
torStatus: com.vitorpamplona.amethyst.commons.tor.TorServiceStatus,
|
||||
onShowComposeDialog: () -> Unit,
|
||||
onShowReplyDialog: (com.vitorpamplona.quartz.nip01Core.core.Event) -> Unit,
|
||||
onShowAddColumnDialog: () -> Unit,
|
||||
@@ -796,6 +798,7 @@ fun MainContent(
|
||||
},
|
||||
signerConnectionState = signerConnectionState,
|
||||
lastPingTimeSec = lastPingTimeSec,
|
||||
torStatus = torStatus,
|
||||
)
|
||||
|
||||
VerticalDivider()
|
||||
@@ -879,6 +882,11 @@ fun RelaySettingsScreen(
|
||||
relayManager: DesktopRelayConnectionManager,
|
||||
account: AccountState.LoggedIn,
|
||||
accountManager: AccountManager,
|
||||
torStatus: com.vitorpamplona.amethyst.commons.tor.TorServiceStatus = com.vitorpamplona.amethyst.commons.tor.TorServiceStatus.Off,
|
||||
torSettings: com.vitorpamplona.amethyst.commons.tor.TorSettings =
|
||||
com.vitorpamplona.amethyst.commons.tor
|
||||
.TorSettings(torType = com.vitorpamplona.amethyst.commons.tor.TorType.OFF),
|
||||
onTorSettingsChanged: (com.vitorpamplona.amethyst.commons.tor.TorSettings) -> Unit = {},
|
||||
) {
|
||||
val relayStatuses by relayManager.relayStatuses.collectAsState()
|
||||
val connectedRelays by relayManager.connectedRelays.collectAsState()
|
||||
@@ -994,6 +1002,16 @@ fun RelaySettingsScreen(
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
// Tor Settings
|
||||
com.vitorpamplona.amethyst.desktop.ui.tor.TorSettingsSection(
|
||||
torStatus = torStatus,
|
||||
currentSettings = torSettings,
|
||||
onSettingsChanged = onTorSettingsChanged,
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
// Developer Settings Section (only in debug mode)
|
||||
if (DebugConfig.isDebugMode) {
|
||||
com.vitorpamplona.amethyst.desktop.ui
|
||||
|
||||
+8
@@ -42,7 +42,9 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.commons.domain.nip46.SignerConnectionState
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorServiceStatus
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.BunkerHeartbeatIndicator
|
||||
import com.vitorpamplona.amethyst.desktop.ui.tor.TorStatusIndicator
|
||||
|
||||
@Composable
|
||||
fun DeckSidebar(
|
||||
@@ -50,6 +52,7 @@ fun DeckSidebar(
|
||||
onOpenSettings: () -> Unit,
|
||||
signerConnectionState: SignerConnectionState,
|
||||
lastPingTimeSec: Long?,
|
||||
torStatus: TorServiceStatus,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
@@ -87,6 +90,11 @@ fun DeckSidebar(
|
||||
lastPingTimeSec = lastPingTimeSec,
|
||||
)
|
||||
|
||||
if (torStatus !is TorServiceStatus.Off) {
|
||||
Spacer(Modifier.size(4.dp))
|
||||
TorStatusIndicator(status = torStatus)
|
||||
}
|
||||
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
IconButton(onClick = onOpenSettings) {
|
||||
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.ui.tor
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogWindow
|
||||
import androidx.compose.ui.window.rememberDialogState
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorPresetType
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorServiceStatus
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorType
|
||||
import com.vitorpamplona.amethyst.commons.tor.torDefaultPreset
|
||||
import com.vitorpamplona.amethyst.commons.tor.torFullyPrivate
|
||||
import com.vitorpamplona.amethyst.commons.tor.torOnlyWhenNeededPreset
|
||||
import com.vitorpamplona.amethyst.commons.tor.torSmallPayloadsPreset
|
||||
import com.vitorpamplona.amethyst.commons.tor.whichPreset
|
||||
|
||||
@Composable
|
||||
fun TorSettingsDialog(
|
||||
currentSettings: TorSettings,
|
||||
torStatus: TorServiceStatus,
|
||||
onSettingsChanged: (TorSettings) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var editSettings by remember { mutableStateOf(currentSettings) }
|
||||
|
||||
DialogWindow(
|
||||
onCloseRequest = onDismiss,
|
||||
title = "Tor Settings",
|
||||
state = rememberDialogState(size = DpSize(480.dp, 640.dp)),
|
||||
) {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
// Status
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
TorStatusIndicator(status = torStatus)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
when (torStatus) {
|
||||
is TorServiceStatus.Off -> "Tor is off"
|
||||
is TorServiceStatus.Connecting -> "Connecting to Tor..."
|
||||
is TorServiceStatus.Active -> "Connected via Tor"
|
||||
is TorServiceStatus.Error -> "Error: ${torStatus.message}"
|
||||
},
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Mode selector
|
||||
Text("Mode", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
TorType.entries.forEachIndexed { index, torType ->
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(index, TorType.entries.size),
|
||||
onClick = { editSettings = editSettings.copy(torType = torType) },
|
||||
selected = editSettings.torType == torType,
|
||||
) {
|
||||
Text(torType.name.lowercase().replaceFirstChar { it.uppercase() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (editSettings.torType == TorType.EXTERNAL) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = editSettings.externalSocksPort.toString(),
|
||||
onValueChange = { text ->
|
||||
text.toIntOrNull()?.let { port ->
|
||||
if (port in 1..65535) {
|
||||
editSettings = editSettings.copy(externalSocksPort = port)
|
||||
}
|
||||
}
|
||||
},
|
||||
label = { Text("SOCKS Port") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.width(150.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Presets
|
||||
Text("Preset", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
val currentPreset = whichPreset(editSettings)
|
||||
PresetRow("Only When Needed", TorPresetType.ONLY_WHEN_NEEDED, currentPreset) {
|
||||
editSettings = torOnlyWhenNeededPreset.copy(torType = editSettings.torType, externalSocksPort = editSettings.externalSocksPort)
|
||||
}
|
||||
PresetRow("Default", TorPresetType.DEFAULT, currentPreset) {
|
||||
editSettings = torDefaultPreset.copy(torType = editSettings.torType, externalSocksPort = editSettings.externalSocksPort)
|
||||
}
|
||||
PresetRow("Small Payloads", TorPresetType.SMALL_PAYLOADS, currentPreset) {
|
||||
editSettings = torSmallPayloadsPreset.copy(torType = editSettings.torType, externalSocksPort = editSettings.externalSocksPort)
|
||||
}
|
||||
PresetRow("Full Privacy", TorPresetType.FULL_PRIVACY, currentPreset) {
|
||||
editSettings = torFullyPrivate.copy(torType = editSettings.torType, externalSocksPort = editSettings.externalSocksPort)
|
||||
}
|
||||
PresetRow("Custom", TorPresetType.CUSTOM, currentPreset) {}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Relay Routing
|
||||
Text("Relay Routing", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ToggleRow(".onion relays via Tor", editSettings.onionRelaysViaTor) { editSettings = editSettings.copy(onionRelaysViaTor = it) }
|
||||
ToggleRow("DM relays via Tor", editSettings.dmRelaysViaTor) { editSettings = editSettings.copy(dmRelaysViaTor = it) }
|
||||
ToggleRow("Trusted relays via Tor", editSettings.trustedRelaysViaTor) { editSettings = editSettings.copy(trustedRelaysViaTor = it) }
|
||||
ToggleRow("New/unknown relays via Tor", editSettings.newRelaysViaTor) { editSettings = editSettings.copy(newRelaysViaTor = it) }
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Content Routing
|
||||
Text("Content Routing", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ToggleRow("URL previews via Tor", editSettings.urlPreviewsViaTor) { editSettings = editSettings.copy(urlPreviewsViaTor = it) }
|
||||
ToggleRow("Profile pictures via Tor", editSettings.profilePicsViaTor) { editSettings = editSettings.copy(profilePicsViaTor = it) }
|
||||
ToggleRow("Images via Tor", editSettings.imagesViaTor) { editSettings = editSettings.copy(imagesViaTor = it) }
|
||||
ToggleRow("Videos via Tor", editSettings.videosViaTor) { editSettings = editSettings.copy(videosViaTor = it) }
|
||||
ToggleRow("NIP-05 verifications via Tor", editSettings.nip05VerificationsViaTor) { editSettings = editSettings.copy(nip05VerificationsViaTor = it) }
|
||||
ToggleRow("Money operations via Tor", editSettings.moneyOperationsViaTor) { editSettings = editSettings.copy(moneyOperationsViaTor = it) }
|
||||
ToggleRow("Media uploads via Tor", editSettings.mediaUploadsViaTor) { editSettings = editSettings.copy(mediaUploadsViaTor = it) }
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
// Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||
Spacer(Modifier.width(8.dp))
|
||||
TextButton(onClick = {
|
||||
onSettingsChanged(editSettings)
|
||||
onDismiss()
|
||||
}) { Text("Save") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PresetRow(
|
||||
label: String,
|
||||
presetType: TorPresetType,
|
||||
currentPreset: TorPresetType,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
RadioButton(
|
||||
selected = currentPreset == presetType,
|
||||
onClick = onClick,
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(label, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleRow(
|
||||
label: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
) {
|
||||
Text(label, style = MaterialTheme.typography.bodyMedium)
|
||||
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.ui.tor
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorServiceStatus
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorType
|
||||
|
||||
/**
|
||||
* Inline Tor settings section for the desktop settings screen.
|
||||
* Shows mode selector (Off/Internal/External) with status indicator
|
||||
* and an "Advanced..." button to open the full dialog.
|
||||
*/
|
||||
@Composable
|
||||
fun TorSettingsSection(
|
||||
torStatus: TorServiceStatus,
|
||||
currentSettings: TorSettings,
|
||||
onSettingsChanged: (TorSettings) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
"Tor",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
TorStatusIndicator(status = torStatus)
|
||||
}
|
||||
TextButton(onClick = { showDialog = true }) {
|
||||
Text("Advanced...")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
"Route relay connections through Tor for privacy. Internal mode bundles a Tor daemon; External mode connects to your own SOCKS proxy.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
// Mode selector
|
||||
SingleChoiceSegmentedButtonRow {
|
||||
TorType.entries.forEachIndexed { index, torType ->
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(index = index, count = TorType.entries.size),
|
||||
onClick = { onSettingsChanged(currentSettings.copy(torType = torType)) },
|
||||
selected = currentSettings.torType == torType,
|
||||
) {
|
||||
Text(torType.name.lowercase().replaceFirstChar { it.uppercase() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// External port input
|
||||
if (currentSettings.torType == TorType.EXTERNAL) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
"SOCKS Port:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
value = currentSettings.externalSocksPort.toString(),
|
||||
onValueChange = { text ->
|
||||
text.toIntOrNull()?.let { port ->
|
||||
if (port in 1..65535) {
|
||||
onSettingsChanged(currentSettings.copy(externalSocksPort = port))
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.width(100.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
TorSettingsDialog(
|
||||
currentSettings = currentSettings,
|
||||
torStatus = torStatus,
|
||||
onSettingsChanged = onSettingsChanged,
|
||||
onDismiss = { showDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.ui.tor
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.TooltipArea
|
||||
import androidx.compose.foundation.TooltipPlacement
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Shield
|
||||
import androidx.compose.material.icons.outlined.Shield
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorServiceStatus
|
||||
|
||||
/**
|
||||
* Small shield icon showing Tor connection status in the sidebar footer.
|
||||
* Tooltip shows status text (no port number for security).
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TorStatusIndicator(
|
||||
status: TorServiceStatus,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (icon, tint, tooltip) =
|
||||
when (status) {
|
||||
is TorServiceStatus.Off -> {
|
||||
Triple(Icons.Outlined.Shield, Color.Gray, "Tor: Off")
|
||||
}
|
||||
|
||||
is TorServiceStatus.Connecting -> {
|
||||
Triple(Icons.Filled.Shield, Color(0xFFFFB300), "Tor: Connecting...")
|
||||
}
|
||||
|
||||
is TorServiceStatus.Active -> {
|
||||
Triple(Icons.Filled.Shield, Color(0xFF4CAF50), "Tor: Connected")
|
||||
}
|
||||
|
||||
is TorServiceStatus.Error -> {
|
||||
Triple(Icons.Outlined.Shield, Color(0xFFF44336), "Tor: ${status.message}")
|
||||
}
|
||||
}
|
||||
|
||||
TooltipArea(
|
||||
tooltip = {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = MaterialTheme.colorScheme.inverseSurface,
|
||||
) {
|
||||
Text(
|
||||
text = tooltip,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.inverseOnSurface,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
},
|
||||
tooltipPlacement = TooltipPlacement.CursorPoint(alignment = Alignment.BottomEnd, offset = DpOffset(0.dp, 16.dp)),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = tooltip,
|
||||
tint = tint,
|
||||
modifier = modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user