android: the rest of the UI in the Final 2026 style: setting rows as on the sub pages, kit colours and type everywhere, server picker labels, shortcut picker and remediation screens; the rules server moves to Scenarios, the old Trusted networks screen goes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9ed278b440
commit
a39993075e
17 files changed
+163
-605
No files matched your search
@@ -133,10 +133,6 @@
|
||||
android:name=".ui.SettingsActivity"
|
||||
android:label="@string/pref_title">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.TrustedNetworksActivity"
|
||||
android:label="@string/auto_connect_title">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.ConnectivityCheckActivity"
|
||||
android:label="@string/health_check_title">
|
||||
|
||||
+14
-28
@@ -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)
|
||||
|
||||
+50
@@ -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<Scenario?>(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(
|
||||
|
||||
-70
@@ -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 <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
|
||||
|
||||
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<List<VpnProfile>>(emptyList()) {
|
||||
value = withContext(Dispatchers.IO) {
|
||||
val source = VpnProfileSource(applicationContext).open()
|
||||
try
|
||||
{
|
||||
source.allVpnProfiles
|
||||
}
|
||||
finally
|
||||
{
|
||||
source.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
TrustedNetworksScreen(profiles = profiles, currentSsid = currentSsid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
-14
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -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) {
|
||||
|
||||
+3
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -127,7 +127,7 @@ fun RoutingScreen()
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { ShxSnackbarHost(snackbarHostState) },
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
containerColor = LocalShxColors.current.background,
|
||||
) { _ ->
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter)
|
||||
{
|
||||
|
||||
+3
-4
@@ -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<VpnProfile>, 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,
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
-456
@@ -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 <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
|
||||
|
||||
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<VpnProfile>,
|
||||
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<String?>(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<String> = 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
+1
-2
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-11
@@ -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)
|
||||
}
|
||||
|
||||
+5
-3
@@ -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() },
|
||||
)
|
||||
}
|
||||
|
||||
+2
-1
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1332,4 +1332,9 @@
|
||||
<string name="weekly_text">%1$d%% через VPN · %2$d%% напрямую</string>
|
||||
<string name="faq_q_bridge">Что такое мост?</string>
|
||||
<string name="faq_a_bridge">Сервер с российским адресом. Когда оператор включает белые списки, обычные серверы недоступны, а мост — работает. Включите «Мост при белых списках» на странице сервера.</string>
|
||||
<string name="scenarios_rules_server">Сервер для этих правил</string>
|
||||
<string name="scenarios_rules_server_pick">К нему подключимся, когда правила включают VPN.</string>
|
||||
<string name="picker_title">Выберите сервер</string>
|
||||
<string name="picker_all">Все серверы</string>
|
||||
<string name="picker_add">Добавить</string>
|
||||
</resources>
|
||||
@@ -1318,4 +1318,9 @@
|
||||
<string name="weekly_text">%1$d%% through the VPN · %2$d%% direct</string>
|
||||
<string name="faq_q_bridge">What is a bridge?</string>
|
||||
<string name="faq_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.</string>
|
||||
<string name="scenarios_rules_server">Server for these rules</string>
|
||||
<string name="scenarios_rules_server_pick">Connect to it when the rules turn the VPN on.</string>
|
||||
<string name="picker_title">Choose a server</string>
|
||||
<string name="picker_all">All servers</string>
|
||||
<string name="picker_add">Add</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user