android: Routes rules can be deleted with one tap and undone: each rule row gets a trash button, the rule disappears at once and is really removed when the «Undo» snackbar goes away or the page is left (so undoing a package pattern does not reconnect the tunnel twice); tapping a rule opens a sheet with its on/off switch, the direction (direct / through the VPN, changed in place keeping the id) and delete; rows say «single address», «network», the number of resolved addresses or matching apps, long values wrap; the Routes tab shows what the rules do («1 via VPN · 2 direct · 1 off») and the hero no longer counts switched-off rules; mockup F_Rules, F_RuleEdit, F_RuleDeleted, F_Routing and their dark pairs
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
26b4edee66
commit
25754b5cc4
22 files changed
+755
-134
No files matched your search
+21
@@ -509,6 +509,12 @@ class RoutingManager private constructor(private val context: Context)
|
||||
}
|
||||
}
|
||||
|
||||
/** Sends an existing site or address rule the other way, keeping its id and on/off state. */
|
||||
fun setRuleDirect(id: String, direct: Boolean)
|
||||
{
|
||||
updateConfigNoReconnect { withRuleDirect(it, id, direct) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the site rules again and pushes the result to the engine.
|
||||
* Called when a rule is added and when the tunnel comes up; repeats every
|
||||
@@ -852,6 +858,21 @@ class RoutingManager private constructor(private val context: Context)
|
||||
/** Config schema: 1 = rule sets, 2 = direct apps + categories + patterns. */
|
||||
const val CONFIG_VERSION = 2
|
||||
|
||||
/** [config] with the site or address rule [id] routed [direct] (or through the VPN). */
|
||||
fun withRuleDirect(config: RoutingConfig, id: String, direct: Boolean): RoutingConfig
|
||||
{
|
||||
val (from, to) = if (direct) config.forceTunnelPrefixes to config.directPrefixes else config.directPrefixes to config.forceTunnelPrefixes
|
||||
val moved = from.firstOrNull { it.id == id }
|
||||
val newFrom = from.filter { it.id != id }
|
||||
/* the same address may already be on the other side: the moved entry replaces it */
|
||||
val newTo = if (moved == null) to else to.filter { it.prefix != moved.prefix } + moved
|
||||
return config.copy(
|
||||
siteRules = config.siteRules.map { if (it.id == id) it.copy(direct = direct) else it },
|
||||
forceTunnelPrefixes = if (direct) newFrom else newTo,
|
||||
directPrefixes = if (direct) newTo else newFrom,
|
||||
)
|
||||
}
|
||||
|
||||
/** Validates and canonicalizes a CIDR/address; null if invalid. */
|
||||
fun normalizePrefix(cidr: String): String? = try
|
||||
{
|
||||
|
||||
+9
-7
@@ -109,7 +109,9 @@ import org.strongswan.android.ui.compose.components.ShxSnackbarHost
|
||||
import org.strongswan.android.ui.compose.routing.DeadHostsPage
|
||||
import org.strongswan.android.ui.compose.routing.DirectAppsScreen
|
||||
import org.strongswan.android.ui.compose.routing.RecentAppsPage
|
||||
import org.strongswan.android.ui.compose.routing.RuleRow
|
||||
import org.strongswan.android.ui.compose.routing.RulesPage
|
||||
import org.strongswan.android.ui.compose.routing.buildRuleRows
|
||||
import org.strongswan.android.ui.compose.routing.routingSummary
|
||||
import org.strongswan.android.ui.compose.theme.LocalShxStatusColors
|
||||
import org.strongswan.android.ui.compose.theme.ShxSpacing
|
||||
@@ -259,14 +261,14 @@ private fun RoutingPageContent(
|
||||
snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
}
|
||||
val ruleCount = config.siteRules.size + config.forceTunnelPrefixes.size + config.directPrefixes.size + config.packagePatterns.size
|
||||
/* the hero says what bypasses the VPN: only the "direct" rules count there */
|
||||
val directRuleCount = config.siteRules.count { it.direct } + config.directPrefixes.size + config.packagePatterns.size
|
||||
val rules = remember(config) { buildRuleRows(config, emptyMap()) }
|
||||
/* the hero says what bypasses the VPN: only the working "direct" rules count there */
|
||||
val directRuleCount = rules.count { it.direct && it.enabled }
|
||||
|
||||
when (page)
|
||||
{
|
||||
RoutingPage.APPS -> DirectAppsScreen(onBack = onBack)
|
||||
RoutingPage.RULES -> RulesPage(onBack = onBack)
|
||||
RoutingPage.RULES -> RulesPage(onBack = onBack, snackbarHostState = snackbarHostState)
|
||||
RoutingPage.RECENT -> RecentAppsPage(
|
||||
deadTargets = deadTargets,
|
||||
onForceTunnel = { dst -> forceTunnel(manager, dst) },
|
||||
@@ -282,7 +284,7 @@ private fun RoutingPageContent(
|
||||
enabled = config.enabled,
|
||||
ruState = ruState,
|
||||
directPackages = directPackages,
|
||||
ruleCount = ruleCount,
|
||||
rules = rules,
|
||||
directRuleCount = directRuleCount,
|
||||
deadCount = deadTargets.count { !it.viaTunnel },
|
||||
split = split,
|
||||
@@ -309,7 +311,7 @@ private fun MainPage(
|
||||
enabled: Boolean,
|
||||
ruState: org.strongswan.android.logic.routing.RuIpListState,
|
||||
directPackages: List<String>,
|
||||
ruleCount: Int,
|
||||
rules: List<RuleRow>,
|
||||
directRuleCount: Int,
|
||||
deadCount: Int,
|
||||
split: Pair<Long, Long>?,
|
||||
@@ -350,7 +352,7 @@ private fun MainPage(
|
||||
ShxSectionHeader(stringResource(R.string.routing_section_exceptions))
|
||||
ExceptionsPanel(
|
||||
directPackages = directPackages,
|
||||
ruleCount = ruleCount,
|
||||
rules = rules,
|
||||
onApps = onOpenApps,
|
||||
onRules = { onOpen(RoutingPage.RULES) },
|
||||
)
|
||||
|
||||
+17
-4
@@ -174,7 +174,7 @@ internal fun RuSitesPanel(state: RuIpListState, onToggle: (Boolean) -> Unit, onM
|
||||
|
||||
/** «Exceptions» card: direct apps (with an icon stack) and rules (with a count). */
|
||||
@Composable
|
||||
internal fun ExceptionsPanel(directPackages: List<String>, ruleCount: Int, onApps: () -> Unit, onRules: () -> Unit)
|
||||
internal fun ExceptionsPanel(directPackages: List<String>, rules: List<RuleRow>, onApps: () -> Unit, onRules: () -> Unit)
|
||||
{
|
||||
val c = LocalShxColors.current
|
||||
ShxPanel()
|
||||
@@ -235,17 +235,30 @@ internal fun ExceptionsPanel(directPackages: List<String>, ruleCount: Int, onApp
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp))
|
||||
{
|
||||
Text(stringResource(R.string.rules_title), style = ShxType.rowTitle, color = c.text)
|
||||
Text(stringResource(R.string.routing_rules_sub), style = ShxType.sub, color = c.textSub)
|
||||
Text(rulesBreakdown(rules), style = ShxType.sub, color = c.textSub)
|
||||
}
|
||||
if (ruleCount > 0)
|
||||
if (rules.isNotEmpty())
|
||||
{
|
||||
Text(ruleCount.toString(), style = ShxType.body.copy(fontWeight = FontWeight.Bold), color = c.textSub)
|
||||
Text(rules.size.toString(), style = ShxType.body.copy(fontWeight = FontWeight.Bold), color = c.textSub)
|
||||
}
|
||||
Icon(ShxIcons.Chevron, contentDescription = null, tint = c.textSub, modifier = Modifier.size(18.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** "2 via VPN · 3 direct · 1 off": what the user rules do right now. */
|
||||
@Composable
|
||||
private fun rulesBreakdown(rules: List<RuleRow>): String
|
||||
{
|
||||
if (rules.isEmpty())
|
||||
{
|
||||
return stringResource(R.string.routing_rules_sub)
|
||||
}
|
||||
val on = stringResource(R.string.routing_rules_breakdown, rules.count { it.enabled && !it.direct }, rules.count { it.enabled && it.direct })
|
||||
val off = rules.count { !it.enabled }
|
||||
return if (off > 0) stringResource(R.string.routing_rules_breakdown_off, on, off) else on
|
||||
}
|
||||
|
||||
/** One app of the «Recent apps» preview. */
|
||||
data class RecentPreview(val packageName: String, val label: String, val direct: Boolean, val failing: Boolean)
|
||||
|
||||
|
||||
+201
-56
@@ -17,7 +17,22 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.strongswan.android.logic.routing.PackagePatterns
|
||||
import org.strongswan.android.ui.compose.design.ShxTag
|
||||
import org.strongswan.android.ui.compose.design.ShxToggleRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
@@ -105,18 +120,54 @@ fun buildRuleRows(config: RoutingConfig, resolved: Map<String, List<String>>): L
|
||||
|
||||
/** «Rules» (F_Rules / F_RulesEmpty): sites, addresses and package templates that override the RU base. */
|
||||
@Composable
|
||||
internal fun RulesPage(onBack: () -> Unit)
|
||||
internal fun RulesPage(onBack: () -> Unit, snackbarHostState: SnackbarHostState)
|
||||
{
|
||||
val context = LocalContext.current
|
||||
val c = LocalShxColors.current
|
||||
val manager = remember { RoutingManager.getInstance(context) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val state by manager.state.collectAsStateWithLifecycle()
|
||||
val resolved by manager.resolvedSites.collectAsStateWithLifecycle()
|
||||
val rows = remember(state.config, resolved) { buildRuleRows(state.config, resolved) }
|
||||
/* a deleted rule only disappears from the list at first; it is really removed when the
|
||||
* «Undo» snackbar goes away or the page is left, so undoing a package pattern does not
|
||||
* reconnect the tunnel twice */
|
||||
var pending by remember { mutableStateOf<RuleRow?>(null) }
|
||||
val pendingNow = rememberUpdatedState(pending)
|
||||
DisposableEffect(Unit) { onDispose { pendingNow.value?.let { delete(manager, it) } } }
|
||||
val rows = remember(state.config, resolved, pending) {
|
||||
buildRuleRows(state.config, resolved).filterNot { it.sameRule(pending) }
|
||||
}
|
||||
val installed by produceState(emptySet<String>()) {
|
||||
value = withContext(Dispatchers.IO) { RoutingManager.loadInternetPackages(context.packageManager) }
|
||||
}
|
||||
var filter by rememberSaveable { mutableStateOf<RuleKind?>(null) }
|
||||
var adding by remember { mutableStateOf(false) }
|
||||
var actionsFor by remember { mutableStateOf<RuleRow?>(null) }
|
||||
val rulesActive = remember { CharonVpnService.isUserspaceRoutingEnabled(context) }
|
||||
val fabLift by animateDpAsState(if (snackbarHostState.currentSnackbarData != null) 96.dp else 24.dp, label = "fabLift")
|
||||
|
||||
fun deleteWithUndo(row: RuleRow)
|
||||
{
|
||||
pending?.let { delete(manager, it) }
|
||||
pending = row
|
||||
scope.launch {
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.rules_deleted, row.value),
|
||||
actionLabel = context.getString(R.string.action_undo),
|
||||
duration = SnackbarDuration.Long,
|
||||
)
|
||||
/* a newer deletion has already committed this one */
|
||||
if (row.sameRule(pending))
|
||||
{
|
||||
if (result != SnackbarResult.ActionPerformed)
|
||||
{
|
||||
delete(manager, row)
|
||||
}
|
||||
pending = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.fillMaxSize().background(c.background))
|
||||
{
|
||||
@@ -143,7 +194,11 @@ internal fun RulesPage(onBack: () -> Unit)
|
||||
{
|
||||
ShxChip(stringResource(R.string.rules_filter_all, rows.size), filter == null, { filter = null })
|
||||
RuleKind.entries.forEach { kind ->
|
||||
ShxChip(stringResource(kind.pluralTitleRes()), filter == kind, { filter = kind })
|
||||
val count = rows.count { it.kind == kind }
|
||||
if (count > 0 || filter == kind)
|
||||
{
|
||||
ShxChip(stringResource(kind.pluralTitleRes()) + " · " + count, filter == kind, { filter = kind })
|
||||
}
|
||||
}
|
||||
}
|
||||
val shown = rows.filter { filter == null || it.kind == filter }
|
||||
@@ -165,7 +220,13 @@ internal fun RulesPage(onBack: () -> Unit)
|
||||
ShxPanel()
|
||||
{
|
||||
group.forEachIndexed { i, row ->
|
||||
RuleRowItem(row, onToggle = { toggle(manager, row, it) }, onClick = { actionsFor = row })
|
||||
RuleRowItem(
|
||||
row,
|
||||
apps = if (row.kind == RuleKind.PACKAGE && installed.isNotEmpty()) installed.count { PackagePatterns.matches(row.value, it) } else -1,
|
||||
onToggle = { toggle(manager, row, it) },
|
||||
onClick = { actionsFor = row },
|
||||
onDelete = { deleteWithUndo(row) },
|
||||
)
|
||||
if (i < group.lastIndex)
|
||||
{
|
||||
Box(Modifier.fillMaxWidth().height(1.dp).background(c.divider))
|
||||
@@ -180,33 +241,94 @@ internal fun RulesPage(onBack: () -> Unit)
|
||||
}
|
||||
if (rows.isNotEmpty())
|
||||
{
|
||||
AddRuleFab(Modifier.align(Alignment.BottomEnd).navigationBarsPadding().padding(end = 16.dp, bottom = 24.dp)) { adding = true }
|
||||
AddRuleFab(Modifier.align(Alignment.BottomEnd).navigationBarsPadding().padding(end = 16.dp, bottom = fabLift)) { adding = true }
|
||||
}
|
||||
}
|
||||
if (adding)
|
||||
{
|
||||
RuleAddSheet(manager = manager, rulesActive = rulesActive, onDismiss = { adding = false })
|
||||
}
|
||||
actionsFor?.let { row ->
|
||||
ShxSheet(onDismiss = { actionsFor = null })
|
||||
/* the sheet follows the live rule: its switch and direction change it in place */
|
||||
actionsFor?.let { opened -> rows.firstOrNull { it.sameRule(opened) } }?.let { row ->
|
||||
RuleEditSheet(
|
||||
row = row,
|
||||
addresses = state.config.siteRules.firstOrNull { it.id == row.id }?.let { resolved[it.domain] }.orEmpty(),
|
||||
onToggle = { toggle(manager, row, it) },
|
||||
onDirect = { manager.setRuleDirect(row.id, it) },
|
||||
onDelete = {
|
||||
actionsFor = null
|
||||
deleteWithUndo(row)
|
||||
},
|
||||
onDismiss = { actionsFor = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Same rule, whatever its on/off state and direction are by now. */
|
||||
private fun RuleRow.sameRule(other: RuleRow?) = other != null && kind == other.kind && id == other.id
|
||||
|
||||
/** «Rule» sheet (F_RuleEdit): on/off, direction, delete. */
|
||||
@Composable
|
||||
private fun RuleEditSheet(
|
||||
row: RuleRow,
|
||||
addresses: List<String>,
|
||||
onToggle: (Boolean) -> Unit,
|
||||
onDirect: (Boolean) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
)
|
||||
{
|
||||
val c = LocalShxColors.current
|
||||
val (tagBg, tagFg) = row.kind.tagColors()
|
||||
ShxSheet(onDismiss = onDismiss)
|
||||
{
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.padding(start = 4.dp, end = 4.dp, top = 4.dp))
|
||||
{
|
||||
ShxSheetTitle(row.value)
|
||||
Text(
|
||||
stringResource(if (row.direct) R.string.rules_to_direct else R.string.rules_to_vpn) + " · " + stringResource(row.kind.tagRes()),
|
||||
style = ShxType.note.copy(fontSize = 14.sp),
|
||||
color = c.textSub,
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
)
|
||||
ShxButton(
|
||||
stringResource(R.string.rules_delete),
|
||||
{
|
||||
delete(manager, row)
|
||||
actionsFor = null
|
||||
},
|
||||
style = ShxButtonStyle.DANGER,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
icon = ShxIcons.Trash,
|
||||
)
|
||||
ShxTag(stringResource(row.kind.tagRes()).uppercase(), tagBg, tagFg)
|
||||
Text(row.value, style = ShxType.mono.copy(fontSize = 22.sp, fontWeight = FontWeight.ExtraBold), color = c.text)
|
||||
}
|
||||
ShxPanel()
|
||||
{
|
||||
ShxToggleRow(stringResource(R.string.rules_enabled), row.enabled, onToggle, subtitle = stringResource(R.string.rules_enabled_sub))
|
||||
}
|
||||
if (row.kind != RuleKind.PACKAGE)
|
||||
{
|
||||
Text(stringResource(R.string.rules_where), style = ShxType.sub.copy(fontWeight = FontWeight.Bold), color = c.textSoft, modifier = Modifier.padding(horizontal = 4.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp))
|
||||
{
|
||||
ShxChoiceCard(
|
||||
title = stringResource(R.string.rules_to_direct),
|
||||
subtitle = stringResource(R.string.rules_direct_short),
|
||||
selected = row.direct,
|
||||
onClick = { onDirect(true) },
|
||||
tone = ShxChoiceTone.SUCCESS,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
ShxChoiceCard(
|
||||
title = stringResource(R.string.rules_to_vpn),
|
||||
subtitle = stringResource(if (row.kind == RuleKind.SITE) R.string.rules_vpn_short_site else R.string.rules_vpn_short),
|
||||
selected = !row.direct,
|
||||
onClick = { onDirect(false) },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
ShxNote(
|
||||
when
|
||||
{
|
||||
row.kind == RuleKind.PACKAGE -> stringResource(R.string.rules_package_note)
|
||||
row.kind == RuleKind.ADDRESS -> stringResource(if (row.direct) R.string.rules_direct_note else R.string.rules_vpn_note)
|
||||
addresses.isEmpty() -> stringResource(R.string.rules_site_note)
|
||||
else -> stringResource(
|
||||
R.string.rules_site_now,
|
||||
addresses.take(4).joinToString(", ") { it.substringBefore('/') } + if (addresses.size > 4) ", …" else "",
|
||||
)
|
||||
}
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp))
|
||||
{
|
||||
ShxButton(stringResource(R.string.routing_force_delete), onDelete, style = ShxButtonStyle.DANGER, modifier = Modifier.weight(1f), icon = ShxIcons.Trash)
|
||||
ShxButton(stringResource(R.string.action_done), onDismiss, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,53 +368,76 @@ private fun RuleKind.pluralTitleRes(): Int = when (this)
|
||||
RuleKind.PACKAGE -> R.string.rules_filter_packages
|
||||
}
|
||||
|
||||
/** Row of the mockup: tag, monospace value with a sub line, switch; 64dp. */
|
||||
@Composable
|
||||
private fun RuleRowItem(row: RuleRow, onToggle: (Boolean) -> Unit, onClick: () -> Unit)
|
||||
private fun RuleKind.tagColors(): Pair<Color, Color>
|
||||
{
|
||||
val c = LocalShxColors.current
|
||||
val (tagBg, tagFg) = when (row.kind)
|
||||
return when (this)
|
||||
{
|
||||
RuleKind.SITE -> c.infoContainer to c.onInfo
|
||||
RuleKind.ADDRESS -> c.tile to c.textSoft
|
||||
RuleKind.PACKAGE -> c.successContainer to c.onSuccess
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().height(64.dp).clickable(role = Role.Button, onClick = onClick).padding(horizontal = 14.dp),
|
||||
)
|
||||
}
|
||||
|
||||
/** A single host ("1.2.3.4", "1.2.3.4/32", "::1/128") rather than a network. */
|
||||
internal fun isSingleAddress(prefix: String): Boolean =
|
||||
prefix.substringAfter('/', "") in setOf("", if (prefix.contains(':')) "128" else "32")
|
||||
|
||||
/** Row of the mockup: tag, monospace value with a sub line (opens the rule), switch, delete; 64dp. */
|
||||
@Composable
|
||||
private fun RuleRowItem(row: RuleRow, apps: Int, onToggle: (Boolean) -> Unit, onClick: () -> Unit, onDelete: () -> Unit)
|
||||
{
|
||||
val c = LocalShxColors.current
|
||||
val (tagBg, tagFg) = row.kind.tagColors()
|
||||
val what = when (row.kind)
|
||||
{
|
||||
Text(
|
||||
stringResource(row.kind.tagRes()).uppercase(),
|
||||
style = ShxType.tag.copy(fontSize = 10.sp),
|
||||
color = tagFg,
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(tagBg).padding(horizontal = 7.dp, vertical = 3.dp),
|
||||
RuleKind.SITE -> if (row.addresses >= 0) pluralStringResource(R.plurals.rules_site_addresses, row.addresses, row.addresses)
|
||||
else stringResource(R.string.rules_site_unresolved)
|
||||
RuleKind.ADDRESS -> stringResource(if (isSingleAddress(row.value)) R.string.rules_address_one else R.string.rules_address_net)
|
||||
RuleKind.PACKAGE -> if (apps >= 0) pluralStringResource(R.plurals.routing_apps_n, apps, apps) else null
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(end = 6.dp))
|
||||
{
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.weight(1f).heightIn(min = 64.dp).clickable(role = Role.Button, onClick = onClick).padding(start = 14.dp, end = 10.dp, top = 8.dp, bottom = 8.dp),
|
||||
)
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp))
|
||||
{
|
||||
Text(
|
||||
row.value,
|
||||
style = ShxType.mono.copy(fontWeight = FontWeight.Bold),
|
||||
color = if (row.enabled) c.text else c.textSub,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
when (row.kind)
|
||||
{
|
||||
RuleKind.SITE -> if (row.addresses >= 0) pluralStringResource(R.plurals.rules_site_addresses, row.addresses, row.addresses)
|
||||
else stringResource(R.string.rules_site_unresolved)
|
||||
RuleKind.ADDRESS -> stringResource(if (row.direct) R.string.rules_address_direct else R.string.rules_address_vpn)
|
||||
RuleKind.PACKAGE -> stringResource(R.string.rules_package_sub)
|
||||
},
|
||||
style = ShxType.meta.copy(fontWeight = FontWeight.Normal),
|
||||
color = c.textSub,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
stringResource(row.kind.tagRes()).uppercase(),
|
||||
style = ShxType.tag.copy(fontSize = 10.sp),
|
||||
color = tagFg,
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(tagBg).padding(horizontal = 7.dp, vertical = 3.dp),
|
||||
)
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp))
|
||||
{
|
||||
/* a long address wraps on a narrow screen instead of being cut; "/32" says nothing next to «single address» */
|
||||
Text(
|
||||
if (row.kind == RuleKind.ADDRESS && isSingleAddress(row.value)) row.value.substringBefore('/') else row.value,
|
||||
style = ShxType.mono.copy(fontWeight = FontWeight.Bold),
|
||||
color = if (row.enabled) c.text else c.textSub,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
listOfNotNull(what, if (row.enabled) null else stringResource(R.string.rules_off)).joinToString(" · "),
|
||||
style = ShxType.meta.copy(fontWeight = FontWeight.Normal),
|
||||
color = c.textSub,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
ShxSwitch(checked = row.enabled, onCheckedChange = onToggle)
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.padding(start = 4.dp).size(44.dp).clip(CircleShape).clickable(role = Role.Button, onClick = onDelete),
|
||||
)
|
||||
{
|
||||
Icon(ShxIcons.Trash, contentDescription = stringResource(R.string.rules_delete), tint = c.textSub, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -638,7 +638,7 @@
|
||||
<string name="route_reason_app_default">Nicht bei den direkten Apps: über das VPN.</string>
|
||||
<string name="route_rules_need_flexible">Regeln für Websites und Adressen wirken, wenn „Russische Websites direkt“ im Modus „Flexibel“ an ist.</string>
|
||||
<string name="rules_title">Regeln</string>
|
||||
<string name="rules_hint">Wohin Websites, Adressen und Apps gehen. Eine Regel hat Vorrang vor der russischen Datenbank.</string>
|
||||
<string name="rules_hint">Wohin Websites, Adressen und Apps gehen. Eine Regel hat Vorrang vor der russischen Datenbank. Tippen Sie auf eine Regel, um sie zu ändern.</string>
|
||||
<string name="rules_filter_all">Alle · %1$d</string>
|
||||
<string name="rules_filter_sites">Websites</string>
|
||||
<string name="rules_filter_addresses">Adressen</string>
|
||||
@@ -650,10 +650,14 @@
|
||||
<string name="rules_tag_address">Adresse</string>
|
||||
<string name="rules_tag_package">Paket</string>
|
||||
<string name="rules_site_unresolved">Adressen noch nicht aufgelöst</string>
|
||||
<string name="rules_address_direct">immer direkt</string>
|
||||
<string name="rules_address_vpn">immer über das VPN, auch in der russischen Datenbank</string>
|
||||
<string name="rules_package_sub">Apps mit passendem Paket gehen direkt</string>
|
||||
<string name="rules_address_one">eine Adresse</string>
|
||||
<string name="rules_address_net">Netz</string>
|
||||
<string name="rules_off">aus</string>
|
||||
<string name="rules_delete">Regel löschen</string>
|
||||
<string name="rules_deleted">Regel %1$s gelöscht</string>
|
||||
<string name="rules_enabled">Regel aktiv</string>
|
||||
<string name="rules_enabled_sub">Eine deaktivierte Regel bleibt in der Liste</string>
|
||||
<string name="rules_site_now">Aktuelle Adressen der Website: %1$s. Sie werden beim Verbinden und alle 30 Minuten aktualisiert.</string>
|
||||
<string name="rules_empty_title">Noch keine Regeln, und das ist in Ordnung</string>
|
||||
<string name="rules_empty_text">Russische Websites und die gewählten Apps gehen schon direkt. Eine Regel braucht es nur, wenn eine Website den falschen Weg nimmt.</string>
|
||||
<string name="rules_ideas">Oft hinzugefügt</string>
|
||||
@@ -697,6 +701,8 @@
|
||||
<string name="recent_access_later">Nicht jetzt</string>
|
||||
<string name="routing_section_exceptions">Ausnahmen</string>
|
||||
<string name="routing_rules_sub">Websites, Adressen und Paketvorlagen</string>
|
||||
<string name="routing_rules_breakdown">%1$d über VPN · %2$d direkt</string>
|
||||
<string name="routing_rules_breakdown_off">%1$s · %2$d aus</string>
|
||||
<string name="routing_section_check">Prüfung</string>
|
||||
<string name="routing_check_row">Wie eine Website, IP oder App läuft</string>
|
||||
<string name="routing_recent_sub">Wer das Netz genutzt hat und auf welchem Weg</string>
|
||||
|
||||
@@ -528,7 +528,7 @@
|
||||
<string name="route_reason_app_default">Nie ma wśród aplikacji bezpośrednich: przez VPN.</string>
|
||||
<string name="route_rules_need_flexible">Reguły dla stron i adresów działają, gdy „Rosyjskie strony bezpośrednio” są włączone w trybie „Elastyczny”.</string>
|
||||
<string name="rules_title">Reguły</string>
|
||||
<string name="rules_hint">Dokąd idą strony, adresy i aplikacje. Reguła ma pierwszeństwo przed bazą rosyjską.</string>
|
||||
<string name="rules_hint">Dokąd idą strony, adresy i aplikacje. Reguła ma pierwszeństwo przed bazą rosyjską. Dotknij reguły, aby ją zmienić.</string>
|
||||
<string name="rules_filter_all">Wszystkie · %1$d</string>
|
||||
<string name="rules_filter_sites">Strony</string>
|
||||
<string name="rules_filter_addresses">Adresy</string>
|
||||
@@ -540,10 +540,14 @@
|
||||
<string name="rules_tag_address">Adres</string>
|
||||
<string name="rules_tag_package">Pakiet</string>
|
||||
<string name="rules_site_unresolved">adresy jeszcze nieustalone</string>
|
||||
<string name="rules_address_direct">zawsze bezpośrednio</string>
|
||||
<string name="rules_address_vpn">zawsze przez VPN, nawet jeśli w bazie rosyjskiej</string>
|
||||
<string name="rules_package_sub">aplikacje z takim pakietem — bezpośrednio</string>
|
||||
<string name="rules_address_one">jeden adres</string>
|
||||
<string name="rules_address_net">sieć</string>
|
||||
<string name="rules_off">wyłączona</string>
|
||||
<string name="rules_delete">Usuń regułę</string>
|
||||
<string name="rules_deleted">Reguła %1$s usunięta</string>
|
||||
<string name="rules_enabled">Reguła włączona</string>
|
||||
<string name="rules_enabled_sub">Wyłączona reguła zostaje na liście</string>
|
||||
<string name="rules_site_now">Adresy strony teraz: %1$s. Odświeżane przy połączeniu i co 30 minut.</string>
|
||||
<string name="rules_empty_title">Jeszcze brak reguł — i to w porządku</string>
|
||||
<string name="rules_empty_text">Rosyjskie strony i wybrane aplikacje już idą bezpośrednio. Reguła jest potrzebna tylko, gdy jakaś strona idzie złą drogą.</string>
|
||||
<string name="rules_ideas">Często dodawane</string>
|
||||
@@ -587,6 +591,8 @@
|
||||
<string name="recent_access_later">Nie teraz</string>
|
||||
<string name="routing_section_exceptions">Wyjątki</string>
|
||||
<string name="routing_rules_sub">Strony, adresy i szablony pakietów</string>
|
||||
<string name="routing_rules_breakdown">%1$d przez VPN · %2$d bezpośrednio</string>
|
||||
<string name="routing_rules_breakdown_off">%1$s · %2$d wyłączone</string>
|
||||
<string name="routing_section_check">Sprawdzanie</string>
|
||||
<string name="routing_check_row">Jak idzie strona, IP lub aplikacja</string>
|
||||
<string name="routing_recent_sub">Kto korzystał z sieci i jaką drogą</string>
|
||||
|
||||
@@ -658,7 +658,7 @@
|
||||
<string name="route_rules_need_flexible">Правила для сайтов и адресов работают, когда «Сайты РФ напрямую» включены в режиме «Гибкий».</string>
|
||||
<!-- Routing: rules, recent apps, sections -->
|
||||
<string name="rules_title">Правила</string>
|
||||
<string name="rules_hint">Куда идут сайты, адреса и приложения. Правило важнее базы РФ.</string>
|
||||
<string name="rules_hint">Куда идут сайты, адреса и приложения. Правило важнее базы РФ. Нажмите на правило, чтобы изменить его.</string>
|
||||
<string name="rules_filter_all">Все · %1$d</string>
|
||||
<string name="rules_filter_sites">Сайты</string>
|
||||
<string name="rules_filter_addresses">Адреса</string>
|
||||
@@ -670,10 +670,14 @@
|
||||
<string name="rules_tag_address">Адрес</string>
|
||||
<string name="rules_tag_package">Пакет</string>
|
||||
<string name="rules_site_unresolved">адреса ещё не определены</string>
|
||||
<string name="rules_address_direct">всегда напрямую</string>
|
||||
<string name="rules_address_vpn">всегда через VPN, даже если в базе РФ</string>
|
||||
<string name="rules_package_sub">приложения с таким пакетом — напрямую</string>
|
||||
<string name="rules_address_one">один адрес</string>
|
||||
<string name="rules_address_net">сеть</string>
|
||||
<string name="rules_off">выключено</string>
|
||||
<string name="rules_delete">Удалить правило</string>
|
||||
<string name="rules_deleted">Правило %1$s удалено</string>
|
||||
<string name="rules_enabled">Правило включено</string>
|
||||
<string name="rules_enabled_sub">Выключенное остаётся в списке</string>
|
||||
<string name="rules_site_now">Адреса сайта сейчас: %1$s. Обновляются при подключении и раз в 30 минут.</string>
|
||||
<string name="rules_empty_title">Правил пока нет — и это нормально</string>
|
||||
<string name="rules_empty_text">Сайты РФ и выбранные приложения уже идут напрямую. Правило нужно, только если какой-то сайт идёт не тем путём.</string>
|
||||
<string name="rules_ideas">Часто добавляют</string>
|
||||
@@ -717,6 +721,8 @@
|
||||
<string name="recent_access_later">Не сейчас</string>
|
||||
<string name="routing_section_exceptions">Исключения</string>
|
||||
<string name="routing_rules_sub">Сайты, адреса и шаблоны пакетов</string>
|
||||
<string name="routing_rules_breakdown">%1$d через VPN · %2$d напрямую</string>
|
||||
<string name="routing_rules_breakdown_off">%1$s · %2$d выключено</string>
|
||||
<string name="routing_section_check">Проверка</string>
|
||||
<string name="routing_check_row">Как ходит сайт, IP или приложение</string>
|
||||
<string name="routing_check_row_sub">Через VPN или напрямую — и почему</string>
|
||||
|
||||
@@ -538,7 +538,7 @@
|
||||
<string name="route_reason_app_default">Не в списку «напряму» — через VPN.</string>
|
||||
<string name="route_rules_need_flexible">Правила для сайтів і адрес працюють, коли «Сайти РФ напряму» увімкнено в режимі «Гнучкий».</string>
|
||||
<string name="rules_title">Правила</string>
|
||||
<string name="rules_hint">Куди йдуть сайти, адреси й застосунки. Правило важливіше за базу РФ.</string>
|
||||
<string name="rules_hint">Куди йдуть сайти, адреси й застосунки. Правило важливіше за базу РФ. Натисніть на правило, щоб змінити його.</string>
|
||||
<string name="rules_filter_all">Усі · %1$d</string>
|
||||
<string name="rules_filter_sites">Сайти</string>
|
||||
<string name="rules_filter_addresses">Адреси</string>
|
||||
@@ -550,10 +550,14 @@
|
||||
<string name="rules_tag_address">Адреса</string>
|
||||
<string name="rules_tag_package">Пакет</string>
|
||||
<string name="rules_site_unresolved">адреси ще не визначено</string>
|
||||
<string name="rules_address_direct">завжди напряму</string>
|
||||
<string name="rules_address_vpn">завжди через VPN, навіть якщо в базі РФ</string>
|
||||
<string name="rules_package_sub">застосунки з таким пакетом — напряму</string>
|
||||
<string name="rules_address_one">одна адреса</string>
|
||||
<string name="rules_address_net">мережа</string>
|
||||
<string name="rules_off">вимкнено</string>
|
||||
<string name="rules_delete">Видалити правило</string>
|
||||
<string name="rules_deleted">Правило %1$s видалено</string>
|
||||
<string name="rules_enabled">Правило ввімкнено</string>
|
||||
<string name="rules_enabled_sub">Вимкнене залишається у списку</string>
|
||||
<string name="rules_site_now">Адреси сайту зараз: %1$s. Оновлюються під час підключення та кожні 30 хвилин.</string>
|
||||
<string name="rules_empty_title">Правил поки немає — і це нормально</string>
|
||||
<string name="rules_empty_text">Сайти РФ і вибрані застосунки вже йдуть напряму. Правило потрібне, лише якщо якийсь сайт іде не тим шляхом.</string>
|
||||
<string name="rules_ideas">Часто додають</string>
|
||||
@@ -597,6 +601,8 @@
|
||||
<string name="recent_access_later">Не зараз</string>
|
||||
<string name="routing_section_exceptions">Винятки</string>
|
||||
<string name="routing_rules_sub">Сайти, адреси та шаблони пакетів</string>
|
||||
<string name="routing_rules_breakdown">%1$d через VPN · %2$d напряму</string>
|
||||
<string name="routing_rules_breakdown_off">%1$s · %2$d вимкнено</string>
|
||||
<string name="routing_section_check">Перевірка</string>
|
||||
<string name="routing_check_row">Як іде сайт, IP або застосунок</string>
|
||||
<string name="routing_recent_sub">Хто виходив у мережу і яким шляхом</string>
|
||||
|
||||
@@ -537,7 +537,7 @@
|
||||
<string name="route_reason_app_default">不在直连应用中:经 VPN。</string>
|
||||
<string name="route_rules_need_flexible">网站和地址规则仅在“灵活”模式下开启“俄罗斯网站直连”时生效。</string>
|
||||
<string name="rules_title">规则</string>
|
||||
<string name="rules_hint">网站、地址和应用的去向。规则优先于俄罗斯地址库。</string>
|
||||
<string name="rules_hint">网站、地址和应用的去向。规则优先于俄罗斯地址库。点按规则即可修改。</string>
|
||||
<string name="rules_filter_all">全部 · %1$d</string>
|
||||
<string name="rules_filter_sites">网站</string>
|
||||
<string name="rules_filter_addresses">地址</string>
|
||||
@@ -549,10 +549,14 @@
|
||||
<string name="rules_tag_address">地址</string>
|
||||
<string name="rules_tag_package">包名</string>
|
||||
<string name="rules_site_unresolved">地址尚未解析</string>
|
||||
<string name="rules_address_direct">始终直连</string>
|
||||
<string name="rules_address_vpn">始终经 VPN,即使在俄罗斯地址库中</string>
|
||||
<string name="rules_package_sub">包名匹配的应用直连</string>
|
||||
<string name="rules_address_one">单个地址</string>
|
||||
<string name="rules_address_net">网段</string>
|
||||
<string name="rules_off">已关闭</string>
|
||||
<string name="rules_delete">删除规则</string>
|
||||
<string name="rules_deleted">规则 %1$s 已删除</string>
|
||||
<string name="rules_enabled">规则已启用</string>
|
||||
<string name="rules_enabled_sub">关闭的规则仍保留在列表中</string>
|
||||
<string name="rules_site_now">网站当前地址:%1$s。连接时及每 30 分钟刷新一次。</string>
|
||||
<string name="rules_empty_title">暂无规则,这很正常</string>
|
||||
<string name="rules_empty_text">俄罗斯网站和所选应用已经直连。只有某个网站走错路径时才需要规则。</string>
|
||||
<string name="rules_ideas">常用</string>
|
||||
@@ -596,6 +600,8 @@
|
||||
<string name="recent_access_later">暂不</string>
|
||||
<string name="routing_section_exceptions">例外</string>
|
||||
<string name="routing_rules_sub">网站、地址和包名模板</string>
|
||||
<string name="routing_rules_breakdown">%1$d 条经 VPN · %2$d 条直连</string>
|
||||
<string name="routing_rules_breakdown_off">%1$s · %2$d 条已关闭</string>
|
||||
<string name="routing_section_check">检查</string>
|
||||
<string name="routing_check_row">网站、IP 或应用的走向</string>
|
||||
<string name="routing_recent_sub">谁联网过以及走哪条路径</string>
|
||||
|
||||
@@ -522,7 +522,7 @@
|
||||
<string name="route_reason_app_default">不在直連應用程式中:經 VPN。</string>
|
||||
<string name="route_rules_need_flexible">網站與位址規則僅在「靈活」模式下開啟「俄羅斯網站直連」時生效。</string>
|
||||
<string name="rules_title">規則</string>
|
||||
<string name="rules_hint">網站、位址和應用程式的去向。規則優先於俄羅斯位址庫。</string>
|
||||
<string name="rules_hint">網站、位址和應用程式的去向。規則優先於俄羅斯位址庫。輕觸規則即可修改。</string>
|
||||
<string name="rules_filter_all">全部 · %1$d</string>
|
||||
<string name="rules_filter_sites">網站</string>
|
||||
<string name="rules_filter_addresses">位址</string>
|
||||
@@ -534,10 +534,14 @@
|
||||
<string name="rules_tag_address">位址</string>
|
||||
<string name="rules_tag_package">套件</string>
|
||||
<string name="rules_site_unresolved">位址尚未解析</string>
|
||||
<string name="rules_address_direct">一律直連</string>
|
||||
<string name="rules_address_vpn">一律經 VPN,即使在俄羅斯位址庫中</string>
|
||||
<string name="rules_package_sub">套件名稱相符的應用程式直連</string>
|
||||
<string name="rules_address_one">單一位址</string>
|
||||
<string name="rules_address_net">網段</string>
|
||||
<string name="rules_off">已關閉</string>
|
||||
<string name="rules_delete">刪除規則</string>
|
||||
<string name="rules_deleted">規則 %1$s 已刪除</string>
|
||||
<string name="rules_enabled">規則已啟用</string>
|
||||
<string name="rules_enabled_sub">關閉的規則仍保留在清單中</string>
|
||||
<string name="rules_site_now">網站目前位址:%1$s。連線時及每 30 分鐘重新整理一次。</string>
|
||||
<string name="rules_empty_title">暫無規則,這很正常</string>
|
||||
<string name="rules_empty_text">俄羅斯網站與所選應用程式已經直連。只有某個網站走錯路徑時才需要規則。</string>
|
||||
<string name="rules_ideas">常用</string>
|
||||
@@ -581,6 +585,8 @@
|
||||
<string name="recent_access_later">暫不</string>
|
||||
<string name="routing_section_exceptions">例外</string>
|
||||
<string name="routing_rules_sub">網站、位址與套件範本</string>
|
||||
<string name="routing_rules_breakdown">%1$d 條經 VPN · %2$d 條直連</string>
|
||||
<string name="routing_rules_breakdown_off">%1$s · %2$d 條已關閉</string>
|
||||
<string name="routing_section_check">檢查</string>
|
||||
<string name="routing_check_row">網站、IP 或應用程式的走向</string>
|
||||
<string name="routing_recent_sub">誰連網過以及走哪條路徑</string>
|
||||
|
||||
@@ -655,7 +655,7 @@
|
||||
<string name="route_rules_need_flexible">Rules for sites and addresses work when «Russian sites direct» is on in the «Flexible» mode.</string>
|
||||
<!-- Routing: rules, recent apps, sections -->
|
||||
<string name="rules_title">Rules</string>
|
||||
<string name="rules_hint">Where sites, addresses and apps go. A rule wins over the RU base.</string>
|
||||
<string name="rules_hint">Where sites, addresses and apps go. A rule wins over the RU base. Tap a rule to change it.</string>
|
||||
<string name="rules_filter_all">All · %1$d</string>
|
||||
<string name="rules_filter_sites">Sites</string>
|
||||
<string name="rules_filter_addresses">Addresses</string>
|
||||
@@ -667,10 +667,14 @@
|
||||
<string name="rules_tag_address">Address</string>
|
||||
<string name="rules_tag_package">Package</string>
|
||||
<string name="rules_site_unresolved">addresses not resolved yet</string>
|
||||
<string name="rules_address_direct">always direct</string>
|
||||
<string name="rules_address_vpn">always through the VPN, even inside the RU base</string>
|
||||
<string name="rules_package_sub">apps whose package matches go direct</string>
|
||||
<string name="rules_address_one">single address</string>
|
||||
<string name="rules_address_net">network</string>
|
||||
<string name="rules_off">off</string>
|
||||
<string name="rules_delete">Delete rule</string>
|
||||
<string name="rules_deleted">Rule %1$s deleted</string>
|
||||
<string name="rules_enabled">Rule enabled</string>
|
||||
<string name="rules_enabled_sub">A disabled rule stays in the list</string>
|
||||
<string name="rules_site_now">Site addresses now: %1$s. They are refreshed on connect and every 30 minutes.</string>
|
||||
<string name="rules_empty_title">No rules yet, and that is fine</string>
|
||||
<string name="rules_empty_text">Russian sites and the selected apps already go direct. A rule is only needed when some site takes the wrong way.</string>
|
||||
<string name="rules_ideas">Often added</string>
|
||||
@@ -714,6 +718,8 @@
|
||||
<string name="recent_access_later">Not now</string>
|
||||
<string name="routing_section_exceptions">Exceptions</string>
|
||||
<string name="routing_rules_sub">Sites, addresses and package templates</string>
|
||||
<string name="routing_rules_breakdown">%1$d via VPN · %2$d direct</string>
|
||||
<string name="routing_rules_breakdown_off">%1$s · %2$d off</string>
|
||||
<string name="routing_section_check">Check</string>
|
||||
<string name="routing_check_row">How a site, IP or app goes</string>
|
||||
<string name="routing_check_row_sub">Through the VPN or direct, and why</string>
|
||||
|
||||
+26
@@ -20,6 +20,7 @@ import org.strongswan.android.ui.compose.DeadTargetUi
|
||||
import org.strongswan.android.ui.compose.routing.RuleKind
|
||||
import org.strongswan.android.ui.compose.routing.buildRecentApps
|
||||
import org.strongswan.android.ui.compose.routing.buildRuleRows
|
||||
import org.strongswan.android.ui.compose.routing.isSingleAddress
|
||||
import org.strongswan.android.utils.IPRange
|
||||
import java.net.InetAddress
|
||||
|
||||
@@ -82,6 +83,31 @@ class RoutingRulesTest
|
||||
assertThat(rows.first { it.value == "2.2.2.0/24" }.enabled).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ruleChangesDirectionInPlace()
|
||||
{
|
||||
val config = RoutingConfig(
|
||||
forceTunnelPrefixes = listOf(ForceTunnelPrefix("f", "1.1.1.1/32", enabled = false), ForceTunnelPrefix("keep", "3.3.3.3/32")),
|
||||
directPrefixes = listOf(ForceTunnelPrefix("dup", "1.1.1.1/32")),
|
||||
siteRules = listOf(SiteRule("s", "chatgpt.com", direct = false, enabled = false)),
|
||||
)
|
||||
val moved = RoutingManager.withRuleDirect(config, "f", direct = true)
|
||||
assertThat(moved.forceTunnelPrefixes.map { it.id }).containsExactly("keep")
|
||||
/* same id and on/off state; the duplicate on the other side is replaced */
|
||||
assertThat(moved.directPrefixes).containsExactly(ForceTunnelPrefix("f", "1.1.1.1/32", enabled = false))
|
||||
assertThat(RoutingManager.withRuleDirect(moved, "f", direct = false).forceTunnelPrefixes.map { it.id }).containsExactly("keep", "f")
|
||||
assertThat(RoutingManager.withRuleDirect(config, "s", direct = true).siteRules)
|
||||
.containsExactly(SiteRule("s", "chatgpt.com", direct = true, enabled = false))
|
||||
assertThat(RoutingManager.withRuleDirect(config, "f", direct = false)).isEqualTo(config)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleAddressOrNetwork()
|
||||
{
|
||||
assertThat(listOf("1.2.3.4", "1.2.3.4/32", "2001:db8::1/128").all(::isSingleAddress)).isTrue()
|
||||
assertThat(listOf("10.8.0.0/24", "2001:db8::/32").none(::isSingleAddress)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun recentAppsPutFailuresFirst()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Утверждённый дизайн SHX VPN. Все экраны приложения сделаны строго по нему.
|
||||
|
||||
- **Живой холст (смотреть и править):** https://claude.ai/artifact/UCC88C9hkU3hDU2YwMck1a. Это артефакт типа «Design» в Claude, доступ только у владельца.
|
||||
- **Копия в репозитории:** `final-2026/` — исходники артбордов и раскладка холста (`canvas.json`) на 2026-10-13, версия 2.18.0.
|
||||
- **Копия в репозитории:** `final-2026/` — исходники артбордов и раскладка холста (`canvas.json`) на 2026-10-13, версия 2.18.0; раздел «Правила» (`F_Rules`, `F_RuleEdit`, `F_RuleDeleted`, `F_Routing`) — на 2026-11-10.
|
||||
|
||||
## Как устроены файлы
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
| Префикс | Страница | Что это |
|
||||
|---|---|---|
|
||||
| `O_*` | Обзор | Анализ, фичи по приоритетам, решения по дизайну, карта экранов |
|
||||
| `F_*` | Финал 2026 | 57 артбордов светлой темы — **эталон** |
|
||||
| `D_*` | Финал 2026 · тёмная | 54 артборда тёмной темы, те же экраны |
|
||||
| `F_*` | Финал 2026 | 59 артбордов светлой темы — **эталон** |
|
||||
| `D_*` | Финал 2026 · тёмная | 56 артбордов тёмной темы, те же экраны |
|
||||
| остальные | Исследование | Ранние варианты (Главная 2.0, простой режим, «Для опытных»). Не реализуются, оставлены для истории |
|
||||
|
||||
`D_*` не рисуются вручную: они получаются из `F_*` заменой цветов светлой палитры на тёмную (`LightShxColors` → `DarkShxColors` в `ShxDesign.kt`). Исключение — `D_QsTile`, он правился руками. Когда меняется `F_*`, обновите и парный `D_*`.
|
||||
@@ -45,7 +45,7 @@
|
||||
| `F_Check` | Как ходит трафик | `ui/compose/RouteCheckScreen.kt` |
|
||||
| `F_Routing` | Маршруты | `ui/compose/routing/RoutingHub.kt` |
|
||||
| `F_Apps`, `F_AppPicker` | Приложения напрямую | `ui/compose/routing/DirectAppsScreen.kt` |
|
||||
| `F_Rules`, `F_RulesEmpty`, `F_RuleAdd` | Правила | `ui/compose/routing/RulesScreen.kt` |
|
||||
| `F_Rules`, `F_RulesEmpty`, `F_RuleAdd`, `F_RuleEdit`, `F_RuleDeleted` | Правила | `ui/compose/routing/RulesScreen.kt` |
|
||||
| `F_Recent`, `F_RecentEmpty` | Недавние приложения | `ui/compose/routing/RecentAppsScreen.kt` |
|
||||
| `F_Automation`, `F_ScenarioAdd` | Сценарии | `ui/ScenariosActivity.kt`, `logic/scenarios/Scenarios.kt` |
|
||||
| `F_Settings` | Настройки | `ui/compose/SettingsScreen.kt` |
|
||||
|
||||
@@ -78,9 +78,9 @@ a{color:#B69DFF}a:hover{color:#D0C2FF}
|
||||
</span>
|
||||
<span style="flex: 1; display: flex; flex-direction: column; gap: 2px">
|
||||
<span style="font-size: 16px; font-weight: 700">Правила</span>
|
||||
<span style="font-size: 13px; color: #A9A3BF">Домены, адреса и шаблоны пакетов</span>
|
||||
<span style="font-size: 13px; color: #A9A3BF">2 через VPN · 3 напрямую · 1 выключено</span>
|
||||
</span>
|
||||
<span style="font-size: 14px; font-weight: 700; color: #A9A3BF">5</span>
|
||||
<span style="font-size: 14px; font-weight: 700; color: #A9A3BF">6</span>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#A9A3BF" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Тёмная · Правило удалено — можно отменить</title>
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap">
|
||||
<style>
|
||||
body{margin:0}
|
||||
a{color:#B69DFF}a:hover{color:#D0C2FF}
|
||||
</style>
|
||||
</helmet>
|
||||
<div style="position: relative; width: 390px; height: 900px; box-sizing: border-box; background: #141318; font-family: 'Manrope', system-ui, sans-serif; color: #E5E1E9; overflow: hidden; font-variant-numeric: tabular-nums">
|
||||
<header style="display: flex; align-items: center; gap: 8px; padding: 16px 12px 8px">
|
||||
<a href="D_Routing.dc.html" aria-label="Назад" style="width: 44px; height: 44px; border-radius: 22px; display: flex; align-items: center; justify-content: center; color: #E5E1E9">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 6l-6 6 6 6"/></svg>
|
||||
</a>
|
||||
<h1 style="margin: 0; flex: 1; font-size: 22px; font-weight: 800">Правила</h1>
|
||||
</header>
|
||||
|
||||
<div style="padding: 0 16px; display: flex; flex-direction: column; gap: 12px">
|
||||
<p style="margin: 0 4px; font-size: 14px; line-height: 1.45; color: #CAC4D0">Куда идут сайты, адреса и приложения. Правило важнее базы РФ. Нажмите на правило, чтобы изменить его.</p>
|
||||
|
||||
<div style="display: flex; gap: 8px">
|
||||
<button aria-pressed="true" style="height: 36px; padding: 0 14px; border-radius: 12px; border: 0; background: #3B2C88; color: #FFFFFF; font: 700 13px 'Manrope', sans-serif">Все · 5</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #48454E; background: transparent; color: #E5E1E9; font: 600 13px 'Manrope', sans-serif">Сайты · 1</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #48454E; background: transparent; color: #E5E1E9; font: 600 13px 'Manrope', sans-serif">Адреса · 2</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #48454E; background: transparent; color: #E5E1E9; font: 600 13px 'Manrope', sans-serif">Пакеты · 2</button>
|
||||
</div>
|
||||
|
||||
<sc-for list="{{groups}}" as="g" hint-placeholder-count="2">
|
||||
<section style="display: flex; flex-direction: column; gap: 6px">
|
||||
<h2 style="margin: 6px 6px 0; display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 800; color: #B69DFF; letter-spacing: 0.04em"><span style="width: 8px; height: 8px; border-radius: 4px; background: {{g.dot}}"></span>{{g.title}}</h2>
|
||||
<div style="border-radius: 24px; background: #1F1D24; border: 1px solid #2E2B35; overflow: hidden">
|
||||
<sc-for list="{{g.rows}}" as="r" hint-placeholder-count="3">
|
||||
<div style="display: flex; align-items: center; gap: 12px; padding: 0 6px 0 14px; height: 64px; border-bottom: {{r.divider}}">
|
||||
<a href="D_RuleEdit.dc.html" style="flex: 1; min-width: 0; height: 64px; display: flex; align-items: center; gap: 12px; text-decoration: none; color: {{r.fg}}">
|
||||
<span style="flex: none; padding: 3px 7px; border-radius: 8px; background: {{r.tagBg}}; color: {{r.tagFg}}; font-size: 10px; font-weight: 800">{{r.kind}}</span>
|
||||
<span style="flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0">
|
||||
<span style="font-size: 15px; font-weight: 700; font-family: ui-monospace, 'Cascadia Mono', Menlo, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{{r.value}}</span>
|
||||
<span style="font-size: 12px; color: #A9A3BF; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{{r.sub}}</span>
|
||||
</span>
|
||||
</a>
|
||||
<button aria-pressed="{{r.on}}" aria-label="Включить правило" style="flex: none; width: 52px; height: 32px; border: 0; border-radius: 16px; background: {{r.track}}; padding: 4px; display: flex; justify-content: {{r.align}}"><span style="width: 24px; height: 24px; border-radius: 12px; background: #1F1D24"></span></button>
|
||||
<a href="D_RuleDeleted.dc.html" aria-label="Удалить правило" style="flex: none; margin-left: -8px; width: 44px; height: 44px; border-radius: 22px; display: flex; align-items: center; justify-content: center; color: #A9A3BF">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 7h14M10 11v6M14 11v6M7 7l1 13h8l1-13M9 7V4h6v3"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</section>
|
||||
</sc-for>
|
||||
</div>
|
||||
|
||||
<a href="D_RuleAdd.dc.html" style="position: absolute; right: 16px; bottom: 96px; height: 60px; padding: 0 22px; border: 0; border-radius: 20px; background: #6D4AFF; color: #FFFFFF; font-size: 16px; font-weight: 800; text-decoration: none; display: flex; align-items: center; gap: 10px; box-shadow: 0 6px 18px rgba(109,74,255,0.35)">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Правило
|
||||
</a>
|
||||
<div role="status" style="position: absolute; left: 16px; right: 16px; bottom: 24px; min-height: 56px; box-sizing: border-box; padding: 0 6px 0 16px; border-radius: 16px; background: #E5E1E9; color: #313036; display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; box-shadow: 0 6px 18px rgba(28,27,32,0.25)">
|
||||
<span style="flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">Правило chatgpt.com удалено</span>
|
||||
<a href="D_Rules.dc.html" style="flex: none; height: 44px; padding: 0 12px; border-radius: 22px; display: flex; align-items: center; text-decoration: none; font-size: 14px; font-weight: 800; color: #6D4AFF">Отменить</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props='{"$preview":{"width":390,"height":900}}'>
|
||||
class Component extends DCLogic {
|
||||
renderVals() {
|
||||
var tag = { 'САЙТ': ['#1E2A44', '#A8C7FF'], 'АДРЕС': ['#2B2930', '#CAC4D0'], 'ПАКЕТ': ['#12321F', '#7CE3B0'] };
|
||||
var grp = function (title, dot, rows) {
|
||||
return { title: title, dot: dot, rows: rows.map(function (r, i) {
|
||||
return { kind: r[0], tagBg: tag[r[0]][0], tagFg: tag[r[0]][1], value: r[1], sub: r[2],
|
||||
on: r[3] ? 'true' : 'false', track: r[3] ? '#6D4AFF' : '#48454E', align: r[3] ? 'flex-end' : 'flex-start',
|
||||
fg: r[3] ? '#E5E1E9' : '#A9A3BF',
|
||||
divider: i < rows.length - 1 ? '1px solid #2B2930' : '0' };
|
||||
}) };
|
||||
};
|
||||
return {
|
||||
groups: [
|
||||
grp('ВСЕГДА ЧЕРЕЗ VPN', '#6D4AFF', [
|
||||
['АДРЕС', '130.49.213.176', 'один адрес', true],
|
||||
['АДРЕС', '10.8.0.0/24', 'сеть · выключено', false]
|
||||
]),
|
||||
grp('ВСЕГДА НАПРЯМУЮ', '#3FBF7F', [
|
||||
['ПАКЕТ', 'ru.*', '14 приложений', true],
|
||||
['ПАКЕТ', '*yandex*', '6 приложений', true],
|
||||
['САЙТ', 'mos.ru', '2 адреса', true]
|
||||
])
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Тёмная · Правило: изменить или удалить</title>
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap">
|
||||
<style>
|
||||
body{margin:0}
|
||||
a{color:#B69DFF}a:hover{color:#D0C2FF}
|
||||
</style>
|
||||
</helmet>
|
||||
<div style="position: relative; width: 390px; height: 844px; box-sizing: border-box; background: rgba(28,27,32,0.5); font-family: 'Manrope', system-ui, sans-serif; color: #E5E1E9; overflow: hidden; display: flex; flex-direction: column; justify-content: flex-end">
|
||||
<section role="dialog" aria-label="Правило" style="border-radius: 32px 32px 0 0; background: #141318; padding: 12px 16px 28px; display: flex; flex-direction: column; gap: 14px">
|
||||
<span style="align-self: center; width: 36px; height: 4px; border-radius: 2px; background: #48454E"></span>
|
||||
|
||||
<div style="margin: 4px 4px 0; display: flex; flex-direction: column; gap: 6px">
|
||||
<span style="align-self: flex-start; padding: 3px 7px; border-radius: 8px; background: #1E2A44; color: #A8C7FF; font-size: 10px; font-weight: 800">САЙТ</span>
|
||||
<h1 style="margin: 0; font: 800 22px ui-monospace, 'Cascadia Mono', Menlo, monospace; letter-spacing: -0.02em; overflow-wrap: anywhere">chatgpt.com</h1>
|
||||
</div>
|
||||
|
||||
<div style="border-radius: 22px; background: #1F1D24; border: 1px solid #2E2B35; display: flex; align-items: center; gap: 12px; padding: 0 14px 0 16px; height: 60px">
|
||||
<span style="flex: 1; display: flex; flex-direction: column; gap: 2px">
|
||||
<span style="font-size: 15px; font-weight: 700">Правило включено</span>
|
||||
<span style="font-size: 12px; color: #A9A3BF">Выключенное остаётся в списке</span>
|
||||
</span>
|
||||
<button aria-pressed="true" aria-label="Правило включено" style="flex: none; width: 52px; height: 32px; border: 0; border-radius: 16px; background: #6D4AFF; padding: 4px; display: flex; justify-content: flex-end"><span style="width: 24px; height: 24px; border-radius: 12px; background: #1F1D24"></span></button>
|
||||
</div>
|
||||
|
||||
<span style="padding: 0 4px; font-size: 13px; font-weight: 700; color: #CAC4D0">Куда направлять</span>
|
||||
<div role="radiogroup" aria-label="Куда" style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px">
|
||||
<button style="padding: 14px; border-radius: 18px; border: 1px solid #48454E; background: #1F1D24; text-align: left; font-family: 'Manrope', sans-serif; color: #E5E1E9; display: flex; flex-direction: column; gap: 4px">
|
||||
<span style="font-size: 15px; font-weight: 800">Напрямую</span>
|
||||
<span style="font-size: 12px; line-height: 1.35; color: #CAC4D0">Мимо VPN</span>
|
||||
</button>
|
||||
<button aria-pressed="true" style="padding: 14px; border-radius: 18px; border: 2px solid #6D4AFF; background: #2A2343; text-align: left; font-family: 'Manrope', sans-serif; color: #E5E1E9; display: flex; flex-direction: column; gap: 4px">
|
||||
<span style="font-size: 15px; font-weight: 800">Через VPN</span>
|
||||
<span style="font-size: 12px; line-height: 1.35; color: #CAC4D0">Даже если сайт в базе РФ</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="padding: 12px 14px; border-radius: 18px; background: #2A2343; font-size: 13px; line-height: 1.45; color: #E0DAF5">Сейчас у сайта 3 адреса: <span style="font-family: ui-monospace, 'Cascadia Mono', Menlo, monospace">104.18.32.47, 172.64.155.209, 2606:4700::6812:202f</span>. Они обновляются при подключении и раз в 30 минут.</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px">
|
||||
<a href="D_RuleDeleted.dc.html" style="height: 56px; border-radius: 18px; background: #4A1C1C; color: #FFB4AB; text-decoration: none; display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 15px; font-weight: 800">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 7h14M10 11v6M14 11v6M7 7l1 13h8l1-13M9 7V4h6v3"/></svg>Удалить
|
||||
</a>
|
||||
<a href="D_Rules.dc.html" style="height: 56px; border-radius: 18px; background: #6D4AFF; color: #FFFFFF; text-decoration: none; display: flex; align-items: center; justify-content: center; font-size: 15px; font-weight: 800">Готово</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props='{"$preview":{"width":390,"height":844}}'>
|
||||
class Component extends DCLogic {
|
||||
renderVals() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -23,13 +23,13 @@ a{color:#B69DFF}a:hover{color:#D0C2FF}
|
||||
</header>
|
||||
|
||||
<div style="padding: 0 16px; display: flex; flex-direction: column; gap: 12px">
|
||||
<p style="margin: 0 4px; font-size: 14px; line-height: 1.45; color: #CAC4D0">Куда идут сайты, адреса и приложения. Правило важнее базы РФ.</p>
|
||||
<p style="margin: 0 4px; font-size: 14px; line-height: 1.45; color: #CAC4D0">Куда идут сайты, адреса и приложения. Правило важнее базы РФ. Нажмите на правило, чтобы изменить его.</p>
|
||||
|
||||
<div style="display: flex; gap: 8px">
|
||||
<button aria-pressed="true" style="height: 36px; padding: 0 14px; border-radius: 12px; border: 0; background: #3B2C88; color: #FFFFFF; font: 700 13px 'Manrope', sans-serif">Все · 6</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #48454E; background: transparent; color: #E5E1E9; font: 600 13px 'Manrope', sans-serif">Сайты</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #48454E; background: transparent; color: #E5E1E9; font: 600 13px 'Manrope', sans-serif">Адреса</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #48454E; background: transparent; color: #E5E1E9; font: 600 13px 'Manrope', sans-serif">Пакеты</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #48454E; background: transparent; color: #E5E1E9; font: 600 13px 'Manrope', sans-serif">Сайты · 2</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #48454E; background: transparent; color: #E5E1E9; font: 600 13px 'Manrope', sans-serif">Адреса · 2</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #48454E; background: transparent; color: #E5E1E9; font: 600 13px 'Manrope', sans-serif">Пакеты · 2</button>
|
||||
</div>
|
||||
|
||||
<sc-for list="{{groups}}" as="g" hint-placeholder-count="2">
|
||||
@@ -37,13 +37,18 @@ a{color:#B69DFF}a:hover{color:#D0C2FF}
|
||||
<h2 style="margin: 6px 6px 0; display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 800; color: #B69DFF; letter-spacing: 0.04em"><span style="width: 8px; height: 8px; border-radius: 4px; background: {{g.dot}}"></span>{{g.title}}</h2>
|
||||
<div style="border-radius: 24px; background: #1F1D24; border: 1px solid #2E2B35; overflow: hidden">
|
||||
<sc-for list="{{g.rows}}" as="r" hint-placeholder-count="3">
|
||||
<div style="display: flex; align-items: center; gap: 12px; padding: 0 14px; height: 64px; border-bottom: {{r.divider}}">
|
||||
<span style="flex: none; padding: 3px 7px; border-radius: 8px; background: {{r.tagBg}}; color: {{r.tagFg}}; font-size: 10px; font-weight: 800">{{r.kind}}</span>
|
||||
<span style="flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0">
|
||||
<span style="font-size: 15px; font-weight: 700; font-family: ui-monospace, 'Cascadia Mono', Menlo, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{{r.value}}</span>
|
||||
<span style="font-size: 12px; color: #A9A3BF">{{r.sub}}</span>
|
||||
</span>
|
||||
<div style="display: flex; align-items: center; gap: 12px; padding: 0 6px 0 14px; height: 64px; border-bottom: {{r.divider}}">
|
||||
<a href="D_RuleEdit.dc.html" style="flex: 1; min-width: 0; height: 64px; display: flex; align-items: center; gap: 12px; text-decoration: none; color: {{r.fg}}">
|
||||
<span style="flex: none; padding: 3px 7px; border-radius: 8px; background: {{r.tagBg}}; color: {{r.tagFg}}; font-size: 10px; font-weight: 800">{{r.kind}}</span>
|
||||
<span style="flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0">
|
||||
<span style="font-size: 15px; font-weight: 700; font-family: ui-monospace, 'Cascadia Mono', Menlo, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{{r.value}}</span>
|
||||
<span style="font-size: 12px; color: #A9A3BF; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{{r.sub}}</span>
|
||||
</span>
|
||||
</a>
|
||||
<button aria-pressed="{{r.on}}" aria-label="Включить правило" style="flex: none; width: 52px; height: 32px; border: 0; border-radius: 16px; background: {{r.track}}; padding: 4px; display: flex; justify-content: {{r.align}}"><span style="width: 24px; height: 24px; border-radius: 12px; background: #1F1D24"></span></button>
|
||||
<a href="D_RuleDeleted.dc.html" aria-label="Удалить правило" style="flex: none; margin-left: -8px; width: 44px; height: 44px; border-radius: 22px; display: flex; align-items: center; justify-content: center; color: #A9A3BF">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 7h14M10 11v6M14 11v6M7 7l1 13h8l1-13M9 7V4h6v3"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
@@ -64,20 +69,21 @@ class Component extends DCLogic {
|
||||
return { title: title, dot: dot, rows: rows.map(function (r, i) {
|
||||
return { kind: r[0], tagBg: tag[r[0]][0], tagFg: tag[r[0]][1], value: r[1], sub: r[2],
|
||||
on: r[3] ? 'true' : 'false', track: r[3] ? '#6D4AFF' : '#48454E', align: r[3] ? 'flex-end' : 'flex-start',
|
||||
fg: r[3] ? '#E5E1E9' : '#A9A3BF',
|
||||
divider: i < rows.length - 1 ? '1px solid #2B2930' : '0' };
|
||||
}) };
|
||||
};
|
||||
return {
|
||||
groups: [
|
||||
grp('ВСЕГДА ЧЕРЕЗ VPN', '#6D4AFF', [
|
||||
['САЙТ', 'chatgpt.com', 'и поддомены', true],
|
||||
['САЙТ', 'chatgpt.com', '3 адреса', true],
|
||||
['АДРЕС', '130.49.213.176', 'один адрес', true],
|
||||
['АДРЕС', '10.8.0.0/24', 'подсеть', false]
|
||||
['АДРЕС', '10.8.0.0/24', 'сеть · выключено', false]
|
||||
]),
|
||||
grp('ВСЕГДА НАПРЯМУЮ', '#3FBF7F', [
|
||||
['ПАКЕТ', 'ru.*', '14 приложений', true],
|
||||
['ПАКЕТ', '*yandex*', '6 приложений · 1 исключение', true],
|
||||
['САЙТ', '*.mos.ru', 'и поддомены', true]
|
||||
['ПАКЕТ', '*yandex*', '6 приложений', true],
|
||||
['САЙТ', 'mos.ru', '2 адреса', true]
|
||||
])
|
||||
]
|
||||
};
|
||||
|
||||
@@ -78,9 +78,9 @@ a{color:#6D4AFF}a:hover{color:#4B2FD1}
|
||||
</span>
|
||||
<span style="flex: 1; display: flex; flex-direction: column; gap: 2px">
|
||||
<span style="font-size: 16px; font-weight: 700">Правила</span>
|
||||
<span style="font-size: 13px; color: #5E5A7D">Домены, адреса и шаблоны пакетов</span>
|
||||
<span style="font-size: 13px; color: #5E5A7D">2 через VPN · 3 напрямую · 1 выключено</span>
|
||||
</span>
|
||||
<span style="font-size: 14px; font-weight: 700; color: #5E5A7D">5</span>
|
||||
<span style="font-size: 14px; font-weight: 700; color: #5E5A7D">6</span>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#5E5A7D" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Правило удалено — можно отменить</title>
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap">
|
||||
<style>
|
||||
body{margin:0}
|
||||
a{color:#6D4AFF}a:hover{color:#4B2FD1}
|
||||
</style>
|
||||
</helmet>
|
||||
<div style="position: relative; width: 390px; height: 900px; box-sizing: border-box; background: #FCF8FF; font-family: 'Manrope', system-ui, sans-serif; color: #1C1B20; overflow: hidden; font-variant-numeric: tabular-nums">
|
||||
<header style="display: flex; align-items: center; gap: 8px; padding: 16px 12px 8px">
|
||||
<a href="F_Routing.dc.html" aria-label="Назад" style="width: 44px; height: 44px; border-radius: 22px; display: flex; align-items: center; justify-content: center; color: #1C1B20">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 6l-6 6 6 6"/></svg>
|
||||
</a>
|
||||
<h1 style="margin: 0; flex: 1; font-size: 22px; font-weight: 800">Правила</h1>
|
||||
</header>
|
||||
|
||||
<div style="padding: 0 16px; display: flex; flex-direction: column; gap: 12px">
|
||||
<p style="margin: 0 4px; font-size: 14px; line-height: 1.45; color: #49454F">Куда идут сайты, адреса и приложения. Правило важнее базы РФ. Нажмите на правило, чтобы изменить его.</p>
|
||||
|
||||
<div style="display: flex; gap: 8px">
|
||||
<button aria-pressed="true" style="height: 36px; padding: 0 14px; border-radius: 12px; border: 0; background: #3B2C88; color: #FFFFFF; font: 700 13px 'Manrope', sans-serif">Все · 5</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #C9C5D0; background: transparent; color: #1C1B20; font: 600 13px 'Manrope', sans-serif">Сайты · 1</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #C9C5D0; background: transparent; color: #1C1B20; font: 600 13px 'Manrope', sans-serif">Адреса · 2</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #C9C5D0; background: transparent; color: #1C1B20; font: 600 13px 'Manrope', sans-serif">Пакеты · 2</button>
|
||||
</div>
|
||||
|
||||
<sc-for list="{{groups}}" as="g" hint-placeholder-count="2">
|
||||
<section style="display: flex; flex-direction: column; gap: 6px">
|
||||
<h2 style="margin: 6px 6px 0; display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 800; color: #6D4AFF; letter-spacing: 0.04em"><span style="width: 8px; height: 8px; border-radius: 4px; background: {{g.dot}}"></span>{{g.title}}</h2>
|
||||
<div style="border-radius: 24px; background: #FFFFFF; border: 1px solid #EDE8F2; overflow: hidden">
|
||||
<sc-for list="{{g.rows}}" as="r" hint-placeholder-count="3">
|
||||
<div style="display: flex; align-items: center; gap: 12px; padding: 0 6px 0 14px; height: 64px; border-bottom: {{r.divider}}">
|
||||
<a href="F_RuleEdit.dc.html" style="flex: 1; min-width: 0; height: 64px; display: flex; align-items: center; gap: 12px; text-decoration: none; color: {{r.fg}}">
|
||||
<span style="flex: none; padding: 3px 7px; border-radius: 8px; background: {{r.tagBg}}; color: {{r.tagFg}}; font-size: 10px; font-weight: 800">{{r.kind}}</span>
|
||||
<span style="flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0">
|
||||
<span style="font-size: 15px; font-weight: 700; font-family: ui-monospace, 'Cascadia Mono', Menlo, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{{r.value}}</span>
|
||||
<span style="font-size: 12px; color: #5E5A7D; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{{r.sub}}</span>
|
||||
</span>
|
||||
</a>
|
||||
<button aria-pressed="{{r.on}}" aria-label="Включить правило" style="flex: none; width: 52px; height: 32px; border: 0; border-radius: 16px; background: {{r.track}}; padding: 4px; display: flex; justify-content: {{r.align}}"><span style="width: 24px; height: 24px; border-radius: 12px; background: #FFFFFF"></span></button>
|
||||
<a href="F_RuleDeleted.dc.html" aria-label="Удалить правило" style="flex: none; margin-left: -8px; width: 44px; height: 44px; border-radius: 22px; display: flex; align-items: center; justify-content: center; color: #5E5A7D">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 7h14M10 11v6M14 11v6M7 7l1 13h8l1-13M9 7V4h6v3"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</section>
|
||||
</sc-for>
|
||||
</div>
|
||||
|
||||
<a href="F_RuleAdd.dc.html" style="position: absolute; right: 16px; bottom: 96px; height: 60px; padding: 0 22px; border: 0; border-radius: 20px; background: #6D4AFF; color: #FFFFFF; font-size: 16px; font-weight: 800; text-decoration: none; display: flex; align-items: center; gap: 10px; box-shadow: 0 6px 18px rgba(109,74,255,0.35)">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Правило
|
||||
</a>
|
||||
<div role="status" style="position: absolute; left: 16px; right: 16px; bottom: 24px; min-height: 56px; box-sizing: border-box; padding: 0 6px 0 16px; border-radius: 16px; background: #313036; color: #F3EFF7; display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; box-shadow: 0 6px 18px rgba(28,27,32,0.25)">
|
||||
<span style="flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">Правило chatgpt.com удалено</span>
|
||||
<a href="F_Rules.dc.html" style="flex: none; height: 44px; padding: 0 12px; border-radius: 22px; display: flex; align-items: center; text-decoration: none; font-size: 14px; font-weight: 800; color: #B69DFF">Отменить</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props='{"$preview":{"width":390,"height":900}}'>
|
||||
class Component extends DCLogic {
|
||||
renderVals() {
|
||||
var tag = { 'САЙТ': ['#E3ECFF', '#1F4FA8'], 'АДРЕС': ['#F0ECF4', '#49454F'], 'ПАКЕТ': ['#DDF3E6', '#0E6B35'] };
|
||||
var grp = function (title, dot, rows) {
|
||||
return { title: title, dot: dot, rows: rows.map(function (r, i) {
|
||||
return { kind: r[0], tagBg: tag[r[0]][0], tagFg: tag[r[0]][1], value: r[1], sub: r[2],
|
||||
on: r[3] ? 'true' : 'false', track: r[3] ? '#6D4AFF' : '#C9C5D0', align: r[3] ? 'flex-end' : 'flex-start',
|
||||
fg: r[3] ? '#1C1B20' : '#5E5A7D',
|
||||
divider: i < rows.length - 1 ? '1px solid #F0ECF4' : '0' };
|
||||
}) };
|
||||
};
|
||||
return {
|
||||
groups: [
|
||||
grp('ВСЕГДА ЧЕРЕЗ VPN', '#6D4AFF', [
|
||||
['АДРЕС', '130.49.213.176', 'один адрес', true],
|
||||
['АДРЕС', '10.8.0.0/24', 'сеть · выключено', false]
|
||||
]),
|
||||
grp('ВСЕГДА НАПРЯМУЮ', '#3FBF7F', [
|
||||
['ПАКЕТ', 'ru.*', '14 приложений', true],
|
||||
['ПАКЕТ', '*yandex*', '6 приложений', true],
|
||||
['САЙТ', 'mos.ru', '2 адреса', true]
|
||||
])
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Правило: изменить или удалить</title>
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap">
|
||||
<style>
|
||||
body{margin:0}
|
||||
a{color:#6D4AFF}a:hover{color:#4B2FD1}
|
||||
</style>
|
||||
</helmet>
|
||||
<div style="position: relative; width: 390px; height: 844px; box-sizing: border-box; background: rgba(28,27,32,0.5); font-family: 'Manrope', system-ui, sans-serif; color: #1C1B20; overflow: hidden; display: flex; flex-direction: column; justify-content: flex-end">
|
||||
<section role="dialog" aria-label="Правило" style="border-radius: 32px 32px 0 0; background: #FCF8FF; padding: 12px 16px 28px; display: flex; flex-direction: column; gap: 14px">
|
||||
<span style="align-self: center; width: 36px; height: 4px; border-radius: 2px; background: #C9C5D0"></span>
|
||||
|
||||
<div style="margin: 4px 4px 0; display: flex; flex-direction: column; gap: 6px">
|
||||
<span style="align-self: flex-start; padding: 3px 7px; border-radius: 8px; background: #E3ECFF; color: #1F4FA8; font-size: 10px; font-weight: 800">САЙТ</span>
|
||||
<h1 style="margin: 0; font: 800 22px ui-monospace, 'Cascadia Mono', Menlo, monospace; letter-spacing: -0.02em; overflow-wrap: anywhere">chatgpt.com</h1>
|
||||
</div>
|
||||
|
||||
<div style="border-radius: 22px; background: #FFFFFF; border: 1px solid #EDE8F2; display: flex; align-items: center; gap: 12px; padding: 0 14px 0 16px; height: 60px">
|
||||
<span style="flex: 1; display: flex; flex-direction: column; gap: 2px">
|
||||
<span style="font-size: 15px; font-weight: 700">Правило включено</span>
|
||||
<span style="font-size: 12px; color: #5E5A7D">Выключенное остаётся в списке</span>
|
||||
</span>
|
||||
<button aria-pressed="true" aria-label="Правило включено" style="flex: none; width: 52px; height: 32px; border: 0; border-radius: 16px; background: #6D4AFF; padding: 4px; display: flex; justify-content: flex-end"><span style="width: 24px; height: 24px; border-radius: 12px; background: #FFFFFF"></span></button>
|
||||
</div>
|
||||
|
||||
<span style="padding: 0 4px; font-size: 13px; font-weight: 700; color: #49454F">Куда направлять</span>
|
||||
<div role="radiogroup" aria-label="Куда" style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px">
|
||||
<button style="padding: 14px; border-radius: 18px; border: 1px solid #C9C5D0; background: #FFFFFF; text-align: left; font-family: 'Manrope', sans-serif; color: #1C1B20; display: flex; flex-direction: column; gap: 4px">
|
||||
<span style="font-size: 15px; font-weight: 800">Напрямую</span>
|
||||
<span style="font-size: 12px; line-height: 1.35; color: #49454F">Мимо VPN</span>
|
||||
</button>
|
||||
<button aria-pressed="true" style="padding: 14px; border-radius: 18px; border: 2px solid #6D4AFF; background: #F3EEFF; text-align: left; font-family: 'Manrope', sans-serif; color: #1C1B20; display: flex; flex-direction: column; gap: 4px">
|
||||
<span style="font-size: 15px; font-weight: 800">Через VPN</span>
|
||||
<span style="font-size: 12px; line-height: 1.35; color: #49454F">Даже если сайт в базе РФ</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="padding: 12px 14px; border-radius: 18px; background: #F3EEFF; font-size: 13px; line-height: 1.45; color: #2A2640">Сейчас у сайта 3 адреса: <span style="font-family: ui-monospace, 'Cascadia Mono', Menlo, monospace">104.18.32.47, 172.64.155.209, 2606:4700::6812:202f</span>. Они обновляются при подключении и раз в 30 минут.</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px">
|
||||
<a href="F_RuleDeleted.dc.html" style="height: 56px; border-radius: 18px; background: #FFDAD8; color: #8C1D24; text-decoration: none; display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 15px; font-weight: 800">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 7h14M10 11v6M14 11v6M7 7l1 13h8l1-13M9 7V4h6v3"/></svg>Удалить
|
||||
</a>
|
||||
<a href="F_Rules.dc.html" style="height: 56px; border-radius: 18px; background: #6D4AFF; color: #FFFFFF; text-decoration: none; display: flex; align-items: center; justify-content: center; font-size: 15px; font-weight: 800">Готово</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props='{"$preview":{"width":390,"height":844}}'>
|
||||
class Component extends DCLogic {
|
||||
renderVals() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -23,13 +23,13 @@ a{color:#6D4AFF}a:hover{color:#4B2FD1}
|
||||
</header>
|
||||
|
||||
<div style="padding: 0 16px; display: flex; flex-direction: column; gap: 12px">
|
||||
<p style="margin: 0 4px; font-size: 14px; line-height: 1.45; color: #49454F">Куда идут сайты, адреса и приложения. Правило важнее базы РФ.</p>
|
||||
<p style="margin: 0 4px; font-size: 14px; line-height: 1.45; color: #49454F">Куда идут сайты, адреса и приложения. Правило важнее базы РФ. Нажмите на правило, чтобы изменить его.</p>
|
||||
|
||||
<div style="display: flex; gap: 8px">
|
||||
<button aria-pressed="true" style="height: 36px; padding: 0 14px; border-radius: 12px; border: 0; background: #3B2C88; color: #FFFFFF; font: 700 13px 'Manrope', sans-serif">Все · 6</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #C9C5D0; background: transparent; color: #1C1B20; font: 600 13px 'Manrope', sans-serif">Сайты</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #C9C5D0; background: transparent; color: #1C1B20; font: 600 13px 'Manrope', sans-serif">Адреса</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #C9C5D0; background: transparent; color: #1C1B20; font: 600 13px 'Manrope', sans-serif">Пакеты</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #C9C5D0; background: transparent; color: #1C1B20; font: 600 13px 'Manrope', sans-serif">Сайты · 2</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #C9C5D0; background: transparent; color: #1C1B20; font: 600 13px 'Manrope', sans-serif">Адреса · 2</button>
|
||||
<button style="height: 36px; padding: 0 14px; border-radius: 12px; border: 1px solid #C9C5D0; background: transparent; color: #1C1B20; font: 600 13px 'Manrope', sans-serif">Пакеты · 2</button>
|
||||
</div>
|
||||
|
||||
<sc-for list="{{groups}}" as="g" hint-placeholder-count="2">
|
||||
@@ -37,13 +37,18 @@ a{color:#6D4AFF}a:hover{color:#4B2FD1}
|
||||
<h2 style="margin: 6px 6px 0; display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 800; color: #6D4AFF; letter-spacing: 0.04em"><span style="width: 8px; height: 8px; border-radius: 4px; background: {{g.dot}}"></span>{{g.title}}</h2>
|
||||
<div style="border-radius: 24px; background: #FFFFFF; border: 1px solid #EDE8F2; overflow: hidden">
|
||||
<sc-for list="{{g.rows}}" as="r" hint-placeholder-count="3">
|
||||
<div style="display: flex; align-items: center; gap: 12px; padding: 0 14px; height: 64px; border-bottom: {{r.divider}}">
|
||||
<span style="flex: none; padding: 3px 7px; border-radius: 8px; background: {{r.tagBg}}; color: {{r.tagFg}}; font-size: 10px; font-weight: 800">{{r.kind}}</span>
|
||||
<span style="flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0">
|
||||
<span style="font-size: 15px; font-weight: 700; font-family: ui-monospace, 'Cascadia Mono', Menlo, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{{r.value}}</span>
|
||||
<span style="font-size: 12px; color: #5E5A7D">{{r.sub}}</span>
|
||||
</span>
|
||||
<div style="display: flex; align-items: center; gap: 12px; padding: 0 6px 0 14px; height: 64px; border-bottom: {{r.divider}}">
|
||||
<a href="F_RuleEdit.dc.html" style="flex: 1; min-width: 0; height: 64px; display: flex; align-items: center; gap: 12px; text-decoration: none; color: {{r.fg}}">
|
||||
<span style="flex: none; padding: 3px 7px; border-radius: 8px; background: {{r.tagBg}}; color: {{r.tagFg}}; font-size: 10px; font-weight: 800">{{r.kind}}</span>
|
||||
<span style="flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0">
|
||||
<span style="font-size: 15px; font-weight: 700; font-family: ui-monospace, 'Cascadia Mono', Menlo, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{{r.value}}</span>
|
||||
<span style="font-size: 12px; color: #5E5A7D; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{{r.sub}}</span>
|
||||
</span>
|
||||
</a>
|
||||
<button aria-pressed="{{r.on}}" aria-label="Включить правило" style="flex: none; width: 52px; height: 32px; border: 0; border-radius: 16px; background: {{r.track}}; padding: 4px; display: flex; justify-content: {{r.align}}"><span style="width: 24px; height: 24px; border-radius: 12px; background: #FFFFFF"></span></button>
|
||||
<a href="F_RuleDeleted.dc.html" aria-label="Удалить правило" style="flex: none; margin-left: -8px; width: 44px; height: 44px; border-radius: 22px; display: flex; align-items: center; justify-content: center; color: #5E5A7D">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 7h14M10 11v6M14 11v6M7 7l1 13h8l1-13M9 7V4h6v3"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
@@ -64,20 +69,21 @@ class Component extends DCLogic {
|
||||
return { title: title, dot: dot, rows: rows.map(function (r, i) {
|
||||
return { kind: r[0], tagBg: tag[r[0]][0], tagFg: tag[r[0]][1], value: r[1], sub: r[2],
|
||||
on: r[3] ? 'true' : 'false', track: r[3] ? '#6D4AFF' : '#C9C5D0', align: r[3] ? 'flex-end' : 'flex-start',
|
||||
fg: r[3] ? '#1C1B20' : '#5E5A7D',
|
||||
divider: i < rows.length - 1 ? '1px solid #F0ECF4' : '0' };
|
||||
}) };
|
||||
};
|
||||
return {
|
||||
groups: [
|
||||
grp('ВСЕГДА ЧЕРЕЗ VPN', '#6D4AFF', [
|
||||
['САЙТ', 'chatgpt.com', 'и поддомены', true],
|
||||
['САЙТ', 'chatgpt.com', '3 адреса', true],
|
||||
['АДРЕС', '130.49.213.176', 'один адрес', true],
|
||||
['АДРЕС', '10.8.0.0/24', 'подсеть', false]
|
||||
['АДРЕС', '10.8.0.0/24', 'сеть · выключено', false]
|
||||
]),
|
||||
grp('ВСЕГДА НАПРЯМУЮ', '#3FBF7F', [
|
||||
['ПАКЕТ', 'ru.*', '14 приложений', true],
|
||||
['ПАКЕТ', '*yandex*', '6 приложений · 1 исключение', true],
|
||||
['САЙТ', '*.mos.ru', 'и поддомены', true]
|
||||
['ПАКЕТ', '*yandex*', '6 приложений', true],
|
||||
['САЙТ', 'mos.ru', '2 адреса', true]
|
||||
])
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1137,6 +1137,38 @@
|
||||
"h": 1000,
|
||||
"title": "Виджеты на рабочем столе",
|
||||
"page": "final_dark"
|
||||
},
|
||||
"F_RuleEdit.dc.html": {
|
||||
"x": 2350,
|
||||
"y": 1540,
|
||||
"w": 390,
|
||||
"h": 844,
|
||||
"page": "final",
|
||||
"title": "Правило: изменить или удалить"
|
||||
},
|
||||
"F_RuleDeleted.dc.html": {
|
||||
"x": 2820,
|
||||
"y": 1540,
|
||||
"w": 390,
|
||||
"h": 900,
|
||||
"page": "final",
|
||||
"title": "Правило удалено — «Отменить»"
|
||||
},
|
||||
"D_RuleEdit.dc.html": {
|
||||
"x": 2350,
|
||||
"y": 1540,
|
||||
"w": 390,
|
||||
"h": 844,
|
||||
"page": "final_dark",
|
||||
"title": "Правило: изменить или удалить"
|
||||
},
|
||||
"D_RuleDeleted.dc.html": {
|
||||
"x": 2820,
|
||||
"y": 1540,
|
||||
"w": 390,
|
||||
"h": 900,
|
||||
"page": "final_dark",
|
||||
"title": "Правило удалено — «Отменить»"
|
||||
}
|
||||
},
|
||||
"createdOnFiles": {
|
||||
@@ -1162,7 +1194,7 @@
|
||||
"fB": {
|
||||
"color": "green",
|
||||
"kind": "title1",
|
||||
"maxW": 2270,
|
||||
"maxW": 3210,
|
||||
"page": "final",
|
||||
"text": "Финал 2026 · Маршруты",
|
||||
"w": 240,
|
||||
@@ -1339,7 +1371,7 @@
|
||||
"dfB": {
|
||||
"color": "purple",
|
||||
"kind": "title1",
|
||||
"maxW": 2270,
|
||||
"maxW": 3210,
|
||||
"page": "final_dark",
|
||||
"text": "Тёмная тема · Маршруты",
|
||||
"w": 240,
|
||||
@@ -1589,7 +1621,11 @@
|
||||
"D_HomeOffline.dc.html",
|
||||
"D_HomeUnreachable.dc.html",
|
||||
"D_QsTile.dc.html",
|
||||
"D_Widgets.dc.html"
|
||||
"D_Widgets.dc.html",
|
||||
"F_RuleEdit.dc.html",
|
||||
"F_RuleDeleted.dc.html",
|
||||
"D_RuleEdit.dc.html",
|
||||
"D_RuleDeleted.dc.html"
|
||||
],
|
||||
"pages": [
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user