android: mockup line icons everywhere, mockup dialog buttons, server picker sheet as on F_Servers; drop the unused old Home and routing parts
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
16d4c72920
commit
9ed278b440
19 files changed
+208
-1237
No files matched your search
+3
-5
@@ -40,9 +40,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.CheckCircle
|
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -61,6 +58,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import org.strongswan.android.R
|
import org.strongswan.android.R
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxIcons
|
||||||
import org.strongswan.android.ui.compose.components.PredictiveBackScale
|
import org.strongswan.android.ui.compose.components.PredictiveBackScale
|
||||||
import org.strongswan.android.logic.imc.RemediationInstruction
|
import org.strongswan.android.logic.imc.RemediationInstruction
|
||||||
import org.strongswan.android.ui.compose.components.ShxCard
|
import org.strongswan.android.ui.compose.components.ShxCard
|
||||||
@@ -143,7 +141,7 @@ private fun RemediationUi(
|
|||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { if (selected != null) onSelect(null) else onFinish() }) {
|
IconButton(onClick = { if (selected != null) onSelect(null) else onFinish() }) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Rounded.ArrowBack,
|
ShxIcons.Back,
|
||||||
contentDescription = stringResource(R.string.back),
|
contentDescription = stringResource(R.string.back),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -167,7 +165,7 @@ private fun RemediationUi(
|
|||||||
if (instructions.isEmpty())
|
if (instructions.isEmpty())
|
||||||
{
|
{
|
||||||
ShxEmptyState(
|
ShxEmptyState(
|
||||||
icon = Icons.Rounded.CheckCircle,
|
icon = ShxIcons.Check,
|
||||||
message = stringResource(R.string.remediation_instructions_title),
|
message = stringResource(R.string.remediation_instructions_title),
|
||||||
modifier = Modifier.padding(ShxSpacing.lg),
|
modifier = Modifier.padding(ShxSpacing.lg),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -80,6 +79,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.strongswan.android.R
|
import org.strongswan.android.R
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxDialogButton
|
||||||
import org.strongswan.android.data.VpnProfile
|
import org.strongswan.android.data.VpnProfile
|
||||||
import org.strongswan.android.data.VpnProfileDataSource
|
import org.strongswan.android.data.VpnProfileDataSource
|
||||||
import org.strongswan.android.data.VpnProfileSource
|
import org.strongswan.android.data.VpnProfileSource
|
||||||
@@ -724,8 +724,8 @@ private fun TextDialog(
|
|||||||
text = {
|
text = {
|
||||||
ShxFormField(label = label, value = text, onValueChange = { text = it }, keyboardOptions = KeyboardOptions(keyboardType = keyboard))
|
ShxFormField(label = label, value = text, onValueChange = { text = it }, keyboardOptions = KeyboardOptions(keyboardType = keyboard))
|
||||||
},
|
},
|
||||||
confirmButton = { TextButton(onClick = { onDone(text) }, enabled = validate(text.trim())) { Text(stringResource(R.string.server_save)) } },
|
confirmButton = { ShxDialogButton(primary = true, onClick = { onDone(text) }, enabled = validate(text.trim())) { Text(stringResource(R.string.server_save)) } },
|
||||||
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(android.R.string.cancel)) } },
|
dismissButton = { ShxDialogButton(onClick = onDismiss) { Text(stringResource(android.R.string.cancel)) } },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -27,7 +27,6 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import org.strongswan.android.ui.compose.design.ShxAlertDialog
|
import org.strongswan.android.ui.compose.design.ShxAlertDialog
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -38,6 +37,7 @@ import java.security.cert.CertificateException
|
|||||||
import java.security.cert.CertificateFactory
|
import java.security.cert.CertificateFactory
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import org.strongswan.android.R
|
import org.strongswan.android.R
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxDialogButton
|
||||||
import org.strongswan.android.logic.TrustedCertificateManager
|
import org.strongswan.android.logic.TrustedCertificateManager
|
||||||
import org.strongswan.android.ui.compose.theme.ShxTheme
|
import org.strongswan.android.ui.compose.theme.ShxTheme
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ class TrustedCertificateImportActivity : AppCompatActivity()
|
|||||||
title = { Text(stringResource(R.string.import_certificate)) },
|
title = { Text(stringResource(R.string.import_certificate)) },
|
||||||
text = { Text(certificate.getSubjectDN().toString()) },
|
text = { Text(certificate.getSubjectDN().toString()) },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
ShxDialogButton(primary = true, onClick = {
|
||||||
if (storeCertificate(certificate))
|
if (storeCertificate(certificate))
|
||||||
{
|
{
|
||||||
Toast.makeText(this, R.string.cert_imported_successfully, Toast.LENGTH_LONG).show()
|
Toast.makeText(this, R.string.cert_imported_successfully, Toast.LENGTH_LONG).show()
|
||||||
@@ -194,7 +194,7 @@ class TrustedCertificateImportActivity : AppCompatActivity()
|
|||||||
}) { Text(stringResource(R.string.import_certificate)) }
|
}) { Text(stringResource(R.string.import_certificate)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { finish() }) {
|
ShxDialogButton(onClick = { finish() }) {
|
||||||
Text(stringResource(android.R.string.cancel))
|
Text(stringResource(android.R.string.cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+14
-22
@@ -22,15 +22,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Add
|
|
||||||
import androidx.compose.material.icons.rounded.BatterySaver
|
|
||||||
import androidx.compose.material.icons.rounded.HealthAndSafety
|
|
||||||
import androidx.compose.material.icons.rounded.NetworkCheck
|
|
||||||
import androidx.compose.material.icons.rounded.Remove
|
|
||||||
import androidx.compose.material.icons.rounded.Public
|
|
||||||
import androidx.compose.material.icons.rounded.Speed
|
|
||||||
import androidx.compose.material.icons.rounded.Timer
|
|
||||||
import org.strongswan.android.ui.compose.design.ShxAlertDialog
|
import org.strongswan.android.ui.compose.design.ShxAlertDialog
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -40,7 +31,6 @@ import androidx.compose.material3.SegmentedButton
|
|||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -52,6 +42,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import org.strongswan.android.R
|
import org.strongswan.android.R
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxDialogButton
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxIcons
|
||||||
import org.strongswan.android.ui.compose.components.ShxSection
|
import org.strongswan.android.ui.compose.components.ShxSection
|
||||||
import org.strongswan.android.ui.compose.components.ShxSectionDivider
|
import org.strongswan.android.ui.compose.components.ShxSectionDivider
|
||||||
import org.strongswan.android.ui.compose.components.ShxSettingRow
|
import org.strongswan.android.ui.compose.components.ShxSettingRow
|
||||||
@@ -92,7 +84,7 @@ fun ConnectivityCheckScreen()
|
|||||||
ShxSection(title = stringResource(R.string.settings_section_general))
|
ShxSection(title = stringResource(R.string.settings_section_general))
|
||||||
{
|
{
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.NetworkCheck,
|
icon = ShxIcons.Pulse,
|
||||||
title = stringResource(R.string.health_check_title),
|
title = stringResource(R.string.health_check_title),
|
||||||
subtitle = stringResource(R.string.health_check_summary),
|
subtitle = stringResource(R.string.health_check_summary),
|
||||||
checked = config.enabled,
|
checked = config.enabled,
|
||||||
@@ -100,7 +92,7 @@ fun ConnectivityCheckScreen()
|
|||||||
)
|
)
|
||||||
ShxSectionDivider()
|
ShxSectionDivider()
|
||||||
ShxSettingRow(
|
ShxSettingRow(
|
||||||
icon = Icons.Rounded.Timer,
|
icon = ShxIcons.Clock,
|
||||||
title = stringResource(R.string.health_check_interval),
|
title = stringResource(R.string.health_check_interval),
|
||||||
subtitle = intervalLabel(config.intervalSec, config.aggressiveMode),
|
subtitle = intervalLabel(config.intervalSec, config.aggressiveMode),
|
||||||
enabled = config.enabled && !config.aggressiveMode,
|
enabled = config.enabled && !config.aggressiveMode,
|
||||||
@@ -108,7 +100,7 @@ fun ConnectivityCheckScreen()
|
|||||||
)
|
)
|
||||||
ShxSectionDivider()
|
ShxSectionDivider()
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.Speed,
|
icon = ShxIcons.Bolt,
|
||||||
title = stringResource(R.string.health_check_aggressive_title),
|
title = stringResource(R.string.health_check_aggressive_title),
|
||||||
subtitle = stringResource(R.string.health_check_aggressive_summary),
|
subtitle = stringResource(R.string.health_check_aggressive_summary),
|
||||||
checked = config.aggressiveMode,
|
checked = config.aggressiveMode,
|
||||||
@@ -116,7 +108,7 @@ fun ConnectivityCheckScreen()
|
|||||||
)
|
)
|
||||||
ShxSectionDivider()
|
ShxSectionDivider()
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.BatterySaver,
|
icon = ShxIcons.Battery,
|
||||||
title = stringResource(R.string.health_check_adaptive_title),
|
title = stringResource(R.string.health_check_adaptive_title),
|
||||||
subtitle = stringResource(R.string.health_check_adaptive_summary),
|
subtitle = stringResource(R.string.health_check_adaptive_summary),
|
||||||
checked = config.adaptiveMode,
|
checked = config.adaptiveMode,
|
||||||
@@ -127,7 +119,7 @@ fun ConnectivityCheckScreen()
|
|||||||
ShxSection(title = stringResource(R.string.health_check_targets_title))
|
ShxSection(title = stringResource(R.string.health_check_targets_title))
|
||||||
{
|
{
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.HealthAndSafety,
|
icon = ShxIcons.Shield,
|
||||||
title = stringResource(R.string.health_check_server_title),
|
title = stringResource(R.string.health_check_server_title),
|
||||||
subtitle = stringResource(R.string.health_check_server_summary),
|
subtitle = stringResource(R.string.health_check_server_summary),
|
||||||
checked = config.serverCheckMode != ServerCheckMode.OFF,
|
checked = config.serverCheckMode != ServerCheckMode.OFF,
|
||||||
@@ -140,7 +132,7 @@ fun ConnectivityCheckScreen()
|
|||||||
listOf(ProbeTarget.GOOGLE, ProbeTarget.YOUTUBE, ProbeTarget.CLOUDFLARE).forEach { target ->
|
listOf(ProbeTarget.GOOGLE, ProbeTarget.YOUTUBE, ProbeTarget.CLOUDFLARE).forEach { target ->
|
||||||
ShxSectionDivider()
|
ShxSectionDivider()
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.Public,
|
icon = ShxIcons.Globe,
|
||||||
title = targetLabel(target),
|
title = targetLabel(target),
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
checked = config.targets.contains(target),
|
checked = config.targets.contains(target),
|
||||||
@@ -175,7 +167,7 @@ fun ConnectivityCheckScreen()
|
|||||||
}
|
}
|
||||||
ShxSectionDivider()
|
ShxSectionDivider()
|
||||||
ShxSettingRow(
|
ShxSettingRow(
|
||||||
icon = Icons.Rounded.NetworkCheck,
|
icon = ShxIcons.Pulse,
|
||||||
title = stringResource(R.string.health_check_action),
|
title = stringResource(R.string.health_check_action),
|
||||||
subtitle = actionLabel(config.serverAction),
|
subtitle = actionLabel(config.serverAction),
|
||||||
enabled = config.enabled,
|
enabled = config.enabled,
|
||||||
@@ -203,7 +195,7 @@ fun ConnectivityCheckScreen()
|
|||||||
ShxSection(title = stringResource(R.string.health_check_tunnel_dead_title))
|
ShxSection(title = stringResource(R.string.health_check_tunnel_dead_title))
|
||||||
{
|
{
|
||||||
ShxSettingRow(
|
ShxSettingRow(
|
||||||
icon = Icons.Rounded.NetworkCheck,
|
icon = ShxIcons.Pulse,
|
||||||
title = stringResource(R.string.health_check_action),
|
title = stringResource(R.string.health_check_action),
|
||||||
subtitle = actionLabel(config.tunnelDeadAction),
|
subtitle = actionLabel(config.tunnelDeadAction),
|
||||||
enabled = config.enabled,
|
enabled = config.enabled,
|
||||||
@@ -231,7 +223,7 @@ fun ConnectivityCheckScreen()
|
|||||||
ShxSection(title = stringResource(R.string.health_check_partial_title))
|
ShxSection(title = stringResource(R.string.health_check_partial_title))
|
||||||
{
|
{
|
||||||
ShxSettingRow(
|
ShxSettingRow(
|
||||||
icon = Icons.Rounded.NetworkCheck,
|
icon = ShxIcons.Pulse,
|
||||||
title = stringResource(R.string.health_check_action),
|
title = stringResource(R.string.health_check_action),
|
||||||
subtitle = actionLabel(config.partialDownAction),
|
subtitle = actionLabel(config.partialDownAction),
|
||||||
enabled = config.enabled,
|
enabled = config.enabled,
|
||||||
@@ -276,7 +268,7 @@ fun ConnectivityCheckScreen()
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = { showIntervalPicker = false }) {
|
ShxDialogButton(onClick = { showIntervalPicker = false }) {
|
||||||
Text(stringResource(android.R.string.cancel))
|
Text(stringResource(android.R.string.cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -341,7 +333,7 @@ private fun AttemptsRow(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
IconButton(onClick = onDecrement, enabled = enabled && attempts > 1) {
|
IconButton(onClick = onDecrement, enabled = enabled && attempts > 1) {
|
||||||
Icon(Icons.Rounded.Remove, contentDescription = stringResource(R.string.health_check_attempt_minus))
|
Icon(ShxIcons.Minus, contentDescription = stringResource(R.string.health_check_attempt_minus))
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = attempts.toString(),
|
text = attempts.toString(),
|
||||||
@@ -349,7 +341,7 @@ private fun AttemptsRow(
|
|||||||
modifier = Modifier.padding(horizontal = ShxSpacing.sm),
|
modifier = Modifier.padding(horizontal = ShxSpacing.sm),
|
||||||
)
|
)
|
||||||
IconButton(onClick = onIncrement, enabled = enabled && attempts < 5) {
|
IconButton(onClick = onIncrement, enabled = enabled && attempts < 5) {
|
||||||
Icon(Icons.Rounded.Add, contentDescription = stringResource(R.string.health_check_attempt_plus))
|
Icon(ShxIcons.Plus, contentDescription = stringResource(R.string.health_check_attempt_plus))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+50
-304
@@ -52,25 +52,6 @@ import androidx.compose.foundation.lazy.LazyListState
|
|||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Add
|
|
||||||
import androidx.compose.material.icons.rounded.AltRoute
|
|
||||||
import androidx.compose.material.icons.rounded.ArrowDownward
|
|
||||||
import androidx.compose.material.icons.rounded.ArrowUpward
|
|
||||||
import androidx.compose.material.icons.rounded.ChevronRight
|
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
|
||||||
import androidx.compose.material.icons.rounded.Dns
|
|
||||||
import androidx.compose.material.icons.rounded.ExpandLess
|
|
||||||
import androidx.compose.material.icons.rounded.GppMaybe
|
|
||||||
import androidx.compose.material.icons.rounded.Language
|
|
||||||
import androidx.compose.material.icons.rounded.PlayArrow
|
|
||||||
import androidx.compose.material.icons.rounded.Public
|
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
|
||||||
import androidx.compose.material.icons.rounded.Schedule
|
|
||||||
import androidx.compose.material.icons.rounded.Share
|
|
||||||
import androidx.compose.material.icons.rounded.SignalCellularAlt
|
|
||||||
import androidx.compose.material.icons.rounded.Edit
|
|
||||||
import androidx.compose.material.icons.rounded.Wifi
|
|
||||||
import org.strongswan.android.ui.compose.design.ShxAlertDialog
|
import org.strongswan.android.ui.compose.design.ShxAlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
@@ -126,6 +107,15 @@ import org.strongswan.android.ui.compose.home.certExpiry
|
|||||||
import org.strongswan.android.ui.compose.home.snoozeCertExpiry
|
import org.strongswan.android.ui.compose.home.snoozeCertExpiry
|
||||||
import org.strongswan.android.ui.compose.home.connectProgress
|
import org.strongswan.android.ui.compose.home.connectProgress
|
||||||
import org.strongswan.android.R
|
import org.strongswan.android.R
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import org.strongswan.android.ui.compose.theme.ShxType
|
||||||
|
import org.strongswan.android.ui.compose.theme.LocalShxColors
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxButtonStyle
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxButton
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxPanel
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxSheetTitle
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxSheet
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxIcons
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import org.strongswan.android.logic.routing.RuIpListManager
|
import org.strongswan.android.logic.routing.RuIpListManager
|
||||||
import org.strongswan.android.ui.DiagnosticsActivity
|
import org.strongswan.android.ui.DiagnosticsActivity
|
||||||
@@ -143,7 +133,6 @@ import org.strongswan.android.ui.compose.components.ShxCard
|
|||||||
import org.strongswan.android.ui.compose.components.ShxEmptyState
|
import org.strongswan.android.ui.compose.components.ShxEmptyState
|
||||||
import org.strongswan.android.ui.compose.theme.LocalShxStatusColors
|
import org.strongswan.android.ui.compose.theme.LocalShxStatusColors
|
||||||
import org.strongswan.android.ui.compose.theme.ScreenPadding
|
import org.strongswan.android.ui.compose.theme.ScreenPadding
|
||||||
import androidx.compose.material.icons.rounded.Check
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.semantics.LiveRegionMode
|
import androidx.compose.ui.semantics.LiveRegionMode
|
||||||
import androidx.compose.ui.semantics.heading
|
import androidx.compose.ui.semantics.heading
|
||||||
@@ -229,8 +218,6 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var pendingDelete by remember { mutableStateOf<VpnProfile?>(null) }
|
|
||||||
var sheetProfile by remember { mutableStateOf<VpnProfile?>(null) }
|
|
||||||
var pickerOpen by remember { mutableStateOf(false) }
|
var pickerOpen by remember { mutableStateOf(false) }
|
||||||
var pauseSheetOpen by remember { mutableStateOf(false) }
|
var pauseSheetOpen by remember { mutableStateOf(false) }
|
||||||
val networkKinds by viewModel.networkKinds.collectAsStateWithLifecycle()
|
val networkKinds by viewModel.networkKinds.collectAsStateWithLifecycle()
|
||||||
@@ -385,7 +372,7 @@ fun HomeScreen(
|
|||||||
},
|
},
|
||||||
onLongPick = { profile ->
|
onLongPick = { profile ->
|
||||||
pickerOpen = false
|
pickerOpen = false
|
||||||
sheetProfile = profile
|
context.startActivity(org.strongswan.android.ui.ServerActivity.intent(context, profile.getUUID().toString()))
|
||||||
},
|
},
|
||||||
onManage = {
|
onManage = {
|
||||||
pickerOpen = false
|
pickerOpen = false
|
||||||
@@ -417,69 +404,10 @@ fun HomeScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.detailsOpen)
|
|
||||||
{
|
|
||||||
ConnectionDetailsSheet(
|
|
||||||
profile = state.activeProfile ?: state.selected,
|
|
||||||
traffic = traffic,
|
|
||||||
durationSec = durationSec,
|
|
||||||
probes = probes,
|
|
||||||
exit = exit,
|
|
||||||
onDismiss = { viewModel.openDetails(false) },
|
|
||||||
onDisconnect = {
|
|
||||||
viewModel.openDetails(false)
|
|
||||||
viewModel.disconnect()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sheetProfile?.let { profile ->
|
|
||||||
ProfileActionsSheet(
|
|
||||||
profile = profile,
|
|
||||||
active = connected && profile.getUUID() == state.activeProfile?.getUUID(),
|
|
||||||
onDismiss = { sheetProfile = null },
|
|
||||||
onConnect = {
|
|
||||||
sheetProfile = null
|
|
||||||
viewModel.select(profile)
|
|
||||||
viewModel.connect(context, profile)
|
|
||||||
},
|
|
||||||
onEdit = {
|
|
||||||
sheetProfile = null
|
|
||||||
onEditProfile(profile)
|
|
||||||
},
|
|
||||||
onShare = {
|
|
||||||
sheetProfile = null
|
|
||||||
onShareProfile(profile)
|
|
||||||
},
|
|
||||||
onDelete = {
|
|
||||||
sheetProfile = null
|
|
||||||
pendingDelete = profile
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingDelete?.let { profile ->
|
|
||||||
ShxAlertDialog(
|
|
||||||
onDismissRequest = { pendingDelete = null },
|
|
||||||
title = { Text(stringResource(R.string.delete_profile)) },
|
|
||||||
text = { Text(profile.name ?: "") },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
viewModel.delete(profile)
|
|
||||||
pendingDelete = null
|
|
||||||
}) { Text(stringResource(R.string.delete_profile)) }
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { pendingDelete = null }) {
|
|
||||||
Text(stringResource(android.R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Bottom sheet to switch the profile without leaving the Connect tab. */
|
/** «Server» sheet of Home: the servers as on F_Servers, manage / add below. */
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProfilePickerSheet(
|
private fun ProfilePickerSheet(
|
||||||
profiles: List<VpnProfile>,
|
profiles: List<VpnProfile>,
|
||||||
@@ -492,240 +420,58 @@ private fun ProfilePickerSheet(
|
|||||||
onAdd: () -> Unit,
|
onAdd: () -> Unit,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
val c = LocalShxColors.current
|
||||||
/* the current profile first, the rest in list order */
|
/* the current profile first, the rest in list order */
|
||||||
val ordered = remember(profiles, selected) {
|
val ordered = remember(profiles, selected) {
|
||||||
profiles.sortedByDescending { it.getUUID() == selected?.getUUID() }
|
profiles.sortedByDescending { it.getUUID() == selected?.getUUID() }
|
||||||
}
|
}
|
||||||
ModalBottomSheet(
|
ShxSheet(onDismiss = onDismiss)
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
Column(Modifier.fillMaxWidth())
|
ShxSheetTitle(stringResource(R.string.home_profile_picker_title))
|
||||||
|
ShxPanel()
|
||||||
{
|
{
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_profile_picker_title),
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
modifier = Modifier.padding(horizontal = ShxSpacing.xl, vertical = ShxSpacing.sm).semantics { heading() },
|
|
||||||
)
|
|
||||||
ordered.forEachIndexed { index, profile ->
|
ordered.forEachIndexed { index, profile ->
|
||||||
ShxProfileRow(
|
val active = profile.getUUID() == activeUuid
|
||||||
profile = profile,
|
val chosen = profile.getUUID() == selected?.getUUID()
|
||||||
active = profile.getUUID() == activeUuid,
|
Row(
|
||||||
onClick = { onPick(profile) },
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
onLongClick = { onLongPick(profile) },
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
onLongClickLabel = stringResource(R.string.profile_actions),
|
modifier = Modifier.fillMaxWidth().height(68.dp)
|
||||||
trailing = if (profile.getUUID() == selected?.getUUID() && profile.getUUID() != activeUuid)
|
.combinedClickable(
|
||||||
{
|
onClick = { onPick(profile) },
|
||||||
{ Icon(Icons.Rounded.Check, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }
|
onLongClick = { onLongPick(profile) },
|
||||||
}
|
onLongClickLabel = stringResource(R.string.profile_actions),
|
||||||
else null,
|
)
|
||||||
|
.padding(horizontal = 14.dp),
|
||||||
)
|
)
|
||||||
|
{
|
||||||
|
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(40.dp).clip(CircleShape).background(if (active) c.accent else c.tile))
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
org.strongswan.android.ui.compose.home.serverCode(profile, null),
|
||||||
|
style = ShxType.chip.copy(fontWeight = FontWeight.ExtraBold), color = if (active) Color.White else c.onAccentContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp))
|
||||||
|
{
|
||||||
|
Text(profile.name ?: profile.gateway ?: "", style = ShxType.rowTitle, color = c.text, maxLines = 1)
|
||||||
|
Text(profile.gateway ?: "", style = ShxType.meta.copy(fontWeight = FontWeight.Normal), color = c.textSub, maxLines = 1)
|
||||||
|
}
|
||||||
|
if (chosen)
|
||||||
|
{
|
||||||
|
Icon(ShxIcons.Check, contentDescription = null, tint = c.accent, modifier = Modifier.size(20.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
if (index < ordered.lastIndex)
|
if (index < ordered.lastIndex)
|
||||||
{
|
{
|
||||||
ShxDivider()
|
Box(Modifier.fillMaxWidth().height(1.dp).background(c.tile))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = ShxSpacing.lg, vertical = ShxSpacing.sm),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
TextButton(onClick = onManage) { Text(stringResource(R.string.home_manage_profiles)) }
|
|
||||||
TextButton(onClick = onAdd) {
|
|
||||||
Icon(Icons.Rounded.Add, contentDescription = null, modifier = Modifier.size(18.dp))
|
|
||||||
Spacer(Modifier.size(ShxSpacing.xs))
|
|
||||||
Text(stringResource(R.string.add_profile_cta))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(ShxSpacing.lg))
|
|
||||||
}
|
}
|
||||||
}
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp))
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun ConnectionDetailsSheet(
|
|
||||||
profile: VpnProfile?,
|
|
||||||
traffic: TrafficUi,
|
|
||||||
durationSec: Long,
|
|
||||||
probes: Map<ProbeTarget, ProbeResult>,
|
|
||||||
exit: ExitUi?,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onDisconnect: () -> Unit,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Column(Modifier.padding(horizontal = 24.dp))
|
|
||||||
{
|
{
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp))
|
ShxButton(stringResource(R.string.home_manage_profiles), onManage, style = ShxButtonStyle.SOFT, height = 48.dp, modifier = Modifier.weight(1f))
|
||||||
{
|
ShxButton(stringResource(R.string.add_profile_cta), onAdd, style = ShxButtonStyle.OUTLINE, height = 48.dp, modifier = Modifier.weight(1f), icon = ShxIcons.Plus)
|
||||||
ProfileAvatar(icon = Icons.Rounded.Language, connected = true)
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
profile?.name ?: "",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
profile?.gateway ?: "",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
DetailRow(icon = Icons.Rounded.Language, label = stringResource(R.string.profile_gateway_label), value = profile?.gateway ?: "—")
|
|
||||||
DetailRow(icon = Icons.Rounded.Schedule, label = stringResource(R.string.duration_label), value = formatDuration(durationSec))
|
|
||||||
DetailRow(
|
|
||||||
icon = Icons.Rounded.ArrowDownward,
|
|
||||||
label = stringResource(R.string.traffic_down),
|
|
||||||
value = "${formatBytes(traffic.bytesIn)} · ${formatRate(traffic.rateIn)}",
|
|
||||||
)
|
|
||||||
DetailRow(
|
|
||||||
icon = Icons.Rounded.ArrowUpward,
|
|
||||||
label = stringResource(R.string.traffic_up),
|
|
||||||
value = "${formatBytes(traffic.bytesOut)} · ${formatRate(traffic.rateOut)}",
|
|
||||||
)
|
|
||||||
/* RU-split session breakdown; null in the system-routes mode where
|
|
||||||
* the engine never sees the direct traffic */
|
|
||||||
traffic.split?.let { split ->
|
|
||||||
DetailRow(
|
|
||||||
icon = Icons.Rounded.AltRoute,
|
|
||||||
label = stringResource(R.string.traffic_split_tunnel),
|
|
||||||
value = "${formatBytes(split.tunnelBytes)} · ${formatRate(split.tunnelRate)}",
|
|
||||||
)
|
|
||||||
DetailRow(
|
|
||||||
icon = Icons.Rounded.AltRoute,
|
|
||||||
label = stringResource(R.string.traffic_split_direct),
|
|
||||||
value = "${formatBytes(split.directBytes)} · ${formatRate(split.directRate)}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
exit?.let { e ->
|
|
||||||
e.ip?.let { ip ->
|
|
||||||
DetailRow(
|
|
||||||
icon = Icons.Rounded.Public,
|
|
||||||
label = stringResource(R.string.exit_ip_label),
|
|
||||||
value = e.country?.let { "$ip · $it" } ?: ip,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
e.network?.let { network ->
|
|
||||||
DetailRow(
|
|
||||||
icon = if (e.ssid != null) Icons.Rounded.Wifi else Icons.Rounded.SignalCellularAlt,
|
|
||||||
label = stringResource(R.string.network_label),
|
|
||||||
value = e.ssid?.let { "$network · $it" } ?: network,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (probes.isNotEmpty())
|
|
||||||
{
|
|
||||||
probes.sortedForDisplay().forEach { result ->
|
|
||||||
DetailRow(
|
|
||||||
icon = Icons.Rounded.Dns,
|
|
||||||
label = stringResource(result.target.labelRes()),
|
|
||||||
value = result.rttText(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(24.dp))
|
|
||||||
FilledTonalButton(
|
|
||||||
onClick = onDisconnect,
|
|
||||||
colors = ButtonDefaults.filledTonalButtonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
|
||||||
),
|
|
||||||
shape = MaterialTheme.shapes.medium,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.disconnect))
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(32.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun DetailRow(icon: ImageVector, label: String, value: String)
|
|
||||||
{
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Surface(
|
|
||||||
shape = CircleShape,
|
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.size(18.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
headlineContent = { Text(label) },
|
|
||||||
supportingContent = { Text(value) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun ProfileActionsSheet(
|
|
||||||
profile: VpnProfile,
|
|
||||||
active: Boolean,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onConnect: () -> Unit,
|
|
||||||
onEdit: () -> Unit,
|
|
||||||
onShare: () -> Unit,
|
|
||||||
onDelete: () -> Unit,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Column(Modifier.fillMaxWidth())
|
|
||||||
{
|
|
||||||
ShxProfileRow(profile = profile, active = active, onClick = {}, onLongClick = {})
|
|
||||||
ShxDivider(indent = false)
|
|
||||||
SheetAction(icon = Icons.Rounded.PlayArrow, label = stringResource(R.string.connect), onClick = onConnect)
|
|
||||||
ShxDivider(indent = false)
|
|
||||||
SheetAction(icon = Icons.Rounded.Edit, label = stringResource(R.string.edit_profile), onClick = onEdit)
|
|
||||||
ShxDivider(indent = false)
|
|
||||||
SheetAction(icon = Icons.Rounded.Share, label = stringResource(R.string.share_profile), onClick = onShare)
|
|
||||||
ShxDivider(indent = false)
|
|
||||||
SheetAction(
|
|
||||||
icon = Icons.Rounded.Delete,
|
|
||||||
label = stringResource(R.string.delete_profile),
|
|
||||||
tint = MaterialTheme.colorScheme.error,
|
|
||||||
onClick = onDelete,
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(24.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SheetAction(icon: ImageVector, label: String, onClick: () -> Unit, tint: Color = Color.Unspecified)
|
|
||||||
{
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.combinedClickable(onClick = onClick)
|
|
||||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Icon(icon, contentDescription = null, tint = tint, modifier = Modifier.size(22.dp))
|
|
||||||
Spacer(Modifier.size(16.dp))
|
|
||||||
Text(label, style = MaterialTheme.typography.bodyLarge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-7
@@ -39,12 +39,6 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
|
||||||
import androidx.compose.material.icons.filled.Visibility
|
|
||||||
import androidx.compose.material.icons.filled.VisibilityOff
|
|
||||||
import androidx.compose.material.icons.rounded.AltRoute
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
@@ -410,7 +404,7 @@ fun ProfileFormScreen(
|
|||||||
trailing = {
|
trailing = {
|
||||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(44.dp).clip(CircleShape).clickable { showPassword = !showPassword })
|
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(44.dp).clip(CircleShape).clickable { showPassword = !showPassword })
|
||||||
{
|
{
|
||||||
Icon(if (showPassword) Icons.Default.VisibilityOff else Icons.Default.Visibility, null, tint = c.textSub, modifier = Modifier.size(20.dp))
|
Icon(if (showPassword) ShxIcons.EyeOff else ShxIcons.Eye, null, tint = c.textSub, modifier = Modifier.size(20.dp))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
-135
@@ -1,135 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 SHX VPN
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation; either version 2 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.strongswan.android.ui.compose
|
|
||||||
|
|
||||||
import org.strongswan.android.ui.compose.components.ShxConnectedPill
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Language
|
|
||||||
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.vector.ImageVector
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import org.strongswan.android.R
|
|
||||||
import org.strongswan.android.data.VpnProfile
|
|
||||||
import org.strongswan.android.ui.compose.theme.LocalShxStatusColors
|
|
||||||
import org.strongswan.android.ui.compose.theme.ShxSpacing
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ProfileAvatar(icon: ImageVector, connected: Boolean, modifier: Modifier = Modifier)
|
|
||||||
{
|
|
||||||
val status = LocalShxStatusColors.current
|
|
||||||
val container = if (connected) status.connectedContainer else MaterialTheme.colorScheme.primaryContainer
|
|
||||||
val tint = if (connected) status.connected else MaterialTheme.colorScheme.primary
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.background(container, CircleShape),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = tint,
|
|
||||||
modifier = Modifier.size(22.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
fun ShxProfileRow(
|
|
||||||
profile: VpnProfile,
|
|
||||||
active: Boolean,
|
|
||||||
leadingIcon: ImageVector = Icons.Rounded.Language,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
onLongClick: () -> Unit = {},
|
|
||||||
onLongClickLabel: String? = null,
|
|
||||||
trailing: (@Composable () -> Unit)? = null,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
onLongClickLabel = onLongClickLabel,
|
|
||||||
)
|
|
||||||
.padding(horizontal = ShxSpacing.lg, vertical = ShxSpacing.md),
|
|
||||||
) {
|
|
||||||
ProfileAvatar(icon = leadingIcon, connected = active)
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(horizontal = ShxSpacing.md),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = profile.name ?: "",
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = profile.gateway ?: "",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (trailing != null)
|
|
||||||
{
|
|
||||||
trailing()
|
|
||||||
}
|
|
||||||
else if (active)
|
|
||||||
{
|
|
||||||
ActiveBadge()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ActiveBadge(modifier: Modifier = Modifier)
|
|
||||||
{
|
|
||||||
ShxConnectedPill(text = stringResource(R.string.badge_connected), modifier = modifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ShxDivider(modifier: Modifier = Modifier, indent: Boolean = true)
|
|
||||||
{
|
|
||||||
Surface(
|
|
||||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f),
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = if (indent) 68.dp else 0.dp, end = if (indent) ShxSpacing.lg else 0.dp)
|
|
||||||
.height(1.dp),
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
+2
-14
@@ -17,18 +17,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Add
|
|
||||||
import androidx.compose.material.icons.rounded.Close
|
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
|
||||||
import androidx.compose.material.icons.rounded.Edit
|
|
||||||
import androidx.compose.material.icons.rounded.Language
|
|
||||||
import androidx.compose.material.icons.rounded.MoreVert
|
|
||||||
import androidx.compose.material.icons.rounded.PlayArrow
|
|
||||||
import androidx.compose.material.icons.rounded.Search
|
|
||||||
import androidx.compose.material.icons.rounded.SearchOff
|
|
||||||
import androidx.compose.material.icons.rounded.Share
|
|
||||||
import androidx.compose.material.icons.rounded.Stop
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -172,7 +160,7 @@ fun ProfilesScreen(
|
|||||||
{
|
{
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center)
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center)
|
||||||
{
|
{
|
||||||
ShxEmptyState(icon = Icons.Rounded.Language, message = stringResource(R.string.profile_detail_placeholder))
|
ShxEmptyState(icon = ShxIcons.Globe, message = stringResource(R.string.profile_detail_placeholder))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,7 +413,7 @@ private fun ServerRow(profile: VpnProfile, active: Boolean, ping: ServerPing?, o
|
|||||||
modifier = Modifier.size(44.dp).clip(CircleShape).clickable(role = Role.Button, onClick = onMore),
|
modifier = Modifier.size(44.dp).clip(CircleShape).clickable(role = Role.Button, onClick = onMore),
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Icon(Icons.Rounded.MoreVert, contentDescription = stringResource(R.string.action_more_options), tint = c.textSub, modifier = Modifier.size(20.dp))
|
Icon(ShxIcons.More, contentDescription = stringResource(R.string.action_more_options), tint = c.textSub, modifier = Modifier.size(20.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-3
@@ -107,11 +107,8 @@ import org.strongswan.android.ui.compose.components.PredictiveBackScale
|
|||||||
import org.strongswan.android.ui.compose.components.ShxCard
|
import org.strongswan.android.ui.compose.components.ShxCard
|
||||||
import org.strongswan.android.ui.compose.components.ShxSnackbarHost
|
import org.strongswan.android.ui.compose.components.ShxSnackbarHost
|
||||||
import org.strongswan.android.ui.compose.routing.DeadHostsPage
|
import org.strongswan.android.ui.compose.routing.DeadHostsPage
|
||||||
import org.strongswan.android.ui.compose.routing.DirectAppsCard
|
|
||||||
import org.strongswan.android.ui.compose.routing.DirectAppsScreen
|
import org.strongswan.android.ui.compose.routing.DirectAppsScreen
|
||||||
import org.strongswan.android.ui.compose.routing.RecentAppsPage
|
import org.strongswan.android.ui.compose.routing.RecentAppsPage
|
||||||
import org.strongswan.android.ui.compose.routing.RoutingHeroCard
|
|
||||||
import org.strongswan.android.ui.compose.routing.RuSitesCard
|
|
||||||
import org.strongswan.android.ui.compose.routing.RulesPage
|
import org.strongswan.android.ui.compose.routing.RulesPage
|
||||||
import org.strongswan.android.ui.compose.routing.routingSummary
|
import org.strongswan.android.ui.compose.routing.routingSummary
|
||||||
import org.strongswan.android.ui.compose.theme.LocalShxStatusColors
|
import org.strongswan.android.ui.compose.theme.LocalShxStatusColors
|
||||||
|
|||||||
+25
-47
@@ -24,32 +24,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Autorenew
|
|
||||||
import androidx.compose.material.icons.rounded.BatteryChargingFull
|
|
||||||
import androidx.compose.material.icons.rounded.Bedtime
|
|
||||||
import androidx.compose.material.icons.rounded.Bolt
|
|
||||||
import androidx.compose.material.icons.rounded.BugReport
|
|
||||||
import androidx.compose.material.icons.rounded.Cached
|
|
||||||
import androidx.compose.material.icons.rounded.CloudDownload
|
|
||||||
import androidx.compose.material.icons.rounded.CloudSync
|
|
||||||
import androidx.compose.material.icons.rounded.Description
|
|
||||||
import androidx.compose.material.icons.rounded.FileDownload
|
|
||||||
import androidx.compose.material.icons.rounded.GppGood
|
|
||||||
import androidx.compose.material.icons.rounded.GppMaybe
|
|
||||||
import androidx.compose.material.icons.rounded.History
|
|
||||||
import androidx.compose.material.icons.rounded.NetworkCheck
|
|
||||||
import androidx.compose.material.icons.rounded.Palette
|
|
||||||
import androidx.compose.material.icons.rounded.RestartAlt
|
|
||||||
import androidx.compose.material.icons.rounded.Security
|
|
||||||
import androidx.compose.material.icons.rounded.SystemUpdateAlt
|
|
||||||
import androidx.compose.material.icons.rounded.VerifiedUser
|
|
||||||
import androidx.compose.material.icons.rounded.Wifi
|
|
||||||
import org.strongswan.android.ui.compose.design.ShxAlertDialog
|
import org.strongswan.android.ui.compose.design.ShxAlertDialog
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -77,7 +55,6 @@ import androidx.compose.animation.slideInHorizontally
|
|||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
|
||||||
import androidx.compose.material.icons.rounded.Brightness6
|
import androidx.compose.material.icons.rounded.Brightness6
|
||||||
import androidx.compose.material3.SegmentedButton
|
import androidx.compose.material3.SegmentedButton
|
||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
@@ -101,6 +78,7 @@ import androidx.compose.runtime.produceState
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.strongswan.android.R
|
import org.strongswan.android.R
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxDialogButton
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -284,14 +262,14 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
ShxSectionDivider()
|
ShxSectionDivider()
|
||||||
}
|
}
|
||||||
ShxSettingRow(
|
ShxSettingRow(
|
||||||
icon = Icons.Rounded.History,
|
icon = ShxIcons.Clock,
|
||||||
title = stringResource(R.string.pref_default_vpn_profile),
|
title = stringResource(R.string.pref_default_vpn_profile),
|
||||||
subtitle = defaultLabel,
|
subtitle = defaultLabel,
|
||||||
onClick = { showDefaultPicker = true },
|
onClick = { showDefaultPicker = true },
|
||||||
)
|
)
|
||||||
ShxSectionDivider()
|
ShxSectionDivider()
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.BatteryChargingFull,
|
icon = ShxIcons.Battery,
|
||||||
title = stringResource(R.string.pref_power_whitelist_title),
|
title = stringResource(R.string.pref_power_whitelist_title),
|
||||||
subtitle = stringResource(R.string.pref_power_whitelist_summary),
|
subtitle = stringResource(R.string.pref_power_whitelist_summary),
|
||||||
checked = ignorePower,
|
checked = ignorePower,
|
||||||
@@ -306,7 +284,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
* disabled, which breaks watchdog-based recovery of the tunnel */
|
* disabled, which breaks watchdog-based recovery of the tunnel */
|
||||||
ShxSectionDivider()
|
ShxSectionDivider()
|
||||||
ShxSettingRow(
|
ShxSettingRow(
|
||||||
icon = Icons.Rounded.Bedtime,
|
icon = ShxIcons.Moon,
|
||||||
title = stringResource(R.string.samsung_sleep_title),
|
title = stringResource(R.string.samsung_sleep_title),
|
||||||
subtitle = stringResource(R.string.samsung_sleep_summary),
|
subtitle = stringResource(R.string.samsung_sleep_summary),
|
||||||
onClick = { openSamsungBatterySettings(context) },
|
onClick = { openSamsungBatterySettings(context) },
|
||||||
@@ -323,7 +301,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
mutableStateOf(WhiteListManager.getInstance(context).currentConfig.enabled)
|
mutableStateOf(WhiteListManager.getInstance(context).currentConfig.enabled)
|
||||||
}
|
}
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.GppMaybe,
|
icon = ShxIcons.Alert,
|
||||||
title = stringResource(R.string.whitelist_analysis_title),
|
title = stringResource(R.string.whitelist_analysis_title),
|
||||||
subtitle = stringResource(R.string.whitelist_analysis_summary),
|
subtitle = stringResource(R.string.whitelist_analysis_summary),
|
||||||
checked = whiteListEnabled,
|
checked = whiteListEnabled,
|
||||||
@@ -337,7 +315,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
mutableStateOf(VpnShortcutManager.getInstance(context).isEnabled())
|
mutableStateOf(VpnShortcutManager.getInstance(context).isEnabled())
|
||||||
}
|
}
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.Bolt,
|
icon = ShxIcons.Bolt,
|
||||||
title = stringResource(R.string.pref_routines_shortcuts_title),
|
title = stringResource(R.string.pref_routines_shortcuts_title),
|
||||||
subtitle = stringResource(R.string.pref_routines_shortcuts_summary),
|
subtitle = stringResource(R.string.pref_routines_shortcuts_summary),
|
||||||
checked = shortcutsEnabled,
|
checked = shortcutsEnabled,
|
||||||
@@ -351,7 +329,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
mutableStateOf(prefs.getBoolean(Constants.PREF_WATCHDOG_ENABLED, true))
|
mutableStateOf(prefs.getBoolean(Constants.PREF_WATCHDOG_ENABLED, true))
|
||||||
}
|
}
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.RestartAlt,
|
icon = ShxIcons.Refresh,
|
||||||
title = stringResource(R.string.watchdog_title),
|
title = stringResource(R.string.watchdog_title),
|
||||||
subtitle = stringResource(R.string.watchdog_summary),
|
subtitle = stringResource(R.string.watchdog_summary),
|
||||||
checked = watchdogEnabled,
|
checked = watchdogEnabled,
|
||||||
@@ -377,7 +355,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
mutableStateOf(prefs.getBoolean(Constants.PREF_SAFE_IPV6, false))
|
mutableStateOf(prefs.getBoolean(Constants.PREF_SAFE_IPV6, false))
|
||||||
}
|
}
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.GppGood,
|
icon = ShxIcons.Shield,
|
||||||
title = stringResource(R.string.safe_ipv6_title),
|
title = stringResource(R.string.safe_ipv6_title),
|
||||||
subtitle = stringResource(R.string.safe_ipv6_summary),
|
subtitle = stringResource(R.string.safe_ipv6_summary),
|
||||||
checked = safeIpv6,
|
checked = safeIpv6,
|
||||||
@@ -394,7 +372,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
ShxSection(title = stringResource(R.string.settings_section_certs))
|
ShxSection(title = stringResource(R.string.settings_section_certs))
|
||||||
{
|
{
|
||||||
ShxSettingRow(
|
ShxSettingRow(
|
||||||
icon = Icons.Rounded.VerifiedUser,
|
icon = ShxIcons.Cert,
|
||||||
title = stringResource(R.string.trusted_certs_title),
|
title = stringResource(R.string.trusted_certs_title),
|
||||||
onClick = {
|
onClick = {
|
||||||
context.startActivity(Intent(context, TrustedCertificatesActivity::class.java))
|
context.startActivity(Intent(context, TrustedCertificatesActivity::class.java))
|
||||||
@@ -402,7 +380,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
)
|
)
|
||||||
ShxSectionDivider()
|
ShxSectionDivider()
|
||||||
ShxSettingRow(
|
ShxSettingRow(
|
||||||
icon = Icons.Rounded.Cached,
|
icon = ShxIcons.Refresh,
|
||||||
title = stringResource(R.string.crl_cache),
|
title = stringResource(R.string.crl_cache),
|
||||||
onClick = { showCrl = true },
|
onClick = { showCrl = true },
|
||||||
)
|
)
|
||||||
@@ -428,7 +406,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
mutableStateOf(prefs.getBoolean(Constants.PREF_EXTENDED_DIAG, false))
|
mutableStateOf(prefs.getBoolean(Constants.PREF_EXTENDED_DIAG, false))
|
||||||
}
|
}
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.BugReport,
|
icon = ShxIcons.Bug,
|
||||||
title = stringResource(R.string.extended_diag_title),
|
title = stringResource(R.string.extended_diag_title),
|
||||||
subtitle = stringResource(R.string.extended_diag_summary),
|
subtitle = stringResource(R.string.extended_diag_summary),
|
||||||
checked = extendedDiag,
|
checked = extendedDiag,
|
||||||
@@ -476,7 +454,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
mutableStateOf(prefs.getBoolean(Constants.PREF_AUTO_UPDATE_CHECK, true))
|
mutableStateOf(prefs.getBoolean(Constants.PREF_AUTO_UPDATE_CHECK, true))
|
||||||
}
|
}
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.Autorenew,
|
icon = ShxIcons.Refresh,
|
||||||
title = stringResource(R.string.update_auto_check_title),
|
title = stringResource(R.string.update_auto_check_title),
|
||||||
subtitle = stringResource(R.string.update_auto_check_summary),
|
subtitle = stringResource(R.string.update_auto_check_summary),
|
||||||
checked = autoUpdate,
|
checked = autoUpdate,
|
||||||
@@ -490,7 +468,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
mutableStateOf(prefs.getBoolean(Constants.PREF_SILENT_UPDATES, false))
|
mutableStateOf(prefs.getBoolean(Constants.PREF_SILENT_UPDATES, false))
|
||||||
}
|
}
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.CloudSync,
|
icon = ShxIcons.Refresh,
|
||||||
title = stringResource(R.string.update_silent_title),
|
title = stringResource(R.string.update_silent_title),
|
||||||
subtitle = stringResource(R.string.update_silent_summary),
|
subtitle = stringResource(R.string.update_silent_summary),
|
||||||
checked = silentUpdates,
|
checked = silentUpdates,
|
||||||
@@ -501,7 +479,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
)
|
)
|
||||||
ShxSectionDivider()
|
ShxSectionDivider()
|
||||||
ShxSettingRow(
|
ShxSettingRow(
|
||||||
icon = Icons.Rounded.SystemUpdateAlt,
|
icon = ShxIcons.Download,
|
||||||
title = stringResource(R.string.update_check_title),
|
title = stringResource(R.string.update_check_title),
|
||||||
subtitle = updateStatusText(updateState),
|
subtitle = updateStatusText(updateState),
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -518,7 +496,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
{
|
{
|
||||||
ShxSectionDivider()
|
ShxSectionDivider()
|
||||||
ShxSettingRow(
|
ShxSettingRow(
|
||||||
icon = Icons.Rounded.CloudDownload,
|
icon = ShxIcons.Download,
|
||||||
title = stringResource(R.string.update_download_install),
|
title = stringResource(R.string.update_download_install),
|
||||||
subtitle = when (val state = updateState)
|
subtitle = when (val state = updateState)
|
||||||
{
|
{
|
||||||
@@ -583,7 +561,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = { showDefaultPicker = false }) {
|
ShxDialogButton(onClick = { showDefaultPicker = false }) {
|
||||||
Text(stringResource(android.R.string.cancel))
|
Text(stringResource(android.R.string.cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -603,7 +581,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
ShxDialogButton(primary = true, danger = true, onClick = {
|
||||||
if (files.isEmpty())
|
if (files.isEmpty())
|
||||||
{
|
{
|
||||||
scope.launch { snackbarHostState.showSnackbar(context.getString(R.string.clear_crl_cache_msg_none)) }
|
scope.launch { snackbarHostState.showSnackbar(context.getString(R.string.clear_crl_cache_msg_none)) }
|
||||||
@@ -616,7 +594,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
}) { Text(stringResource(R.string.clear)) }
|
}) { Text(stringResource(R.string.clear)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showCrl = false }) {
|
ShxDialogButton(onClick = { showCrl = false }) {
|
||||||
Text(stringResource(android.R.string.cancel))
|
Text(stringResource(android.R.string.cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -632,13 +610,13 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
title = { Text(stringResource(R.string.update_confirm_title, info.versionName)) },
|
title = { Text(stringResource(R.string.update_confirm_title, info.versionName)) },
|
||||||
text = { if (notes.isNotEmpty()) { Text(notes) } },
|
text = { if (notes.isNotEmpty()) { Text(notes) } },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
ShxDialogButton(primary = true, onClick = {
|
||||||
showUpdateConfirm = false
|
showUpdateConfirm = false
|
||||||
startUpdate()
|
startUpdate()
|
||||||
}) { Text(stringResource(R.string.update_action)) }
|
}) { Text(stringResource(R.string.update_action)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showUpdateConfirm = false }) {
|
ShxDialogButton(onClick = { showUpdateConfirm = false }) {
|
||||||
Text(stringResource(android.R.string.cancel))
|
Text(stringResource(android.R.string.cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -930,13 +908,13 @@ private fun SettingsRoot(onOpen: (SettingsPage) -> Unit, updateState: UpdateStat
|
|||||||
title = { Text(stringResource(R.string.kill_switch_dialog_title)) },
|
title = { Text(stringResource(R.string.kill_switch_dialog_title)) },
|
||||||
text = { Text(stringResource(R.string.kill_switch_dialog_text)) },
|
text = { Text(stringResource(R.string.kill_switch_dialog_text)) },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
ShxDialogButton(primary = true, onClick = {
|
||||||
showKillSwitchDialog = false
|
showKillSwitchDialog = false
|
||||||
context.startActivity(Intent(Settings.ACTION_VPN_SETTINGS))
|
context.startActivity(Intent(Settings.ACTION_VPN_SETTINGS))
|
||||||
}) { Text(stringResource(R.string.kill_switch_dialog_enable)) }
|
}) { Text(stringResource(R.string.kill_switch_dialog_enable)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showKillSwitchDialog = false }) {
|
ShxDialogButton(onClick = { showKillSwitchDialog = false }) {
|
||||||
Text(stringResource(android.R.string.cancel))
|
Text(stringResource(android.R.string.cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -999,20 +977,20 @@ private fun KillSwitchRow()
|
|||||||
title = { Text(stringResource(R.string.kill_switch_dialog_title)) },
|
title = { Text(stringResource(R.string.kill_switch_dialog_title)) },
|
||||||
text = { Text(stringResource(R.string.kill_switch_dialog_text)) },
|
text = { Text(stringResource(R.string.kill_switch_dialog_text)) },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
ShxDialogButton(primary = true, onClick = {
|
||||||
showKillSwitchDialog = false
|
showKillSwitchDialog = false
|
||||||
context.startActivity(Intent(Settings.ACTION_VPN_SETTINGS))
|
context.startActivity(Intent(Settings.ACTION_VPN_SETTINGS))
|
||||||
}) { Text(stringResource(R.string.kill_switch_dialog_enable)) }
|
}) { Text(stringResource(R.string.kill_switch_dialog_enable)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showKillSwitchDialog = false }) {
|
ShxDialogButton(onClick = { showKillSwitchDialog = false }) {
|
||||||
Text(stringResource(android.R.string.cancel))
|
Text(stringResource(android.R.string.cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ShxSettingRow(
|
ShxSettingRow(
|
||||||
icon = Icons.Rounded.Security,
|
icon = ShxIcons.Shield,
|
||||||
title = stringResource(R.string.kill_switch_title),
|
title = stringResource(R.string.kill_switch_title),
|
||||||
subtitle = stringResource(
|
subtitle = stringResource(
|
||||||
if (lockdownActive) R.string.kill_switch_summary_on
|
if (lockdownActive) R.string.kill_switch_summary_on
|
||||||
|
|||||||
+8
-12
@@ -33,11 +33,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Add
|
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
|
||||||
import androidx.compose.material.icons.rounded.Language
|
|
||||||
import androidx.compose.material.icons.rounded.Wifi
|
|
||||||
import org.strongswan.android.ui.compose.design.ShxAlertDialog
|
import org.strongswan.android.ui.compose.design.ShxAlertDialog
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -67,6 +62,7 @@ import androidx.compose.ui.semantics.heading
|
|||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import org.strongswan.android.R
|
import org.strongswan.android.R
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxIcons
|
||||||
import org.strongswan.android.ui.compose.components.ShxSection
|
import org.strongswan.android.ui.compose.components.ShxSection
|
||||||
import org.strongswan.android.ui.compose.components.ShxSectionDivider
|
import org.strongswan.android.ui.compose.components.ShxSectionDivider
|
||||||
import org.strongswan.android.ui.compose.components.ShxSettingRow
|
import org.strongswan.android.ui.compose.components.ShxSettingRow
|
||||||
@@ -169,7 +165,7 @@ fun TrustedNetworksScreen(
|
|||||||
ShxSection(title = stringResource(R.string.settings_section_general))
|
ShxSection(title = stringResource(R.string.settings_section_general))
|
||||||
{
|
{
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.Wifi,
|
icon = ShxIcons.Wifi,
|
||||||
title = stringResource(R.string.auto_connect_title),
|
title = stringResource(R.string.auto_connect_title),
|
||||||
subtitle = stringResource(R.string.auto_connect_summary),
|
subtitle = stringResource(R.string.auto_connect_summary),
|
||||||
checked = enabled,
|
checked = enabled,
|
||||||
@@ -181,7 +177,7 @@ fun TrustedNetworksScreen(
|
|||||||
)
|
)
|
||||||
ShxSectionDivider()
|
ShxSectionDivider()
|
||||||
ShxSettingRow(
|
ShxSettingRow(
|
||||||
icon = Icons.Rounded.Language,
|
icon = ShxIcons.Globe,
|
||||||
title = stringResource(R.string.auto_connect_profile),
|
title = stringResource(R.string.auto_connect_profile),
|
||||||
subtitle = selectedProfile?.name
|
subtitle = selectedProfile?.name
|
||||||
?: stringResource(R.string.auto_connect_profile_none),
|
?: stringResource(R.string.auto_connect_profile_none),
|
||||||
@@ -209,7 +205,7 @@ fun TrustedNetworksScreen(
|
|||||||
{
|
{
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(ShxSpacing.md))
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(ShxSpacing.md))
|
||||||
{
|
{
|
||||||
Icon(Icons.Rounded.Wifi, contentDescription = null)
|
Icon(ShxIcons.Wifi, contentDescription = null)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.auto_connect_location_title),
|
text = stringResource(R.string.auto_connect_location_title),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
@@ -269,7 +265,7 @@ fun TrustedNetworksScreen(
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Wifi,
|
ShxIcons.Wifi,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
@@ -283,7 +279,7 @@ fun TrustedNetworksScreen(
|
|||||||
)
|
)
|
||||||
IconButton(onClick = { pendingDeleteSsid = ssid }) {
|
IconButton(onClick = { pendingDeleteSsid = ssid }) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Delete,
|
ShxIcons.Trash,
|
||||||
contentDescription = stringResource(R.string.delete_profile),
|
contentDescription = stringResource(R.string.delete_profile),
|
||||||
tint = MaterialTheme.colorScheme.error,
|
tint = MaterialTheme.colorScheme.error,
|
||||||
)
|
)
|
||||||
@@ -410,7 +406,7 @@ private fun TrustedAddNetworkRow(
|
|||||||
{
|
{
|
||||||
TextButton(onClick = { onAddCurrent(currentSsid) })
|
TextButton(onClick = { onAddCurrent(currentSsid) })
|
||||||
{
|
{
|
||||||
Icon(Icons.Rounded.Add, contentDescription = null, modifier = Modifier.size(18.dp))
|
Icon(ShxIcons.Plus, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.size(ShxSpacing.sm))
|
Spacer(Modifier.size(ShxSpacing.sm))
|
||||||
Text(stringResource(R.string.auto_connect_add_current, currentSsid))
|
Text(stringResource(R.string.auto_connect_add_current, currentSsid))
|
||||||
}
|
}
|
||||||
@@ -419,7 +415,7 @@ private fun TrustedAddNetworkRow(
|
|||||||
{
|
{
|
||||||
TextButton(onClick = onAddManual)
|
TextButton(onClick = onAddManual)
|
||||||
{
|
{
|
||||||
Icon(Icons.Rounded.Add, contentDescription = null, modifier = Modifier.size(18.dp))
|
Icon(ShxIcons.Plus, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.size(ShxSpacing.sm))
|
Spacer(Modifier.size(ShxSpacing.sm))
|
||||||
Text(stringResource(R.string.auto_connect_add_network))
|
Text(stringResource(R.string.auto_connect_add_network))
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-14
@@ -19,9 +19,6 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Visibility
|
|
||||||
import androidx.compose.material.icons.rounded.VisibilityOff
|
|
||||||
import org.strongswan.android.ui.compose.design.ShxAlertDialog
|
import org.strongswan.android.ui.compose.design.ShxAlertDialog
|
||||||
import org.strongswan.android.ui.compose.design.ShxFormField
|
import org.strongswan.android.ui.compose.design.ShxFormField
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -29,7 +26,6 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -62,6 +58,7 @@ import org.strongswan.android.ui.compose.design.ShxIcons
|
|||||||
import org.strongswan.android.ui.compose.theme.LocalShxColors
|
import org.strongswan.android.ui.compose.theme.LocalShxColors
|
||||||
import org.strongswan.android.ui.compose.theme.ShxType
|
import org.strongswan.android.ui.compose.theme.ShxType
|
||||||
import org.strongswan.android.R
|
import org.strongswan.android.R
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxDialogButton
|
||||||
import org.strongswan.android.ui.compose.theme.ShxSpacing
|
import org.strongswan.android.ui.compose.theme.ShxSpacing
|
||||||
import org.strongswan.android.ui.compose.theme.ShxTheme
|
import org.strongswan.android.ui.compose.theme.ShxTheme
|
||||||
|
|
||||||
@@ -182,7 +179,7 @@ private fun ConfirmationUi(dialog: VpnDialog.Confirmation)
|
|||||||
Row(horizontalArrangement = Arrangement.spacedBy(ShxSpacing.xs)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(ShxSpacing.xs)) {
|
||||||
if (dialog.reconnect)
|
if (dialog.reconnect)
|
||||||
{
|
{
|
||||||
TextButton(onClick = {
|
ShxDialogButton(onClick = {
|
||||||
VpnDialogBus.dismiss()
|
VpnDialogBus.dismiss()
|
||||||
VpnDialogBus.listener?.onVpnDialogDisconnect()
|
VpnDialogBus.listener?.onVpnDialogDisconnect()
|
||||||
}) {
|
}) {
|
||||||
@@ -192,7 +189,7 @@ private fun ConfirmationUi(dialog: VpnDialog.Confirmation)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextButton(onClick = {
|
ShxDialogButton(primary = true, onClick = {
|
||||||
VpnDialogBus.dismiss()
|
VpnDialogBus.dismiss()
|
||||||
val listener = VpnDialogBus.listener
|
val listener = VpnDialogBus.listener
|
||||||
if (dialog.disconnect)
|
if (dialog.disconnect)
|
||||||
@@ -207,7 +204,7 @@ private fun ConfirmationUi(dialog: VpnDialog.Confirmation)
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = ::cancelDialog) {
|
ShxDialogButton(onClick = ::cancelDialog) {
|
||||||
Text(stringResource(android.R.string.cancel))
|
Text(stringResource(android.R.string.cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -245,7 +242,7 @@ private fun LoginUi(dialog: VpnDialog.Login)
|
|||||||
trailing = {
|
trailing = {
|
||||||
IconButton(onClick = { showPassword = !showPassword }) {
|
IconButton(onClick = { showPassword = !showPassword }) {
|
||||||
Icon(
|
Icon(
|
||||||
if (showPassword) Icons.Rounded.Visibility else Icons.Rounded.VisibilityOff,
|
if (showPassword) ShxIcons.Eye else ShxIcons.EyeOff,
|
||||||
contentDescription = stringResource(
|
contentDescription = stringResource(
|
||||||
if (showPassword) R.string.login_hide_password else R.string.login_show_password
|
if (showPassword) R.string.login_hide_password else R.string.login_show_password
|
||||||
),
|
),
|
||||||
@@ -256,13 +253,13 @@ private fun LoginUi(dialog: VpnDialog.Login)
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
ShxDialogButton(primary = true, onClick = {
|
||||||
VpnDialogBus.dismiss()
|
VpnDialogBus.dismiss()
|
||||||
VpnDialogBus.listener?.onVpnDialogLogin(dialog.profileInfo, password)
|
VpnDialogBus.listener?.onVpnDialogLogin(dialog.profileInfo, password)
|
||||||
}) { Text(stringResource(R.string.login_confirm)) }
|
}) { Text(stringResource(R.string.login_confirm)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = ::cancelDialog) {
|
ShxDialogButton(onClick = ::cancelDialog) {
|
||||||
Text(stringResource(android.R.string.cancel))
|
Text(stringResource(android.R.string.cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -277,13 +274,13 @@ private fun CertificateRequiredUi(dialog: VpnDialog.CertificateRequired)
|
|||||||
title = { Text(stringResource(R.string.certificate_required_title)) },
|
title = { Text(stringResource(R.string.certificate_required_title)) },
|
||||||
text = { Text(stringResource(R.string.certificate_required_text)) },
|
text = { Text(stringResource(R.string.certificate_required_text)) },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
ShxDialogButton(primary = true, onClick = {
|
||||||
VpnDialogBus.dismiss()
|
VpnDialogBus.dismiss()
|
||||||
VpnDialogBus.listener?.onVpnDialogEditProfile(dialog.uuid)
|
VpnDialogBus.listener?.onVpnDialogEditProfile(dialog.uuid)
|
||||||
}) { Text(stringResource(R.string.edit_profile)) }
|
}) { Text(stringResource(R.string.edit_profile)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = ::cancelDialog) {
|
ShxDialogButton(onClick = ::cancelDialog) {
|
||||||
Text(stringResource(android.R.string.cancel))
|
Text(stringResource(android.R.string.cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -298,7 +295,7 @@ private fun PowerWhitelistUi()
|
|||||||
title = { Text(stringResource(R.string.power_whitelist_title)) },
|
title = { Text(stringResource(R.string.power_whitelist_title)) },
|
||||||
text = { Text(stringResource(R.string.power_whitelist_text)) },
|
text = { Text(stringResource(R.string.power_whitelist_text)) },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
ShxDialogButton(primary = true, onClick = {
|
||||||
VpnDialogBus.dismiss()
|
VpnDialogBus.dismiss()
|
||||||
VpnDialogBus.listener?.onVpnDialogPowerWhitelist()
|
VpnDialogBus.listener?.onVpnDialogPowerWhitelist()
|
||||||
}) { Text(stringResource(android.R.string.ok)) }
|
}) { Text(stringResource(android.R.string.ok)) }
|
||||||
@@ -314,7 +311,7 @@ private fun NotSupportedUi(dialog: VpnDialog.NotSupported)
|
|||||||
title = { Text(stringResource(R.string.vpn_not_supported_title)) },
|
title = { Text(stringResource(R.string.vpn_not_supported_title)) },
|
||||||
text = { Text(stringResource(dialog.message)) },
|
text = { Text(stringResource(dialog.message)) },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = ::cancelDialog) {
|
ShxDialogButton(primary = true, onClick = ::cancelDialog) {
|
||||||
Text(stringResource(android.R.string.ok))
|
Text(stringResource(android.R.string.ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
-62
@@ -1,62 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 SHX VPN
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation; either version 2 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.strongswan.android.ui.compose.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.Shape
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import org.strongswan.android.ui.compose.theme.LocalShxStatusColors
|
|
||||||
import org.strongswan.android.ui.compose.theme.ShxSpacing
|
|
||||||
|
|
||||||
/** Small rounded label with explicit colors, e.g. "Connected". */
|
|
||||||
@Composable
|
|
||||||
fun ShxStatusPill(
|
|
||||||
text: String,
|
|
||||||
containerColor: Color,
|
|
||||||
contentColor: Color,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
shape: Shape = CircleShape,
|
|
||||||
style: TextStyle = MaterialTheme.typography.labelSmall,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Surface(
|
|
||||||
color = containerColor,
|
|
||||||
contentColor = contentColor,
|
|
||||||
shape = shape,
|
|
||||||
modifier = modifier,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
style = style,
|
|
||||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = ShxSpacing.xs),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Pill in the "connected" status palette (independent of dynamic color). */
|
|
||||||
@Composable
|
|
||||||
fun ShxConnectedPill(text: String, modifier: Modifier = Modifier)
|
|
||||||
{
|
|
||||||
val status = LocalShxStatusColors.current
|
|
||||||
ShxStatusPill(
|
|
||||||
text = text,
|
|
||||||
containerColor = status.connectedContainer,
|
|
||||||
contentColor = status.connected,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+10
@@ -73,4 +73,14 @@ object ShxIcons
|
|||||||
val Pin = line("pin", "M12 21s-7-6.1-7-11a7 7 0 0 1 14 0c0 4.9-7 11-7 11zM12 12.5a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5", 2f)
|
val Pin = line("pin", "M12 21s-7-6.1-7-11a7 7 0 0 1 14 0c0 4.9-7 11-7 11zM12 12.5a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5", 2f)
|
||||||
val Stats = line("stats", "M5 20V10M12 20V4M19 20v-7", 2.4f)
|
val Stats = line("stats", "M5 20V10M12 20V4M19 20v-7", 2.4f)
|
||||||
val Clock = line("clock", "M12 7v5l3 2M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18", 2.2f)
|
val Clock = line("clock", "M12 7v5l3 2M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18", 2.2f)
|
||||||
|
val Eye = line("eye", "M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12zM12 9a3 3 0 1 0 0 6a3 3 0 1 0 0-6", 2f)
|
||||||
|
val EyeOff = line("eyeOff", "M3 3l18 18M10.6 5.1A10 10 0 0 1 12 5c6.4 0 10 7 10 7a17 17 0 0 1-3.2 4.2M6.6 6.6C3.9 8.4 2 12 2 12s3.6 7 10 7a9.6 9.6 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2", 2f)
|
||||||
|
val More = line("more", "M12 5h.01M12 12h.01M12 19h.01", 3f)
|
||||||
|
val Battery = line("battery", "M3 8h15v8H3zM21 11v2M6 11v2", 2f)
|
||||||
|
val Bug = line("bug", "M9 9h6v8a3 3 0 0 1-6 0zM9 13H5M19 13h-4M8 5l2 2M16 5l-2 2M10 7h4", 2f)
|
||||||
|
val Edit = line("edit", "M4 20h4l10-10-4-4L4 16zM13 7l4 4", 2f)
|
||||||
|
val ArrowUp = line("arrowUp", "M12 19V5M6 11l6-6 6 6", 2.2f)
|
||||||
|
val ArrowDown = line("arrowDown", "M12 5v14M6 13l6 6 6-6", 2.2f)
|
||||||
|
val Minus = line("minus", "M5 12h14", 2.4f)
|
||||||
|
val Share = line("share", "M18 8a3 3 0 1 0 0-6a3 3 0 0 0 0 6zM6 15a3 3 0 1 0 0-6a3 3 0 0 0 0 6zM18 22a3 3 0 1 0 0-6a3 3 0 0 0 0 6zM8.6 13.5l6.8 4M15.4 6.5l-6.8 4", 2f)
|
||||||
}
|
}
|
||||||
+48
-5
@@ -39,9 +39,6 @@ import androidx.compose.foundation.text.BasicTextField
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
|
||||||
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
@@ -107,7 +104,7 @@ fun ShxPageHeader(title: String, onBack: () -> Unit, modifier: Modifier = Modifi
|
|||||||
modifier = Modifier.size(44.dp).clip(CircleShape).clickable(role = Role.Button, onClick = onBack),
|
modifier = Modifier.size(44.dp).clip(CircleShape).clickable(role = Role.Button, onClick = onBack),
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = stringResource(R.string.back), tint = c.text, modifier = Modifier.size(22.dp))
|
Icon(ShxIcons.Back, contentDescription = stringResource(R.string.back), tint = c.text, modifier = Modifier.size(22.dp))
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
@@ -259,7 +256,7 @@ fun ShxRow(
|
|||||||
trailing?.invoke(this)
|
trailing?.invoke(this)
|
||||||
if (chevron)
|
if (chevron)
|
||||||
{
|
{
|
||||||
Icon(Icons.AutoMirrored.Rounded.KeyboardArrowRight, contentDescription = null, tint = c.textSub, modifier = Modifier.size(20.dp))
|
Icon(ShxIcons.Chevron, contentDescription = null, tint = c.textSub, modifier = Modifier.size(20.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (divider)
|
if (divider)
|
||||||
@@ -875,3 +872,49 @@ private fun DialogScrim()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dialog button of the mockup: the action as a filled 44dp pill (red when
|
||||||
|
* [danger]), «Cancel» and the like as accent text. Same shape of call as
|
||||||
|
* the Material TextButton it replaces.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ShxDialogButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
primary: Boolean = false,
|
||||||
|
danger: Boolean = false,
|
||||||
|
content: @Composable RowScope.() -> Unit,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
val c = LocalShxColors.current
|
||||||
|
val bg = when
|
||||||
|
{
|
||||||
|
!primary -> Color.Transparent
|
||||||
|
danger -> c.error
|
||||||
|
else -> c.accent
|
||||||
|
}
|
||||||
|
val fg = when
|
||||||
|
{
|
||||||
|
!primary -> if (danger) c.error else c.accentText
|
||||||
|
danger -> c.background
|
||||||
|
else -> Color.White
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = modifier.height(44.dp).clip(RoundedCornerShape(22.dp)).background(if (enabled) bg else bg.copy(alpha = 0.4f))
|
||||||
|
.clickable(enabled = enabled, role = Role.Button, onClick = onClick)
|
||||||
|
.padding(horizontal = if (primary) 20.dp else 16.dp),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
androidx.compose.runtime.CompositionLocalProvider(androidx.compose.material3.LocalContentColor provides if (enabled) fg else fg.copy(alpha = 0.5f))
|
||||||
|
{
|
||||||
|
androidx.compose.material3.ProvideTextStyle(if (primary) ShxType.button else ShxType.button.copy(fontWeight = FontWeight.Bold))
|
||||||
|
{
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-185
@@ -1,185 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 SHX VPN
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation; either version 2 of the License, or (at your
|
|
||||||
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
||||||
* for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.strongswan.android.ui.compose.routing
|
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
|
|
||||||
import androidx.compose.material.icons.rounded.Add
|
|
||||||
import androidx.compose.material.icons.rounded.Apps
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
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.draw.clip
|
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import org.strongswan.android.R
|
|
||||||
import org.strongswan.android.ui.compose.components.ShxCard
|
|
||||||
import org.strongswan.android.ui.compose.components.ShxIconBadge
|
|
||||||
import org.strongswan.android.ui.compose.theme.ShxSpacing
|
|
||||||
|
|
||||||
private const val STACK_ICONS = 3
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Apps direct" entry card: counter, a stack of real app icons and a chevron
|
|
||||||
* into the full list. While the list is empty it offers the one-tap preset
|
|
||||||
* for Russian apps instead.
|
|
||||||
*
|
|
||||||
* @param previewPackages a few direct packages for the icon stack (the rest is a "+N" bubble)
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
internal fun DirectAppsCard(
|
|
||||||
count: Int,
|
|
||||||
previewPackages: List<String>,
|
|
||||||
onOpen: () -> Unit,
|
|
||||||
onAddAllRf: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
ShxCard(modifier = modifier.fillMaxWidth())
|
|
||||||
{
|
|
||||||
Column {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = onOpen)
|
|
||||||
.heightIn(min = 72.dp)
|
|
||||||
.padding(ShxSpacing.lg),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
ShxIconBadge(Icons.Rounded.Apps)
|
|
||||||
Column(
|
|
||||||
Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(horizontal = ShxSpacing.lg),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.routing_apps_direct_title),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = if (count > 0)
|
|
||||||
{
|
|
||||||
pluralStringResource(R.plurals.routing_apps_direct_summary, count, count)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
stringResource(R.string.routing_apps_direct_empty)
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (count > 0)
|
|
||||||
{
|
|
||||||
IconStack(previewPackages.take(STACK_ICONS), extra = count - previewPackages.take(STACK_ICONS).size)
|
|
||||||
Spacer(Modifier.width(ShxSpacing.sm))
|
|
||||||
}
|
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Rounded.KeyboardArrowRight,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (count == 0)
|
|
||||||
{
|
|
||||||
FlowRow(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(ShxSpacing.sm),
|
|
||||||
modifier = Modifier.padding(start = 72.dp, end = ShxSpacing.lg, bottom = ShxSpacing.lg),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Button(onClick = onAddAllRf)
|
|
||||||
{
|
|
||||||
Icon(Icons.Rounded.Add, contentDescription = null, modifier = Modifier.size(18.dp))
|
|
||||||
Spacer(Modifier.width(6.dp))
|
|
||||||
Text(stringResource(R.string.routing_apps_add_all_rf))
|
|
||||||
}
|
|
||||||
OutlinedButton(onClick = onOpen)
|
|
||||||
{
|
|
||||||
Text(stringResource(R.string.routing_apps_pick_manually))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Overlapping app icons with a "+N" bubble for the rest. */
|
|
||||||
@Composable
|
|
||||||
private fun IconStack(packages: List<String>, extra: Int)
|
|
||||||
{
|
|
||||||
val ring = MaterialTheme.colorScheme.surfaceContainerLow
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically)
|
|
||||||
{
|
|
||||||
packages.forEachIndexed { index, pkg ->
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.offset(x = (-8 * index).dp)
|
|
||||||
.size(28.dp)
|
|
||||||
.border(BorderStroke(2.dp, ring), CircleShape)
|
|
||||||
.padding(2.dp)
|
|
||||||
.clip(CircleShape),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
AppIcon(pkg, 24.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (extra > 0)
|
|
||||||
{
|
|
||||||
Surface(
|
|
||||||
shape = CircleShape,
|
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier
|
|
||||||
.offset(x = (-8 * packages.size).dp)
|
|
||||||
.heightIn(min = 28.dp),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Text(
|
|
||||||
text = "+$extra",
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 5.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-128
@@ -1,128 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 SHX VPN
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation; either version 2 of the License, or (at your
|
|
||||||
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
||||||
* for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.strongswan.android.ui.compose.routing
|
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
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.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.selection.toggleable
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.AltRoute
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import org.strongswan.android.R
|
|
||||||
import org.strongswan.android.ui.compose.theme.ShxSpacing
|
|
||||||
|
|
||||||
/** Plain-words summary of what currently bypasses the VPN. */
|
|
||||||
@Composable
|
|
||||||
internal fun routingSummary(enabled: Boolean, ruEnabled: Boolean, appCount: Int, ruleCount: Int = 0): String
|
|
||||||
{
|
|
||||||
if (!enabled)
|
|
||||||
{
|
|
||||||
return stringResource(R.string.routing_hero_off_summary)
|
|
||||||
}
|
|
||||||
val parts = ArrayList<String>()
|
|
||||||
if (ruEnabled)
|
|
||||||
{
|
|
||||||
parts.add(stringResource(R.string.routing_hero_part_sites))
|
|
||||||
}
|
|
||||||
if (appCount > 0)
|
|
||||||
{
|
|
||||||
parts.add(pluralStringResource(R.plurals.routing_apps_n, appCount, appCount))
|
|
||||||
}
|
|
||||||
if (ruleCount > 0)
|
|
||||||
{
|
|
||||||
parts.add(pluralStringResource(R.plurals.routing_rules_n, ruleCount, ruleCount))
|
|
||||||
}
|
|
||||||
return if (parts.isEmpty())
|
|
||||||
{
|
|
||||||
stringResource(R.string.routing_hero_nothing)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
stringResource(R.string.routing_hero_summary, parts.joinToString(" · "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Hero card: the master switch plus a one-line summary of what bypasses the VPN. */
|
|
||||||
@Composable
|
|
||||||
internal fun RoutingHeroCard(
|
|
||||||
enabled: Boolean,
|
|
||||||
summary: String,
|
|
||||||
onToggle: (Boolean) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Surface(
|
|
||||||
shape = MaterialTheme.shapes.large,
|
|
||||||
color = if (enabled) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerLow,
|
|
||||||
contentColor = if (enabled) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface,
|
|
||||||
border = if (enabled) null else BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.toggleable(value = enabled, role = Role.Switch, onValueChange = onToggle)
|
|
||||||
.padding(start = ShxSpacing.lg, top = 14.dp, bottom = 14.dp, end = ShxSpacing.lg),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.AltRoute,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = if (enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(ShxSpacing.lg))
|
|
||||||
Column(Modifier.weight(1f))
|
|
||||||
{
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.routing_hero_title),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = summary,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = if (enabled)
|
|
||||||
{
|
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.85f)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.width(ShxSpacing.md))
|
|
||||||
Switch(checked = enabled, onCheckedChange = null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+30
@@ -307,3 +307,33 @@ internal fun RecentAppsCard(apps: List<RecentPreview>, failing: Int, onClick: ()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Plain-words summary of what currently bypasses the VPN. */
|
||||||
|
@Composable
|
||||||
|
internal fun routingSummary(enabled: Boolean, ruEnabled: Boolean, appCount: Int, ruleCount: Int = 0): String
|
||||||
|
{
|
||||||
|
if (!enabled)
|
||||||
|
{
|
||||||
|
return stringResource(R.string.routing_hero_off_summary)
|
||||||
|
}
|
||||||
|
val parts = ArrayList<String>()
|
||||||
|
if (ruEnabled)
|
||||||
|
{
|
||||||
|
parts.add(stringResource(R.string.routing_hero_part_sites))
|
||||||
|
}
|
||||||
|
if (appCount > 0)
|
||||||
|
{
|
||||||
|
parts.add(pluralStringResource(R.plurals.routing_apps_n, appCount, appCount))
|
||||||
|
}
|
||||||
|
if (ruleCount > 0)
|
||||||
|
{
|
||||||
|
parts.add(pluralStringResource(R.plurals.routing_rules_n, ruleCount, ruleCount))
|
||||||
|
}
|
||||||
|
return if (parts.isEmpty())
|
||||||
|
{
|
||||||
|
stringResource(R.string.routing_hero_nothing)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stringResource(R.string.routing_hero_summary, parts.joinToString(" · "))
|
||||||
|
}
|
||||||
|
}
|
||||||
-288
@@ -1,288 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 SHX VPN
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation; either version 2 of the License, or (at your
|
|
||||||
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
||||||
* for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.strongswan.android.ui.compose.routing
|
|
||||||
|
|
||||||
import android.text.format.DateUtils
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.expandVertically
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.shrinkVertically
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.selection.selectable
|
|
||||||
import androidx.compose.foundation.selection.toggleable
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Public
|
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
|
||||||
import androidx.compose.material.icons.rounded.Warning
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.LoadingIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.RadioButton
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import org.strongswan.android.R
|
|
||||||
import org.strongswan.android.logic.routing.RuIpListState
|
|
||||||
import org.strongswan.android.logic.routing.RuIpListStatus
|
|
||||||
import org.strongswan.android.ui.compose.components.ShxCard
|
|
||||||
import org.strongswan.android.ui.compose.components.ShxIconBadge
|
|
||||||
import org.strongswan.android.ui.compose.theme.ShxSpacing
|
|
||||||
import org.strongswan.android.utils.Constants
|
|
||||||
import java.text.NumberFormat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Russian sites direct": the switch, the two ways to do it as selectable
|
|
||||||
* cards (right under the switch), a warning for the flexible mode and the
|
|
||||||
* freshness of the address database with a refresh button.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
internal fun RuSitesCard(
|
|
||||||
state: RuIpListState,
|
|
||||||
onToggle: (Boolean) -> Unit,
|
|
||||||
onModeChange: (String) -> Unit,
|
|
||||||
onRefresh: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
ShxCard(modifier = modifier.fillMaxWidth())
|
|
||||||
{
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
|
||||||
modifier = Modifier.padding(ShxSpacing.lg),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.toggleable(value = state.enabled, role = Role.Switch, onValueChange = onToggle),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
ShxIconBadge(Icons.Rounded.Public)
|
|
||||||
Column(
|
|
||||||
Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(horizontal = ShxSpacing.lg),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.rusplit_toggle_title),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.rusplit_toggle_summary),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Switch(checked = state.enabled, onCheckedChange = null)
|
|
||||||
}
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = state.enabled,
|
|
||||||
enter = expandVertically() + fadeIn(),
|
|
||||||
exit = shrinkVertically() + fadeOut(),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(ShxSpacing.md))
|
|
||||||
{
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.routing_mode_label),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(ShxSpacing.sm),
|
|
||||||
modifier = Modifier.height(IntrinsicSize.Max),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
val stable = state.mode != Constants.RUSPLIT_MODE_USERSPACE
|
|
||||||
ModeCard(
|
|
||||||
title = stringResource(R.string.routing_mode_stable),
|
|
||||||
description = stringResource(R.string.routing_mode_stable_desc),
|
|
||||||
selected = stable,
|
|
||||||
beta = false,
|
|
||||||
onClick = { onModeChange(Constants.RUSPLIT_MODE_SYSTEM) },
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.fillMaxHeight(),
|
|
||||||
)
|
|
||||||
ModeCard(
|
|
||||||
title = stringResource(R.string.routing_mode_flexible),
|
|
||||||
description = stringResource(R.string.routing_mode_flexible_desc),
|
|
||||||
selected = !stable,
|
|
||||||
beta = true,
|
|
||||||
onClick = { onModeChange(Constants.RUSPLIT_MODE_USERSPACE) },
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.fillMaxHeight(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
AnimatedVisibility(visible = state.mode == Constants.RUSPLIT_MODE_USERSPACE)
|
|
||||||
{
|
|
||||||
Surface(
|
|
||||||
shape = MaterialTheme.shapes.medium,
|
|
||||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Row(Modifier.padding(ShxSpacing.md), horizontalArrangement = Arrangement.spacedBy(10.dp))
|
|
||||||
{
|
|
||||||
Icon(Icons.Rounded.Warning, contentDescription = null, modifier = Modifier.size(18.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.routing_mode_flexible_warning),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DatabaseStatusRow(state = state, onRefresh = onRefresh)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ModeCard(
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
selected: Boolean,
|
|
||||||
beta: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Surface(
|
|
||||||
shape = MaterialTheme.shapes.medium,
|
|
||||||
color = if (selected) MaterialTheme.colorScheme.primaryContainer else androidx.compose.ui.graphics.Color.Transparent,
|
|
||||||
contentColor = if (selected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface,
|
|
||||||
border = if (selected)
|
|
||||||
{
|
|
||||||
BorderStroke(2.dp, MaterialTheme.colorScheme.primary)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
|
||||||
},
|
|
||||||
modifier = modifier.selectable(selected = selected, role = Role.RadioButton, onClick = onClick),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Column(Modifier.padding(ShxSpacing.md), verticalArrangement = Arrangement.spacedBy(6.dp))
|
|
||||||
{
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically)
|
|
||||||
{
|
|
||||||
RadioButton(selected = selected, onClick = null, modifier = Modifier.size(20.dp))
|
|
||||||
Spacer(Modifier.width(ShxSpacing.sm))
|
|
||||||
/* FlowRow: on narrow cards the BETA badge drops below the title
|
|
||||||
* as a whole instead of wrapping letter by letter */
|
|
||||||
androidx.compose.foundation.layout.FlowRow(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
|
||||||
itemVerticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Text(text = title, style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
|
||||||
if (beta)
|
|
||||||
{
|
|
||||||
Surface(
|
|
||||||
shape = MaterialTheme.shapes.extraSmall,
|
|
||||||
color = MaterialTheme.colorScheme.tertiaryContainer,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.routing_mode_beta),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
maxLines = 1,
|
|
||||||
softWrap = false,
|
|
||||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = description,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = if (selected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** "Address database: N networks - updated 3 h ago" with the refresh button. */
|
|
||||||
@OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class)
|
|
||||||
@Composable
|
|
||||||
private fun DatabaseStatusRow(state: RuIpListState, onRefresh: () -> Unit)
|
|
||||||
{
|
|
||||||
val text = if (state.lastUpdatedMs > 0L)
|
|
||||||
{
|
|
||||||
stringResource(
|
|
||||||
R.string.routing_db_status,
|
|
||||||
NumberFormat.getIntegerInstance().format(state.ipv4Ranges + state.ipv6Ranges),
|
|
||||||
DateUtils.getRelativeTimeSpanString(state.lastUpdatedMs).toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
stringResource(R.string.routing_db_never)
|
|
||||||
}
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically)
|
|
||||||
{
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
if (state.status == RuIpListStatus.UPDATING)
|
|
||||||
{
|
|
||||||
LoadingIndicator(Modifier.size(24.dp))
|
|
||||||
Spacer(Modifier.width(ShxSpacing.md))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
IconButton(onClick = onRefresh)
|
|
||||||
{
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.Refresh,
|
|
||||||
contentDescription = stringResource(R.string.routing_db_refresh),
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user