android: appearance, kill switch and update pages as in the Final 2026 mockup
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fecef1135c
commit
dc8f5a75e1
4 files changed
+384
-20
No files matched your search
+302
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* Copyright (C) 2026 SHX VPN
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*/
|
||||
|
||||
package org.strongswan.android.ui.compose
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.strongswan.android.R
|
||||
import org.strongswan.android.logic.updates.UpdateState
|
||||
import org.strongswan.android.ui.compose.design.ShxButton
|
||||
import org.strongswan.android.ui.compose.design.ShxButtonStyle
|
||||
import org.strongswan.android.ui.compose.design.ShxIcons
|
||||
import org.strongswan.android.ui.compose.design.ShxNote
|
||||
import org.strongswan.android.ui.compose.design.ShxPanel
|
||||
import org.strongswan.android.ui.compose.design.ShxSectionHeader
|
||||
import org.strongswan.android.ui.compose.design.ShxToggleRow
|
||||
import org.strongswan.android.ui.compose.theme.LocalShxColors
|
||||
import org.strongswan.android.ui.compose.theme.ShxThemeSettings
|
||||
import org.strongswan.android.ui.compose.theme.ShxType
|
||||
import org.strongswan.android.ui.compose.theme.ThemeMode
|
||||
|
||||
/** «Appearance» (F_Appearance): three theme cards with a preview, wallpaper colors, language. */
|
||||
@Composable
|
||||
fun AppearancePage()
|
||||
{
|
||||
val context = LocalContext.current
|
||||
val c = LocalShxColors.current
|
||||
val mode by remember { ShxThemeSettings.load(context); ShxThemeSettings.themeMode }.collectAsStateWithLifecycle()
|
||||
val dynamic by remember { ShxThemeSettings.useDynamicColors }.collectAsStateWithLifecycle()
|
||||
Column(Modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp))
|
||||
{
|
||||
ShxSectionHeader(stringResource(R.string.appearance_theme))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp))
|
||||
{
|
||||
listOf(ThemeMode.SYSTEM to R.string.theme_system, ThemeMode.LIGHT to R.string.theme_light, ThemeMode.DARK to R.string.theme_dark).forEach { (m, label) ->
|
||||
val on = mode == m
|
||||
val shape = RoundedCornerShape(20.dp)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(shape)
|
||||
.background(c.card)
|
||||
.border(if (on) 2.dp else 1.dp, if (on) c.accent else c.cardBorder, shape)
|
||||
.selectable(selected = on, role = Role.RadioButton) { ShxThemeSettings.setThemeMode(context, m) }
|
||||
.padding(10.dp),
|
||||
)
|
||||
{
|
||||
ThemePreview(m)
|
||||
Text(stringResource(label), style = ShxType.chip, color = c.text, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
ShxPanel(Modifier.padding(top = 4.dp))
|
||||
{
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
{
|
||||
ShxToggleRow(
|
||||
stringResource(R.string.appearance_wallpaper),
|
||||
dynamic,
|
||||
{ ShxThemeSettings.setUseDynamicColors(context, it) },
|
||||
subtitle = stringResource(R.string.appearance_wallpaper_sub),
|
||||
divider = true,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().height(60.dp)
|
||||
.clickable(role = Role.Button) {
|
||||
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
Intent(Settings.ACTION_APP_LOCALE_SETTINGS, Uri.fromParts("package", context.packageName, null))
|
||||
else Intent(Settings.ACTION_LOCALE_SETTINGS)
|
||||
runCatching { context.startActivity(intent) }
|
||||
}
|
||||
.padding(start = 16.dp, end = 14.dp),
|
||||
)
|
||||
{
|
||||
Text(stringResource(R.string.appearance_language), style = ShxType.cardTitle.copy(fontWeight = FontWeight.Bold), color = c.text, modifier = Modifier.weight(1f))
|
||||
Text(stringResource(R.string.theme_system), style = ShxType.body, color = c.textSub)
|
||||
Icon(ShxIcons.Chevron, contentDescription = null, tint = c.iconMuted, modifier = Modifier.size(18.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Mini screen of a theme: background, the hero blob, two text lines. */
|
||||
@Composable
|
||||
private fun ThemePreview(mode: ThemeMode)
|
||||
{
|
||||
val light = Color(0xFFFCF8FF)
|
||||
val dark = Color(0xFF141318)
|
||||
val bg = when (mode)
|
||||
{
|
||||
ThemeMode.SYSTEM -> Brush.horizontalGradient(0f to light, 0.5f to light, 0.5f to dark, 1f to dark)
|
||||
ThemeMode.LIGHT -> Brush.linearGradient(listOf(light, light))
|
||||
ThemeMode.DARK -> Brush.linearGradient(listOf(dark, dark))
|
||||
}
|
||||
val accent = if (mode == ThemeMode.DARK) Color(0xFF9C7CFF) else Color(0xFF6D4AFF)
|
||||
val line = when (mode)
|
||||
{
|
||||
ThemeMode.SYSTEM -> Color(0xFFC9C5D0)
|
||||
ThemeMode.LIGHT -> Color(0xFFE5E1E9)
|
||||
ThemeMode.DARK -> Color(0xFF36333D)
|
||||
}
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
|
||||
modifier = Modifier.fillMaxWidth().height(120.dp).clip(RoundedCornerShape(14.dp)).background(bg).border(1.dp, Color(0xFFE5E1E9), RoundedCornerShape(14.dp)),
|
||||
)
|
||||
{
|
||||
Box(Modifier.size(34.dp).clip(RoundedCornerShape(topStartPercent = 45, topEndPercent = 55, bottomEndPercent = 50, bottomStartPercent = 50)).background(accent))
|
||||
Box(Modifier.width(56.dp).height(8.dp).clip(RoundedCornerShape(4.dp)).background(line))
|
||||
Box(Modifier.width(40.dp).height(8.dp).clip(RoundedCornerShape(4.dp)).background(line))
|
||||
}
|
||||
}
|
||||
|
||||
/** «No VPN, no network» (F_KillSwitch): status, three steps, note, button to the Android settings. */
|
||||
@Composable
|
||||
fun KillSwitchPage(lockdown: Boolean)
|
||||
{
|
||||
val context = LocalContext.current
|
||||
val c = LocalShxColors.current
|
||||
Column(Modifier.padding(horizontal = 16.dp).padding(top = 4.dp), verticalArrangement = Arrangement.spacedBy(14.dp))
|
||||
{
|
||||
val shape = RoundedCornerShape(24.dp)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().clip(shape)
|
||||
.background(if (lockdown) c.successContainer else c.warningContainer)
|
||||
.border(1.dp, if (lockdown) c.successContainer else c.warningBorder, shape).padding(16.dp),
|
||||
)
|
||||
{
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(44.dp).clip(RoundedCornerShape(14.dp)).background(if (lockdown) c.success else c.warning))
|
||||
{
|
||||
Icon(ShxIcons.Lock, contentDescription = null, tint = Color.White, modifier = Modifier.size(22.dp))
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp))
|
||||
{
|
||||
Text(
|
||||
stringResource(if (lockdown) R.string.ks_on_title else R.string.ks_off_title),
|
||||
style = ShxType.cardHeading, color = if (lockdown) c.onSuccessStrong else c.onWarningStrong,
|
||||
)
|
||||
Text(
|
||||
stringResource(if (lockdown) R.string.ks_on_text else R.string.ks_off_text),
|
||||
style = ShxType.note, color = if (lockdown) c.onSuccess else c.onWarningContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(stringResource(R.string.ks_intro), style = ShxType.note.copy(fontSize = 15.sp, lineHeight = 22.sp), color = c.textTonal, modifier = Modifier.padding(horizontal = 4.dp))
|
||||
listOf(R.string.ks_step1, R.string.ks_step2, R.string.ks_step3).forEachIndexed { i, step ->
|
||||
val s = RoundedCornerShape(20.dp)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().clip(s).background(c.card).border(1.dp, c.cardBorder, s).padding(14.dp),
|
||||
)
|
||||
{
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(28.dp).clip(CircleShape).background(c.accent))
|
||||
{
|
||||
Text("${i + 1}", style = ShxType.chip.copy(fontWeight = FontWeight.ExtraBold), color = Color.White)
|
||||
}
|
||||
Text(stringResource(step), style = ShxType.note.copy(fontSize = 14.sp), color = c.text)
|
||||
}
|
||||
}
|
||||
ShxNote(stringResource(R.string.ks_note))
|
||||
ShxButton(
|
||||
stringResource(R.string.ks_open),
|
||||
{ runCatching { context.startActivity(Intent(Settings.ACTION_VPN_SETTINGS)) } },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Update card (F_Update): app icon, version, what is new, progress, Later / Install. */
|
||||
@Composable
|
||||
fun UpdateCard(state: UpdateState, installed: String, onCheck: () -> Unit, onInstall: () -> Unit)
|
||||
{
|
||||
val c = LocalShxColors.current
|
||||
val info = (state as? UpdateState.Available)?.info ?: (state as? UpdateState.Downloading)?.info
|
||||
Column(Modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp))
|
||||
{
|
||||
val shape = RoundedCornerShape(24.dp)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().clip(shape).background(c.card).border(1.dp, c.cardBorder, shape).padding(16.dp),
|
||||
)
|
||||
{
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp))
|
||||
{
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(56.dp).clip(RoundedCornerShape(16.dp)).background(c.accent))
|
||||
{
|
||||
Icon(painterResource(R.drawable.ic_notification), contentDescription = null, tint = Color.White, modifier = Modifier.size(36.dp))
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp))
|
||||
{
|
||||
Text(
|
||||
if (info != null) "SHX VPN ${info.versionName}" else stringResource(R.string.update_card_uptodate),
|
||||
style = ShxType.cardHeading.copy(fontSize = 20.sp), color = c.text,
|
||||
)
|
||||
Text(
|
||||
when
|
||||
{
|
||||
info != null -> stringResource(R.string.update_card_have, installed, formatBytes(info.size))
|
||||
state is UpdateState.Checking -> stringResource(R.string.update_card_checking)
|
||||
state is UpdateState.Failed -> stringResource(R.string.update_card_failed)
|
||||
else -> stringResource(R.string.update_card_version, installed)
|
||||
},
|
||||
style = ShxType.sub, color = c.textSub,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (info != null)
|
||||
{
|
||||
val notes = info.notes["ru"] ?: info.notes["en"] ?: info.notes.values.firstOrNull()
|
||||
if (!notes.isNullOrBlank())
|
||||
{
|
||||
Text(stringResource(R.string.update_card_whats_new).uppercase(), style = ShxType.section.copy(fontSize = 12.sp), color = c.accentText)
|
||||
notes.split(Regex("(?<=[.;])\\s+")).map { it.trim().trimEnd('.', ';') }.filter { it.isNotEmpty() }.forEach { line ->
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp))
|
||||
{
|
||||
Box(Modifier.padding(top = 8.dp).size(6.dp).clip(CircleShape).background(c.accent))
|
||||
Text(line, style = ShxType.note.copy(fontSize = 14.sp), color = c.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state is UpdateState.Downloading)
|
||||
{
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp))
|
||||
{
|
||||
Row(Modifier.fillMaxWidth())
|
||||
{
|
||||
Text(stringResource(R.string.update_card_downloading), style = ShxType.meta, color = c.textSub, modifier = Modifier.weight(1f))
|
||||
Text("${state.progress}%", style = ShxType.meta, color = c.textSub)
|
||||
}
|
||||
Box(Modifier.fillMaxWidth().height(6.dp).clip(RoundedCornerShape(3.dp)).background(c.tile))
|
||||
{
|
||||
Box(Modifier.fillMaxWidth(state.progress.coerceIn(0, 100) / 100f).height(6.dp).clip(RoundedCornerShape(3.dp)).background(c.accent))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp))
|
||||
{
|
||||
if (info != null)
|
||||
{
|
||||
ShxButton(stringResource(R.string.update_card_check), onCheck, style = ShxButtonStyle.OUTLINE, modifier = Modifier.weight(1f))
|
||||
ShxButton(stringResource(R.string.update_card_install), onInstall, modifier = Modifier.weight(2f), enabled = state !is UpdateState.Downloading)
|
||||
}
|
||||
else
|
||||
{
|
||||
ShxButton(stringResource(R.string.update_card_check), onCheck, style = ShxButtonStyle.SOFT, modifier = Modifier.fillMaxWidth(), enabled = state !is UpdateState.Checking)
|
||||
}
|
||||
}
|
||||
Text(stringResource(R.string.update_card_footer), style = ShxType.meta.copy(fontWeight = FontWeight.Normal), color = c.textSub, modifier = Modifier.padding(horizontal = 6.dp))
|
||||
}
|
||||
}
|
||||
+32
-20
@@ -245,24 +245,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
||||
|
||||
if (page == SettingsPage.APPEARANCE)
|
||||
{
|
||||
ShxSection(title = stringResource(R.string.settings_page_appearance))
|
||||
{
|
||||
ThemeModeRow()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
{
|
||||
var dynamicColors by remember { ShxThemeSettings.load(context); mutableStateOf(ShxThemeSettings.useDynamicColors.value) }
|
||||
ShxSettingToggleRow(
|
||||
icon = Icons.Rounded.Palette,
|
||||
title = stringResource(R.string.pref_dynamic_colors_title),
|
||||
subtitle = stringResource(R.string.pref_dynamic_colors_summary),
|
||||
checked = dynamicColors,
|
||||
onCheckedChange = {
|
||||
dynamicColors = it
|
||||
ShxThemeSettings.setUseDynamicColors(context, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
AppearancePage()
|
||||
}
|
||||
|
||||
if (page == SettingsPage.STABILITY)
|
||||
@@ -467,7 +450,35 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
||||
|
||||
if (page == SettingsPage.UPDATES)
|
||||
{
|
||||
ShxSection(title = stringResource(R.string.settings_section_updates))
|
||||
UpdateCard(
|
||||
state = updateState,
|
||||
installed = versionName,
|
||||
onCheck = {
|
||||
when (updateState)
|
||||
{
|
||||
is UpdateState.Checking, is UpdateState.Downloading -> Unit
|
||||
else -> updateManager.checkNow()
|
||||
}
|
||||
},
|
||||
onInstall = { showUpdateConfirm = true },
|
||||
)
|
||||
}
|
||||
|
||||
if (page == SettingsPage.KILLSWITCH)
|
||||
{
|
||||
var resumes by remember { mutableStateOf(0) }
|
||||
val owner = LocalLifecycleOwner.current
|
||||
DisposableEffect(owner) {
|
||||
val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) resumes++ }
|
||||
owner.lifecycle.addObserver(observer)
|
||||
onDispose { owner.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
KillSwitchPage(remember(resumes) { context.isVpnLockdownEnabled() })
|
||||
}
|
||||
|
||||
if (page == SettingsPage.UPDATES)
|
||||
{
|
||||
ShxSection(title = stringResource(R.string.update_settings))
|
||||
{
|
||||
var autoUpdate by remember {
|
||||
mutableStateOf(prefs.getBoolean(Constants.PREF_AUTO_UPDATE_CHECK, true))
|
||||
@@ -733,6 +744,7 @@ enum class SettingsPage(val titleRes: Int)
|
||||
SECURITY(R.string.settings_section_security),
|
||||
UPDATES(R.string.settings_section_updates),
|
||||
SUPPORT(R.string.settings_section_support),
|
||||
KILLSWITCH(R.string.settings_killswitch),
|
||||
}
|
||||
|
||||
/** One row of the settings list (F_Settings): 62dp, 36dp grey icon well, 15/700 title, 12 sub. */
|
||||
@@ -787,7 +799,7 @@ private fun SettingsRoot(onOpen: (SettingsPage) -> Unit, updateState: UpdateStat
|
||||
stringResource(if (lockdown) R.string.settings_killswitch_on else R.string.settings_killswitch_off),
|
||||
ShxIcons.Lock,
|
||||
warn = !lockdown,
|
||||
) { if (lockdown) context.startActivity(Intent(Settings.ACTION_VPN_SETTINGS)) else showKillSwitchDialog = true },
|
||||
) { onOpen(SettingsPage.KILLSWITCH) },
|
||||
SettingsEntry(stringResource(R.string.settings_section_security), stringResource(R.string.settings_security_sub), ShxIcons.Shield) { onOpen(SettingsPage.SECURITY) },
|
||||
),
|
||||
stringResource(R.string.settings_app) to listOf(
|
||||
|
||||
@@ -958,4 +958,29 @@
|
||||
<string name="form_login_cert">Сертификат</string>
|
||||
<string name="form_login_both">Оба</string>
|
||||
<string name="form_more">Дополнительно: порт, MTU, DNS, идентификаторы</string>
|
||||
<string name="appearance_theme">Тема</string>
|
||||
<string name="appearance_wallpaper">Цвета обоев (Material You)</string>
|
||||
<string name="appearance_wallpaper_sub">Вместо фиолетового — цвета вашего телефона. Статусы остаются прежними.</string>
|
||||
<string name="appearance_language">Язык</string>
|
||||
<string name="ks_on_title">Сейчас включено</string>
|
||||
<string name="ks_on_text">Без VPN приложения не выйдут в сеть</string>
|
||||
<string name="ks_off_title">Сейчас выключено</string>
|
||||
<string name="ks_off_text">При обрыве VPN приложения на секунды выйдут в сеть напрямую</string>
|
||||
<string name="ks_intro">Android может блокировать интернет, пока VPN не подключён. Включается в системных настройках — приложение не может сделать это само.</string>
|
||||
<string name="ks_step1">Нажмите кнопку ниже — откроется раздел VPN в настройках Android.</string>
|
||||
<string name="ks_step2">Нажмите на шестерёнку рядом с «SHX VPN».</string>
|
||||
<string name="ks_step3">Включите «Постоянный VPN» и «Блокировать подключения без VPN».</string>
|
||||
<string name="ks_note">Приложения из списка «напрямую» продолжат работать без VPN — Android выпускает их и при блокировке.</string>
|
||||
<string name="ks_open">Открыть настройки Android</string>
|
||||
<string name="update_card_uptodate">У вас последняя версия</string>
|
||||
<string name="update_card_have">У вас %1$s · %2$s · подпись проверена</string>
|
||||
<string name="update_card_checking">Проверяем обновления…</string>
|
||||
<string name="update_card_failed">Не удалось проверить, попробуйте позже</string>
|
||||
<string name="update_card_version">Версия %1$s</string>
|
||||
<string name="update_card_whats_new">Что нового</string>
|
||||
<string name="update_card_downloading">Скачивание</string>
|
||||
<string name="update_card_check">Проверить</string>
|
||||
<string name="update_card_install">Установить</string>
|
||||
<string name="update_card_footer">Текст «Что нового» берётся из заметок релиза</string>
|
||||
<string name="update_settings">Как обновлять</string>
|
||||
</resources>
|
||||
@@ -943,4 +943,29 @@
|
||||
<string name="form_login_cert">Certificate</string>
|
||||
<string name="form_login_both">Both</string>
|
||||
<string name="form_more">More: port, MTU, DNS, identities</string>
|
||||
<string name="appearance_theme">Theme</string>
|
||||
<string name="appearance_wallpaper">Wallpaper colors (Material You)</string>
|
||||
<string name="appearance_wallpaper_sub">Your phone colors instead of purple. Statuses stay the same.</string>
|
||||
<string name="appearance_language">Language</string>
|
||||
<string name="ks_on_title">On now</string>
|
||||
<string name="ks_on_text">Without the VPN apps get no network</string>
|
||||
<string name="ks_off_title">Off now</string>
|
||||
<string name="ks_off_text">When the VPN drops, apps go online directly for a few seconds</string>
|
||||
<string name="ks_intro">Android can block the internet while the VPN is not connected. It is turned on in the system settings: the app cannot do it by itself.</string>
|
||||
<string name="ks_step1">Tap the button below: the VPN section of the Android settings opens.</string>
|
||||
<string name="ks_step2">Tap the gear next to «SHX VPN».</string>
|
||||
<string name="ks_step3">Turn on «Always-on VPN» and «Block connections without VPN».</string>
|
||||
<string name="ks_note">Apps from the «direct» list keep working without the VPN: Android lets them out even when blocking.</string>
|
||||
<string name="ks_open">Open Android settings</string>
|
||||
<string name="update_card_uptodate">You have the latest version</string>
|
||||
<string name="update_card_have">You have %1$s · %2$s · signature checked</string>
|
||||
<string name="update_card_checking">Checking for updates…</string>
|
||||
<string name="update_card_failed">Could not check, try again later</string>
|
||||
<string name="update_card_version">Version %1$s</string>
|
||||
<string name="update_card_whats_new">What is new</string>
|
||||
<string name="update_card_downloading">Downloading</string>
|
||||
<string name="update_card_check">Check</string>
|
||||
<string name="update_card_install">Install</string>
|
||||
<string name="update_card_footer">«What is new» comes from the release notes</string>
|
||||
<string name="update_settings">How to update</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user