android: help screen as in the Final 2026 mockup; log opens directly from settings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-10-12 10:43:54 +07:00
1 parent 8cd704ed08
commit 739068ccb9
4 files changed
+165 -29

No files matched your search

@@ -20,8 +20,10 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -38,6 +40,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -300,3 +303,115 @@ fun UpdateCard(state: UpdateState, installed: String, onCheck: () -> Unit, onIns
Text(stringResource(R.string.update_card_footer), style = ShxType.meta.copy(fontWeight = FontWeight.Normal), color = c.textSub, modifier = Modifier.padding(horizontal = 6.dp)) Text(stringResource(R.string.update_card_footer), style = ShxType.meta.copy(fontWeight = FontWeight.Normal), color = c.textSub, modifier = Modifier.padding(horizontal = 6.dp))
} }
} }
/**
* «Help» (F_Help): two big buttons (check the connection, contact support),
* the frequent questions as an accordion, and «About the app».
*/
@Composable
internal fun HelpPage(onUpdates: () -> Unit)
{
val c = LocalShxColors.current
val context = LocalContext.current
val info = remember { runCatching { context.packageManager.getPackageInfo(context.packageName, 0) }.getOrNull() }
val build = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info?.longVersionCode?.toInt() ?: 0 else @Suppress("DEPRECATION") (info?.versionCode ?: 0)
fun open(url: String) = runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) }
Column(Modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp))
{
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.height(androidx.compose.foundation.layout.IntrinsicSize.Min))
{
HelpTile(ShxIcons.Pulse, stringResource(R.string.help_check), c.accent, Color.White, Modifier.weight(1f).fillMaxHeight()) {
context.startActivity(Intent(context, org.strongswan.android.ui.DiagnosticsActivity::class.java))
}
HelpTile(ShxIcons.Doc, stringResource(R.string.help_write), c.accentContainer, c.onAccentContainer, Modifier.weight(1f).fillMaxHeight()) {
context.startActivity(Intent(context, org.strongswan.android.ui.LogActivity::class.java))
}
}
ShxSectionHeader(stringResource(R.string.help_faq).uppercase(), Modifier.padding(start = 6.dp, end = 6.dp, top = 6.dp))
val faq = listOf(
R.string.faq_q1 to R.string.faq_a1, R.string.faq_q2 to R.string.faq_a2, R.string.faq_q3 to R.string.faq_a3,
R.string.faq_q4 to R.string.faq_a4, R.string.faq_q5 to R.string.faq_a5,
)
var open by remember { mutableStateOf(0) }
ShxPanel()
{
faq.forEachIndexed { i, (q, a) ->
val expanded = open == i
Column(Modifier.fillMaxWidth())
{
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.fillMaxWidth().heightIn(min = 58.dp)
.clickable(role = Role.Button) { open = if (expanded) -1 else i }.padding(horizontal = 16.dp, vertical = 10.dp),
)
{
Text(stringResource(q), style = ShxType.rowTitle.copy(fontSize = 15.sp), color = c.text, modifier = Modifier.weight(1f))
Icon(
ShxIcons.Chevron, contentDescription = null, tint = c.textSub,
modifier = Modifier.size(18.dp).rotate(if (expanded) 270f else 90f),
)
}
if (expanded)
{
Text(
stringResource(a), style = ShxType.note.copy(fontSize = 14.sp, lineHeight = 21.sp, fontWeight = FontWeight.Normal), color = c.textSoft,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
)
}
if (i < faq.lastIndex)
{
Box(Modifier.fillMaxWidth().height(1.dp).background(c.tile))
}
}
}
}
ShxSectionHeader(stringResource(R.string.help_about).uppercase(), Modifier.padding(start = 6.dp, end = 6.dp, top = 6.dp))
ShxPanel()
{
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp), modifier = Modifier.padding(16.dp))
{
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(52.dp).clip(RoundedCornerShape(15.dp)).background(c.accent))
{
Icon(painterResource(R.drawable.ic_widget_logo_accent), contentDescription = null, tint = Color.Unspecified, modifier = Modifier.size(32.dp))
}
Column(verticalArrangement = Arrangement.spacedBy(2.dp))
{
Text("SHX VPN " + (info?.versionName ?: ""), style = ShxType.cardHeading, color = c.text)
Text(stringResource(R.string.help_build, build), style = ShxType.meta.copy(fontWeight = FontWeight.Normal), color = c.textSub)
}
}
HelpLink(stringResource(R.string.help_updates), onUpdates)
HelpLink(stringResource(R.string.help_licenses)) { open("https://www.gnu.org/licenses/old-licenses/gpl-2.0.html") }
HelpLink(stringResource(R.string.help_source)) { open("https://github.com/strongswan/strongswan") }
}
}
}
@Composable
private fun HelpTile(icon: androidx.compose.ui.graphics.vector.ImageVector, text: String, bg: Color, fg: Color, modifier: Modifier, onClick: () -> Unit)
{
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = modifier.heightIn(min = 84.dp).clip(RoundedCornerShape(22.dp)).background(bg).clickable(role = Role.Button, onClick = onClick).padding(14.dp),
)
{
Icon(icon, contentDescription = null, tint = fg, modifier = Modifier.size(22.dp))
Text(text, style = ShxType.body.copy(fontWeight = FontWeight.ExtraBold), color = fg, maxLines = 2)
}
}
@Composable
private fun HelpLink(text: String, onClick: () -> Unit)
{
val c = LocalShxColors.current
Box(Modifier.fillMaxWidth().height(1.dp).background(c.tile))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().height(52.dp).clickable(role = Role.Button, onClick = onClick).padding(horizontal = 16.dp),
)
{
Text(text, style = ShxType.body.copy(fontSize = 15.sp), color = c.text, modifier = Modifier.weight(1f))
Icon(ShxIcons.Chevron, contentDescription = null, tint = c.textSub, modifier = Modifier.size(18.dp))
}
}
@@ -404,35 +404,11 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
} }
} }
if (page == SettingsPage.SUPPORT) if (page == SettingsPage.HELP)
{ {
if (managed.isAllowProfileImport) HelpPage(onUpdates = { pageState = SettingsPage.UPDATES })
ShxSection(title = stringResource(R.string.help_for_support))
{ {
ShxSection(title = stringResource(R.string.settings_section_profiles))
{
ShxSettingRow(
icon = Icons.Rounded.FileDownload,
title = stringResource(R.string.profile_import),
onClick = {
context.startActivity(Intent(context, VpnProfileImportActivity::class.java))
},
)
}
}
}
if (page == SettingsPage.SUPPORT)
{
ShxSection(title = stringResource(R.string.settings_section_support))
{
ShxSettingRow(
icon = Icons.Rounded.Description,
title = stringResource(R.string.show_log),
onClick = {
context.startActivity(Intent(context, LogActivity::class.java))
},
)
ShxSectionDivider()
var extendedDiag by remember { var extendedDiag by remember {
mutableStateOf(prefs.getBoolean(Constants.PREF_EXTENDED_DIAG, false)) mutableStateOf(prefs.getBoolean(Constants.PREF_EXTENDED_DIAG, false))
} }
@@ -744,8 +720,8 @@ enum class SettingsPage(val titleRes: Int)
STABILITY(R.string.settings_stability), STABILITY(R.string.settings_stability),
SECURITY(R.string.settings_section_security), SECURITY(R.string.settings_section_security),
UPDATES(R.string.settings_section_updates), UPDATES(R.string.settings_section_updates),
SUPPORT(R.string.settings_section_support),
KILLSWITCH(R.string.settings_killswitch), KILLSWITCH(R.string.settings_killswitch),
HELP(R.string.settings_help),
} }
/** One row of the settings list (F_Settings): 62dp, 36dp grey icon well, 15/700 title, 12 sub. */ /** One row of the settings list (F_Settings): 62dp, 36dp grey icon well, 15/700 title, 12 sub. */
@@ -832,7 +808,10 @@ private fun SettingsRoot(onOpen: (SettingsPage) -> Unit, updateState: UpdateStat
) { onOpen(SettingsPage.APPEARANCE) }, ) { onOpen(SettingsPage.APPEARANCE) },
), ),
stringResource(R.string.settings_section_support) to listOf( stringResource(R.string.settings_section_support) to listOf(
SettingsEntry(stringResource(R.string.settings_log), stringResource(R.string.settings_log_sub), ShxIcons.Doc) { onOpen(SettingsPage.SUPPORT) }, SettingsEntry(stringResource(R.string.settings_help), stringResource(R.string.settings_help_sub), ShxIcons.Help) { onOpen(SettingsPage.HELP) },
SettingsEntry(stringResource(R.string.settings_log), stringResource(R.string.settings_log_sub), ShxIcons.Doc) {
context.startActivity(Intent(context, LogActivity::class.java))
},
SettingsEntry( SettingsEntry(
stringResource(R.string.trusted_certs_title), stringResource(R.string.trusted_certs_title),
pluralStringResource(R.plurals.settings_certs_n, certs, certs), pluralStringResource(R.plurals.settings_certs_n, certs, certs),
@@ -1036,4 +1036,25 @@
<string name="log_send_report">Отправить отчёт</string> <string name="log_send_report">Отправить отчёт</string>
<string name="log_copy">Скопировать журнал</string> <string name="log_copy">Скопировать журнал</string>
<string name="log_copied">Журнал скопирован</string> <string name="log_copied">Журнал скопирован</string>
<string name="settings_help">Помощь</string>
<string name="settings_help_sub">Частые вопросы, о приложении</string>
<string name="help_check">Проверить подключение</string>
<string name="help_write">Написать в поддержку</string>
<string name="help_faq">Частые вопросы</string>
<string name="help_about">О приложении</string>
<string name="help_build">сборка %1$d · на основе strongSwan · GPLv2</string>
<string name="help_updates">Проверить обновления</string>
<string name="help_licenses">Лицензии открытого ПО</string>
<string name="help_source">Исходный код strongSwan</string>
<string name="help_for_support">Для поддержки</string>
<string name="faq_q1">Банк или Госуслуги не открываются</string>
<string name="faq_a1">Многие российские сервисы не пускают зарубежные адреса. Откройте «Не открывается?» на главной и выберите приложение — мы пустим его напрямую, мимо VPN.</string>
<string name="faq_q2">Где взять профиль .sswan?</string>
<string name="faq_a2">Его выдаёт администратор вашего VPN: файлом, ссылкой или QR-кодом. Можно отсканировать QR с экрана друга.</string>
<string name="faq_q3">VPN сам отключается</string>
<string name="faq_a3">Скорее всего, Android экономит батарею. Откройте Настройки → Стабильная работа и снимите ограничения.</string>
<string name="faq_q4">Стабильный или Гибкий режим?</string>
<string name="faq_a4">Стабильный надёжнее: новые адреса применяются после переподключения. Гибкий обновляет маршруты на лету, но пока в бете.</string>
<string name="faq_q5">Видит ли SHX мои сайты?</string>
<string name="faq_a5">Нет. С доступом к статистике приложение считает только объём трафика по приложениям; сайты и содержимое не сохраняются и никуда не отправляются.</string>
</resources> </resources>
@@ -1022,4 +1022,25 @@
<string name="log_send_report">Send report</string> <string name="log_send_report">Send report</string>
<string name="log_copy">Copy log</string> <string name="log_copy">Copy log</string>
<string name="log_copied">Log copied</string> <string name="log_copied">Log copied</string>
<string name="settings_help">Help</string>
<string name="settings_help_sub">FAQ, about the app</string>
<string name="help_check">Check the connection</string>
<string name="help_write">Contact support</string>
<string name="help_faq">Frequent questions</string>
<string name="help_about">About the app</string>
<string name="help_build">build %1$d · based on strongSwan · GPLv2</string>
<string name="help_updates">Check for updates</string>
<string name="help_licenses">Open source licenses</string>
<string name="help_source">strongSwan source code</string>
<string name="help_for_support">For support</string>
<string name="faq_q1">A bank or Gosuslugi does not open</string>
<string name="faq_a1">Many Russian services reject foreign addresses. Tap «Not loading?» on Home and pick the app: it will go direct, past the VPN.</string>
<string name="faq_q2">Where do I get an .sswan profile?</string>
<string name="faq_a2">Your VPN admin gives it out: as a file, a link or a QR code. You can also scan a QR from a friend\'s screen.</string>
<string name="faq_q3">The VPN turns itself off</string>
<string name="faq_a3">Most likely Android is saving battery. Open Settings → Stable work and lift the restrictions.</string>
<string name="faq_q4">Stable or Flexible mode?</string>
<string name="faq_a4">Stable is more reliable: new addresses apply after a reconnect. Flexible updates routes on the fly, but is still in beta.</string>
<string name="faq_q5">Does SHX see my sites?</string>
<string name="faq_a5">No. With usage access the app only counts traffic per app; sites and content are not stored or sent anywhere.</string>
</resources> </resources>