diff --git a/src/frontends/android/app/src/main/AndroidManifest.xml b/src/frontends/android/app/src/main/AndroidManifest.xml
index 4c0b0e565..154446118 100644
--- a/src/frontends/android/app/src/main/AndroidManifest.xml
+++ b/src/frontends/android/app/src/main/AndroidManifest.xml
@@ -133,10 +133,6 @@
android:name=".ui.SettingsActivity"
android:label="@string/pref_title">
-
-
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.kt
index fb9751d9c..6b831f8e8 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.kt
@@ -44,7 +44,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
@@ -58,6 +57,9 @@ 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.theme.LocalShxColors
+import org.strongswan.android.ui.compose.theme.ShxType
+import org.strongswan.android.ui.compose.design.ShxPageHeader
import org.strongswan.android.ui.compose.design.ShxIcons
import org.strongswan.android.ui.compose.components.PredictiveBackScale
import org.strongswan.android.logic.imc.RemediationInstruction
@@ -83,7 +85,7 @@ class RemediationInstructionsActivity : AppCompatActivity()
setContent {
ShxTheme {
- Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
+ Surface(Modifier.fillMaxSize(), color = LocalShxColors.current.background) {
RemediationUi(
instructions = loadInstructions(),
selected = selected,
@@ -130,25 +132,9 @@ private fun RemediationUi(
{
Column(Modifier.fillMaxSize())
{
- TopAppBar(
- title = {
- Text(
- selected?.getTitle() ?: stringResource(R.string.remediation_instructions_title),
- fontWeight = FontWeight.SemiBold,
- maxLines = 1,
- )
- },
- navigationIcon = {
- IconButton(onClick = { if (selected != null) onSelect(null) else onFinish() }) {
- Icon(
- ShxIcons.Back,
- contentDescription = stringResource(R.string.back),
- )
- }
- },
- colors = androidx.compose.material3.TopAppBarDefaults.topAppBarColors(
- containerColor = MaterialTheme.colorScheme.background,
- ),
+ ShxPageHeader(
+ selected?.getTitle() ?: stringResource(R.string.remediation_instructions_title),
+ onBack = { if (selected != null) onSelect(null) else onFinish() },
)
AnimatedContent(
targetState = selected,
@@ -188,14 +174,14 @@ private fun RemediationUi(
.clickable { onSelect(item) }
.padding(ShxSpacing.lg),
) {
- Text(item.getTitle(), style = MaterialTheme.typography.titleMedium)
+ Text(item.getTitle(), style = ShxType.cardTitle)
if (!item.getDescription().isNullOrEmpty())
{
Spacer(Modifier.height(ShxSpacing.xs))
Text(
item.getDescription(),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = ShxType.note,
+ color = LocalShxColors.current.textSub,
)
}
}
@@ -214,15 +200,15 @@ private fun RemediationUi(
) {
Text(
instruction.getDescription(),
- style = MaterialTheme.typography.bodyMedium,
+ style = ShxType.note,
modifier = Modifier.padding(top = ShxSpacing.sm),
)
if (!instruction.getHeader().isNullOrEmpty())
{
Text(
instruction.getHeader(),
- style = MaterialTheme.typography.titleSmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = ShxType.section,
+ color = LocalShxColors.current.textSub,
modifier = Modifier.padding(top = 20.dp, bottom = ShxSpacing.sm),
)
ShxCard {
@@ -230,7 +216,7 @@ private fun RemediationUi(
instruction.getItems().forEachIndexed { index, item ->
Text(
item,
- style = MaterialTheme.typography.bodyLarge,
+ style = ShxType.body,
modifier = Modifier.padding(horizontal = ShxSpacing.lg, vertical = 10.dp),
)
if (index < instruction.getItems().lastIndex)
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/ScenariosActivity.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/ScenariosActivity.kt
index 9091592d4..d6ef6acd2 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/ScenariosActivity.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/ScenariosActivity.kt
@@ -147,6 +147,7 @@ private fun ScenariosScreen(onBack: () -> Unit)
var config by remember { mutableStateOf(auto.currentConfig) }
val scenarios by remember { ScenarioEngine.flow(context) }.collectAsStateWithLifecycle()
var editing by remember { mutableStateOf(null) }
+ var pickRulesServer by remember { mutableStateOf(false) }
var creating by remember { mutableStateOf(false) }
var resumes by remember { mutableIntStateOf(0) }
val owner = LocalLifecycleOwner.current
@@ -208,6 +209,24 @@ private fun ScenariosScreen(onBack: () -> Unit)
{ on -> updateAuto { it.copy(parkOnTrusted = on, connectOnUntrusted = if (on && !it.enabled) true else it.connectOnUntrusted) } }
RuleCard(ShxIcons.Phone, c.accentContainer, c.onAccentContainer, stringResource(R.string.scenarios_rule_untrusted), stringResource(R.string.scenario_do_on), config.enabled && config.connectOnUntrusted, null)
{ on -> updateAuto { it.copy(connectOnUntrusted = on, parkOnTrusted = if (on && !it.enabled) true else it.parkOnTrusted) } }
+ /* the server the two network rules connect (was the old «Trusted networks» screen) */
+ if (config.enabled)
+ {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(18.dp)).background(c.card).border(1.dp, c.cardBorder, RoundedCornerShape(18.dp))
+ .clickable(role = Role.Button) { pickRulesServer = true }.padding(horizontal = 14.dp, vertical = 12.dp),
+ )
+ {
+ Text(stringResource(R.string.scenarios_rules_server), style = ShxType.body.copy(fontWeight = FontWeight.Bold), color = c.text, modifier = Modifier.weight(1f))
+ Text(
+ profiles.firstOrNull { it.getUUID().toString() == config.profileUuid }?.let { it.name ?: it.gateway } ?: stringResource(R.string.scenario_server_auto),
+ style = ShxType.body, color = c.textSub, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false),
+ )
+ Icon(ShxIcons.Chevron, contentDescription = null, tint = c.textSub, modifier = Modifier.size(18.dp))
+ }
+ }
scenarios.forEach { s ->
val (icon, bg, fg) = triggerLook(s.trigger)
RuleCard(icon, bg, fg, ScenarioEngine.describeWhen(context, s), stringResource(ScenarioEngine.actionLabel(s.action)), s.enabled, onClick = { editing = s })
@@ -260,6 +279,37 @@ private fun ScenariosScreen(onBack: () -> Unit)
}
}
+ if (pickRulesServer)
+ {
+ ShxSheet(onDismiss = { pickRulesServer = false })
+ {
+ Text(stringResource(R.string.scenarios_rules_server_pick), style = ShxType.note.copy(fontWeight = FontWeight.Normal, lineHeight = 19.sp), color = c.textSoft, modifier = Modifier.padding(horizontal = 4.dp))
+ ShxPanel()
+ {
+ profiles.forEachIndexed { i, p ->
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp).clickable {
+ updateAuto { it.copy(profileUuid = p.getUUID().toString()) }
+ pickRulesServer = false
+ }.padding(horizontal = 16.dp),
+ )
+ {
+ Text(p.name ?: p.gateway, style = ShxType.rowTitle.copy(fontSize = 15.sp), color = c.text, modifier = Modifier.weight(1f))
+ if (config.profileUuid == p.getUUID().toString())
+ {
+ Icon(ShxIcons.Check, contentDescription = null, tint = c.accent, modifier = Modifier.size(20.dp))
+ }
+ }
+ if (i < profiles.lastIndex)
+ {
+ Box(Modifier.fillMaxWidth().height(1.dp).background(c.tile))
+ }
+ }
+ }
+ }
+ }
+
if (creating || editing != null)
{
ScenarioSheet(
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedNetworksActivity.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedNetworksActivity.kt
deleted file mode 100644
index 945a437d4..000000000
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedNetworksActivity.kt
+++ /dev/null
@@ -1,70 +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 .
- *
- * 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
-
-import android.os.Bundle
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.padding
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.produceState
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import org.strongswan.android.R
-import org.strongswan.android.data.VpnProfile
-import org.strongswan.android.data.VpnProfileSource
-import org.strongswan.android.logic.autoconnect.AutoConnectManager
-import org.strongswan.android.logic.autoconnect.SsidReader
-import org.strongswan.android.ui.compose.SecondaryScaffold
-import org.strongswan.android.ui.compose.TrustedNetworksScreen
-import org.strongswan.android.ui.compose.theme.ShxTheme
-
-class TrustedNetworksActivity : AppCompatActivity()
-{
- override fun onCreate(savedInstanceState: Bundle?)
- {
- super.onCreate(savedInstanceState)
- enableEdgeToEdge()
- AutoConnectManager.getInstance(this).reloadConfig()
- val currentSsid = SsidReader.from(this)
- setContent {
- ShxTheme {
- SecondaryScaffold(title = stringResource(R.string.auto_connect_title), onBack = { finish() }) { padding ->
- Box(Modifier.padding(padding)) {
- /* profile list comes from SQLite, load off the main thread */
- val profiles by produceState>(emptyList()) {
- value = withContext(Dispatchers.IO) {
- val source = VpnProfileSource(applicationContext).open()
- try
- {
- source.allVpnProfiles
- }
- finally
- {
- source.close()
- }
- }
- }
- TrustedNetworksScreen(profiles = profiles, currentSsid = currentSsid)
- }
- }
- }
- }
- }
-}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.kt
index c56f7a9b2..5270ff524 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.kt
@@ -17,9 +17,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material3.ListItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -33,6 +30,28 @@ import androidx.core.graphics.drawable.IconCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.strongswan.android.R
+import org.strongswan.android.ui.compose.theme.ShxType
+import org.strongswan.android.ui.compose.theme.LocalShxColors
+import org.strongswan.android.ui.compose.home.serverCode
+import org.strongswan.android.ui.compose.design.ShxPanel
+import org.strongswan.android.ui.compose.design.ShxIcons
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.Alignment
+import androidx.compose.material3.Icon
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.background
import org.strongswan.android.data.VpnProfile
import org.strongswan.android.data.VpnProfileSource
import org.strongswan.android.ui.compose.SecondaryScaffold
@@ -90,20 +109,40 @@ private fun ProfileSelectScreen(
onSelect: (VpnProfile) -> Unit,
)
{
+ val c = LocalShxColors.current
SecondaryScaffold(title = stringResource(R.string.strongswan_shortcut), onBack = onBack) { padding ->
- if (profiles.isEmpty())
+ Column(Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState()).padding(start = 16.dp, end = 16.dp, bottom = 24.dp))
{
- Text(stringResource(R.string.no_profiles), modifier = Modifier.padding(padding).padding(24.dp))
- }
- else
- {
- LazyColumn(Modifier.fillMaxSize().padding(padding)) {
- items(profiles, key = { it.getUUID().toString() }) { profile ->
- ListItem(
- headlineContent = { Text(profile.name ?: "") },
- supportingContent = { Text(profile.gateway ?: "") },
- modifier = Modifier.clickable { onSelect(profile) },
+ if (profiles.isEmpty())
+ {
+ Text(stringResource(R.string.no_profiles), style = ShxType.body, color = c.textSub, modifier = Modifier.padding(horizontal = 6.dp, vertical = 8.dp))
+ return@Column
+ }
+ /* the servers as on F_Servers: code circle, name, address */
+ ShxPanel()
+ {
+ profiles.forEachIndexed { i, profile ->
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.fillMaxWidth().height(68.dp).clickable(role = Role.Button) { onSelect(profile) }.padding(horizontal = 14.dp),
)
+ {
+ Box(contentAlignment = Alignment.Center, modifier = Modifier.size(40.dp).clip(CircleShape).background(c.tile))
+ {
+ Text(serverCode(profile, null), style = ShxType.chip.copy(fontWeight = FontWeight.ExtraBold), color = c.onAccentContainer)
+ }
+ Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp))
+ {
+ Text(profile.name ?: profile.gateway ?: "", style = ShxType.rowTitle, color = c.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text(profile.gateway ?: "", style = ShxType.meta.copy(fontWeight = FontWeight.Normal), color = c.textSub, maxLines = 1)
+ }
+ Icon(ShxIcons.Chevron, contentDescription = null, tint = c.textSub, modifier = Modifier.size(18.dp))
+ }
+ if (i < profiles.lastIndex)
+ {
+ Box(Modifier.fillMaxWidth().height(1.dp).background(c.tile))
+ }
}
}
}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ConnectivityCheckScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ConnectivityCheckScreen.kt
index 4ec31717e..6b5d27147 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ConnectivityCheckScreen.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ConnectivityCheckScreen.kt
@@ -25,7 +25,6 @@ import androidx.compose.foundation.verticalScroll
import org.strongswan.android.ui.compose.design.ShxAlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
@@ -42,6 +41,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.strongswan.android.R
+import org.strongswan.android.ui.compose.theme.LocalShxColors
+import org.strongswan.android.ui.compose.theme.ShxType
import org.strongswan.android.ui.compose.design.ShxDialogButton
import org.strongswan.android.ui.compose.design.ShxIcons
import org.strongswan.android.ui.compose.components.ShxSection
@@ -149,8 +150,8 @@ fun ConnectivityCheckScreen()
{
Text(
stringResource(R.string.health_check_mode),
- style = MaterialTheme.typography.bodyLarge,
- color = if (config.enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
+ style = ShxType.body,
+ color = if (config.enabled) LocalShxColors.current.text else LocalShxColors.current.textSub,
)
SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth().padding(top = ShxSpacing.sm))
{
@@ -328,8 +329,8 @@ private fun AttemptsRow(
{
Text(
title,
- style = MaterialTheme.typography.bodyLarge,
- color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
+ style = ShxType.body,
+ color = if (enabled) LocalShxColors.current.text else LocalShxColors.current.textSub,
modifier = Modifier.weight(1f),
)
IconButton(onClick = onDecrement, enabled = enabled && attempts > 1) {
@@ -337,7 +338,7 @@ private fun AttemptsRow(
}
Text(
text = attempts.toString(),
- style = MaterialTheme.typography.titleMedium,
+ style = ShxType.cardTitle,
modifier = Modifier.padding(horizontal = ShxSpacing.sm),
)
IconButton(onClick = onIncrement, enabled = enabled && attempts < 5) {
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/HomeScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/HomeScreen.kt
index 019ec7119..4f195695f 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/HomeScreen.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/HomeScreen.kt
@@ -427,7 +427,7 @@ private fun ProfilePickerSheet(
}
ShxSheet(onDismiss = onDismiss)
{
- ShxSheetTitle(stringResource(R.string.home_profile_picker_title))
+ ShxSheetTitle(stringResource(R.string.picker_title))
ShxPanel()
{
ordered.forEachIndexed { index, profile ->
@@ -470,8 +470,8 @@ private fun ProfilePickerSheet(
}
Row(horizontalArrangement = Arrangement.spacedBy(10.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)
+ ShxButton(stringResource(R.string.picker_all), onManage, style = ShxButtonStyle.SOFT, height = 48.dp, modifier = Modifier.weight(1f))
+ ShxButton(stringResource(R.string.picker_add), onAdd, style = ShxButtonStyle.OUTLINE, height = 48.dp, modifier = Modifier.weight(1f), icon = ShxIcons.Plus)
}
}
}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/RoutingScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/RoutingScreen.kt
index d6b9d0398..1b88dfe0c 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/RoutingScreen.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/RoutingScreen.kt
@@ -127,7 +127,7 @@ fun RoutingScreen()
Scaffold(
snackbarHost = { ShxSnackbarHost(snackbarHostState) },
- containerColor = MaterialTheme.colorScheme.background,
+ containerColor = LocalShxColors.current.background,
) { _ ->
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter)
{
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsScreen.kt
index c030a8e6b..8887bd235 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsScreen.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsScreen.kt
@@ -111,7 +111,6 @@ import org.strongswan.android.logic.updates.UpdateManager
import org.strongswan.android.logic.updates.UpdateState
import org.strongswan.android.ui.LogActivity
import org.strongswan.android.ui.TrustedCertificatesActivity
-import org.strongswan.android.ui.TrustedNetworksActivity
import org.strongswan.android.ui.ConnectivityCheckActivity
import org.strongswan.android.ui.VpnProfileImportActivity
import org.strongswan.android.ui.compose.components.ShxSection
@@ -516,8 +515,8 @@ fun SettingsScreen(profiles: List, showTitle: Boolean = true)
if (page == SettingsPage.ROOT || page == SettingsPage.UPDATES) Text(
text = stringResource(R.string.app_version_fmt, versionName),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = ShxType.meta,
+ color = LocalShxColors.current.textSub,
modifier = Modifier
.fillMaxWidth()
.padding(top = ShxSpacing.lg),
@@ -931,7 +930,7 @@ private fun ThemeModeRow()
remember { ShxThemeSettings.load(context) }
Column(Modifier.padding(horizontal = ShxSpacing.lg, vertical = ShxSpacing.md))
{
- Text(stringResource(R.string.pref_theme_title), style = MaterialTheme.typography.bodyLarge)
+ Text(stringResource(R.string.pref_theme_title), style = ShxType.body)
Spacer(Modifier.height(ShxSpacing.sm))
val options = listOf(
ThemeMode.SYSTEM to R.string.theme_system,
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxApp.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxApp.kt
index 269eadeb2..4688f6fb4 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxApp.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxApp.kt
@@ -192,7 +192,7 @@ fun ShxApp(viewModel: HomeViewModel = viewModel())
navigationSuiteItems = {
ShxNavigationSuiteItems(selected = state.tab, onSelect = viewModel::setTab, colors = navItemColors)
},
- containerColor = MaterialTheme.colorScheme.background,
+ containerColor = LocalShxColors.current.background,
navigationSuiteColors = NavigationSuiteDefaults.colors(
navigationBarContainerColor = LocalShxColors.current.tile,
navigationRailContainerColor = LocalShxColors.current.tile,
@@ -200,7 +200,7 @@ fun ShxApp(viewModel: HomeViewModel = viewModel())
) {
Surface(
modifier = Modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.background,
+ color = LocalShxColors.current.background,
) {
Box(Modifier.fillMaxSize())
{
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/TrustedNetworksScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/TrustedNetworksScreen.kt
deleted file mode 100644
index c42a43ec8..000000000
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/TrustedNetworksScreen.kt
+++ /dev/null
@@ -1,456 +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 .
- *
- * 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
-
-import android.Manifest
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.net.Uri
-import android.os.Build
-import android.provider.Settings
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import org.strongswan.android.ui.compose.design.ShxAlertDialog
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.RadioButton
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import androidx.core.content.ContextCompat
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleEventObserver
-import androidx.lifecycle.compose.LocalLifecycleOwner
-import androidx.compose.material3.Button
-import androidx.compose.ui.semantics.heading
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.foundation.layout.Arrangement
-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.ShxSectionDivider
-import org.strongswan.android.ui.compose.components.ShxSettingRow
-import org.strongswan.android.ui.compose.components.ShxSettingToggleRow
-import org.strongswan.android.data.VpnProfile
-import org.strongswan.android.logic.autoconnect.AutoConnectManager
-import org.strongswan.android.ui.compose.theme.ShxSpacing
-
-/**
- * Trusted networks settings: when enabled, the VPN is started on cellular and untrusted
- * Wi-Fi, and auto sessions are stopped on trusted Wi-Fi.
- */
-@Composable
-fun TrustedNetworksScreen(
- profiles: List,
- currentSsid: String?,
-)
-{
- val context = LocalContext.current
- val manager = remember { AutoConnectManager.getInstance(context) }
- var config by remember { mutableStateOf(manager.currentConfig) }
- var showProfilePicker by remember { mutableStateOf(false) }
- var showAddSsid by remember { mutableStateOf(false) }
- var pendingDeleteSsid by remember { mutableStateOf(null) }
- var hasBackgroundLocation by remember { mutableStateOf(isBackgroundLocationGranted(context)) }
- var askedBackground by remember { mutableStateOf(false) }
- val lifecycleOwner = LocalLifecycleOwner.current
-
- fun refreshPermissions()
- {
- hasBackgroundLocation = isBackgroundLocationGranted(context)
- if (isFineLocationGranted(context))
- {
- manager.reloadConfig()
- }
- }
-
- val requestBackground = rememberLauncherForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { refreshPermissions() }
- val requestForeground = rememberLauncherForActivityResult(
- ActivityResultContracts.RequestMultiplePermissions()
- ) {
- refreshPermissions()
- if (isFineLocationGranted(context) && !isBackgroundLocationGranted(context) &&
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
- {
- askedBackground = true
- requestBackground.launch(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
- }
- }
-
- fun requestSsidPermissions()
- {
- if (!isFineLocationGranted(context))
- {
- requestForeground.launch(wifiSsidPermissions())
- return
- }
- if (!isBackgroundLocationGranted(context) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
- {
- if (askedBackground)
- {
- openAppDetails(context)
- return
- }
- askedBackground = true
- requestBackground.launch(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
- }
- }
-
- DisposableEffect(lifecycleOwner)
- {
- val observer = LifecycleEventObserver { _, event ->
- if (event == Lifecycle.Event.ON_RESUME)
- {
- refreshPermissions()
- }
- }
- lifecycleOwner.lifecycle.addObserver(observer)
- onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
- }
- val selectedProfile = profiles.firstOrNull { it.getUUID().toString() == config.profileUuid }
- val enabled = config.enabled
-
- fun update(transform: (AutoConnectManager.Config) -> AutoConnectManager.Config)
- {
- val updated = transform(config)
- manager.updateConfig { updated }
- config = updated
- }
-
- Column(
- Modifier
- .fillMaxSize()
- .verticalScroll(rememberScrollState())
- .padding(bottom = ShxSpacing.xl),
- )
- {
- ShxSection(title = stringResource(R.string.settings_section_general))
- {
- ShxSettingToggleRow(
- icon = ShxIcons.Wifi,
- title = stringResource(R.string.auto_connect_title),
- subtitle = stringResource(R.string.auto_connect_summary),
- checked = enabled,
- onCheckedChange = { checked ->
- /* the permission is requested from the explainer card below, not
- * straight from the toggle */
- update { it.copy(enabled = checked) }
- },
- )
- ShxSectionDivider()
- ShxSettingRow(
- icon = ShxIcons.Globe,
- title = stringResource(R.string.auto_connect_profile),
- subtitle = selectedProfile?.name
- ?: stringResource(R.string.auto_connect_profile_none),
- enabled = enabled,
- onClick = { showProfilePicker = true },
- )
- }
-
- if (enabled && !hasBackgroundLocation)
- {
- Spacer(Modifier.size(ShxSpacing.md))
- Surface(
- shape = MaterialTheme.shapes.large,
- color = MaterialTheme.colorScheme.primaryContainer,
- contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = ShxSpacing.lg),
- )
- {
- Column(
- modifier = Modifier.padding(ShxSpacing.lg),
- verticalArrangement = Arrangement.spacedBy(ShxSpacing.sm),
- )
- {
- Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(ShxSpacing.md))
- {
- Icon(ShxIcons.Wifi, contentDescription = null)
- Text(
- text = stringResource(R.string.auto_connect_location_title),
- style = MaterialTheme.typography.titleSmall,
- modifier = Modifier.semantics { heading() },
- )
- }
- Text(
- text = stringResource(R.string.auto_connect_location_why),
- style = MaterialTheme.typography.bodyMedium,
- )
- Text(
- text = stringResource(R.string.auto_connect_location_banner),
- style = MaterialTheme.typography.bodySmall,
- )
- Button(
- onClick = { requestSsidPermissions() },
- modifier = Modifier.align(Alignment.End),
- ) { Text(stringResource(R.string.auto_connect_location_grant)) }
- }
- }
- }
-
- ShxSection(title = stringResource(R.string.auto_connect_trusted_title))
- {
- Text(
- text = stringResource(R.string.auto_connect_trusted_hint),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = ShxSpacing.lg, vertical = ShxSpacing.sm),
- )
- if (config.trustedSsids.isEmpty())
- {
- Text(
- text = stringResource(R.string.auto_connect_no_networks),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = ShxSpacing.lg, vertical = 14.dp),
- )
- }
- else
- {
- val sortedSsids = remember(config.trustedSsids) { config.trustedSsids.sorted() }
- sortedSsids.forEachIndexed { index, ssid ->
- if (index > 0)
- {
- ShxSectionDivider()
- }
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = ShxSpacing.lg),
- )
- {
- Icon(
- ShxIcons.Wifi,
- contentDescription = null,
- tint = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.size(20.dp),
- )
- Text(
- text = ssid,
- style = MaterialTheme.typography.bodyLarge,
- modifier = Modifier
- .weight(1f)
- .padding(horizontal = ShxSpacing.lg, vertical = 14.dp),
- )
- IconButton(onClick = { pendingDeleteSsid = ssid }) {
- Icon(
- ShxIcons.Trash,
- contentDescription = stringResource(R.string.delete_profile),
- tint = MaterialTheme.colorScheme.error,
- )
- }
- }
- }
- }
- ShxSectionDivider()
- TrustedAddNetworkRow(
- currentSsid = currentSsid,
- onAddCurrent = { ssid -> update { it.copy(trustedSsids = it.trustedSsids + ssid) } },
- onAddManual = { showAddSsid = true },
- )
- }
- }
-
- if (showProfilePicker)
- {
- ShxAlertDialog(
- onDismissRequest = { showProfilePicker = false },
- title = { Text(stringResource(R.string.auto_connect_profile)) },
- text = {
- Column {
- profiles.forEach { profile ->
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- update { it.copy(profileUuid = profile.getUUID().toString()) }
- showProfilePicker = false
- }
- .padding(vertical = 10.dp),
- )
- {
- RadioButton(
- selected = config.profileUuid == profile.getUUID().toString(),
- onClick = null,
- )
- Text(profile.name ?: "", modifier = Modifier.padding(start = ShxSpacing.md))
- }
- }
- }
- },
- confirmButton = {
- TextButton(onClick = { showProfilePicker = false }) {
- Text(stringResource(android.R.string.cancel))
- }
- },
- )
- }
-
- if (showAddSsid)
- {
- var ssid by remember { mutableStateOf("") }
- ShxAlertDialog(
- onDismissRequest = { showAddSsid = false },
- title = { Text(stringResource(R.string.auto_connect_add_network)) },
- text = {
- OutlinedTextField(
- value = ssid,
- onValueChange = { ssid = it },
- singleLine = true,
- label = { Text(stringResource(R.string.auto_connect_ssid_label)) },
- )
- },
- confirmButton = {
- TextButton(
- onClick = {
- val trimmed = ssid.trim()
- if (trimmed.isNotEmpty())
- {
- update { it.copy(trustedSsids = it.trustedSsids + trimmed) }
- }
- showAddSsid = false
- },
- enabled = ssid.isNotBlank(),
- ) { Text(stringResource(R.string.add_profile_cta)) }
- },
- dismissButton = {
- TextButton(onClick = { showAddSsid = false }) {
- Text(stringResource(android.R.string.cancel))
- }
- },
- )
- }
-
- pendingDeleteSsid?.let { ssid ->
- ShxAlertDialog(
- onDismissRequest = { pendingDeleteSsid = null },
- title = { Text(stringResource(R.string.auto_connect_remove_network)) },
- text = { Text(ssid) },
- confirmButton = {
- TextButton(onClick = {
- update { it.copy(trustedSsids = it.trustedSsids - ssid) }
- pendingDeleteSsid = null
- }) { Text(stringResource(R.string.delete_profile), color = MaterialTheme.colorScheme.error) }
- },
- dismissButton = {
- TextButton(onClick = { pendingDeleteSsid = null }) {
- Text(stringResource(android.R.string.cancel))
- }
- },
- )
- }
-}
-
-@Composable
-private fun TrustedAddNetworkRow(
- currentSsid: String?,
- onAddCurrent: (String) -> Unit,
- onAddManual: () -> Unit,
-)
-{
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = androidx.compose.foundation.layout.Arrangement.Center,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = ShxSpacing.lg, vertical = 6.dp),
- )
- {
- if (currentSsid != null)
- {
- TextButton(onClick = { onAddCurrent(currentSsid) })
- {
- Icon(ShxIcons.Plus, contentDescription = null, modifier = Modifier.size(18.dp))
- Spacer(Modifier.size(ShxSpacing.sm))
- Text(stringResource(R.string.auto_connect_add_current, currentSsid))
- }
- }
- else
- {
- TextButton(onClick = onAddManual)
- {
- Icon(ShxIcons.Plus, contentDescription = null, modifier = Modifier.size(18.dp))
- Spacer(Modifier.size(ShxSpacing.sm))
- Text(stringResource(R.string.auto_connect_add_network))
- }
- }
- }
-}
-
-private fun isFineLocationGranted(context: Context): Boolean =
- ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
- PackageManager.PERMISSION_GRANTED
-
-private fun isBackgroundLocationGranted(context: Context): Boolean
-{
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
- {
- return true
- }
- return ContextCompat.checkSelfPermission(
- context, Manifest.permission.ACCESS_BACKGROUND_LOCATION
- ) == PackageManager.PERMISSION_GRANTED
-}
-
-private fun wifiSsidPermissions(): Array = buildList {
- add(Manifest.permission.ACCESS_FINE_LOCATION)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
- {
- add(Manifest.permission.NEARBY_WIFI_DEVICES)
- }
-}.toTypedArray()
-
-private fun openAppDetails(context: Context)
-{
- context.startActivity(
- Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
- data = Uri.fromParts("package", context.packageName, null)
- }
- )
-}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/VpnDialogs.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/VpnDialogs.kt
index 5e40f76af..cf95d92a4 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/VpnDialogs.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/VpnDialogs.kt
@@ -23,7 +23,6 @@ import org.strongswan.android.ui.compose.design.ShxAlertDialog
import org.strongswan.android.ui.compose.design.ShxFormField
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -185,7 +184,7 @@ private fun ConfirmationUi(dialog: VpnDialog.Confirmation)
}) {
Text(
stringResource(R.string.disconnect),
- color = MaterialTheme.colorScheme.error,
+ color = LocalShxColors.current.error,
)
}
}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxComponents.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxComponents.kt
index 7810991a6..0e8ddca9f 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxComponents.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxComponents.kt
@@ -44,6 +44,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import org.strongswan.android.ui.compose.theme.ShxSpacing
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.heightIn
@@ -103,13 +104,13 @@ private fun RowScope.RowLabels(title: String, subtitle: String?, enabled: Boolea
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.weight(1f)
- .padding(horizontal = 14.dp),
+ .padding(end = 10.dp),
)
{
- Text(title, style = ShxType.rowTitle, color = if (enabled) c.text else c.textSub)
+ Text(title, style = ShxType.rowTitle.copy(fontSize = 15.sp), color = if (enabled) c.text else c.textSub)
if (subtitle != null)
{
- Text(subtitle, style = ShxType.sub, color = c.textSub)
+ Text(subtitle, style = ShxType.meta.copy(fontWeight = FontWeight.Normal, lineHeight = 16.sp), color = c.textSub)
}
}
}
@@ -117,7 +118,7 @@ private fun RowScope.RowLabels(title: String, subtitle: String?, enabled: Boolea
/** Setting row with a chevron; tap opens a target screen or dialog. */
@Composable
fun ShxSettingRow(
- icon: ImageVector,
+ @Suppress("UNUSED_PARAMETER") icon: ImageVector,
title: String,
subtitle: String? = null,
enabled: Boolean = true,
@@ -130,11 +131,11 @@ fun ShxSettingRow(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = enabled, onClick = onClick)
- .heightIn(min = 76.dp)
- .padding(start = 16.dp, end = 14.dp, top = 10.dp, bottom = 10.dp),
+ .heightIn(min = 58.dp)
+ .padding(start = 16.dp, end = 14.dp, top = 8.dp, bottom = 8.dp),
)
{
- ShxIconBadge(icon = icon, tint = if (enabled) c.onAccentContainer else c.textSub)
+ /* the sub-page rows of the mockup carry no icon (the icon stays in the API) */
RowLabels(title, subtitle, enabled)
Icon(ShxIcons.Chevron, contentDescription = null, tint = c.textSub, modifier = Modifier.size(18.dp))
}
@@ -143,7 +144,7 @@ fun ShxSettingRow(
/** Setting row with the mockup switch; the whole row toggles. */
@Composable
fun ShxSettingToggleRow(
- icon: ImageVector,
+ @Suppress("UNUSED_PARAMETER") icon: ImageVector,
title: String,
subtitle: String? = null,
checked: Boolean,
@@ -162,11 +163,11 @@ fun ShxSettingToggleRow(
role = Role.Switch,
onValueChange = onCheckedChange,
)
- .heightIn(min = 76.dp)
- .padding(start = 16.dp, end = 14.dp, top = 10.dp, bottom = 10.dp),
+ .heightIn(min = 58.dp)
+ .padding(start = 16.dp, end = 14.dp, top = 8.dp, bottom = 8.dp),
)
{
- ShxIconBadge(icon = icon, tint = if (enabled) c.onAccentContainer else c.textSub)
+ /* the sub-page rows of the mockup carry no icon (the icon stays in the API) */
RowLabels(title, subtitle, enabled)
ShxSwitch(checked = checked, onCheckedChange = null, enabled = enabled)
}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxSectionCard.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxSectionCard.kt
index 02cfc6224..2006a1494 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxSectionCard.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/components/ShxSectionCard.kt
@@ -9,6 +9,8 @@
package org.strongswan.android.ui.compose.components
+import org.strongswan.android.ui.compose.theme.LocalShxColors
+import org.strongswan.android.ui.compose.theme.ShxType
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@@ -32,7 +34,7 @@ fun ShxSectionCard(
{
Surface(
shape = MaterialTheme.shapes.large,
- color = MaterialTheme.colorScheme.surfaceContainerLow,
+ color = LocalShxColors.current.card,
modifier = modifier.fillMaxWidth(),
) {
Column(Modifier.padding(ShxSpacing.lg), verticalArrangement = Arrangement.spacedBy(ShxSpacing.md))
@@ -41,8 +43,8 @@ fun ShxSectionCard(
{
Text(
text = title.uppercase(),
- style = MaterialTheme.typography.labelMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = ShxType.chip,
+ color = LocalShxColors.current.textSub,
modifier = Modifier.semantics { heading() },
)
}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/routing/RoutingUiUtil.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/routing/RoutingUiUtil.kt
index 992e15df2..dd8f54469 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/routing/RoutingUiUtil.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/routing/RoutingUiUtil.kt
@@ -36,6 +36,7 @@ import androidx.core.graphics.drawable.toBitmap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.strongswan.android.R
+import org.strongswan.android.ui.compose.theme.LocalShxColors
import org.strongswan.android.logic.routing.RoutingPresets
import org.strongswan.android.logic.routing.TemplateMatch
@@ -86,7 +87,7 @@ fun AppIcon(packageName: String, size: Dp, modifier: Modifier = Modifier)
modifier
.size(size)
.clip(MaterialTheme.shapes.small)
- .background(MaterialTheme.colorScheme.surfaceContainerHigh),
+ .background(LocalShxColors.current.tile),
)
}
}
diff --git a/src/frontends/android/app/src/main/res/values-ru/strings.xml b/src/frontends/android/app/src/main/res/values-ru/strings.xml
index cda47a3e2..339bfb16b 100644
--- a/src/frontends/android/app/src/main/res/values-ru/strings.xml
+++ b/src/frontends/android/app/src/main/res/values-ru/strings.xml
@@ -1332,4 +1332,9 @@
%1$d%% через VPN · %2$d%% напрямую
Что такое мост?
Сервер с российским адресом. Когда оператор включает белые списки, обычные серверы недоступны, а мост — работает. Включите «Мост при белых списках» на странице сервера.
+ Сервер для этих правил
+ К нему подключимся, когда правила включают VPN.
+ Выберите сервер
+ Все серверы
+ Добавить
diff --git a/src/frontends/android/app/src/main/res/values/strings.xml b/src/frontends/android/app/src/main/res/values/strings.xml
index 6adf858cd..043afca2c 100644
--- a/src/frontends/android/app/src/main/res/values/strings.xml
+++ b/src/frontends/android/app/src/main/res/values/strings.xml
@@ -1318,4 +1318,9 @@
%1$d%% through the VPN · %2$d%% direct
What is a bridge?
A server with a Russian address. When the operator turns on whitelists, ordinary servers are unreachable while a bridge still works. Turn on «Bridge for whitelists» on the server page.
+ Server for these rules
+ Connect to it when the rules turn the VPN on.
+ Choose a server
+ All servers
+ Add