android: a11y (headings, switch role), form snackbar, German strings, smoke tests, docs
This commit is contained in:
1 parent
3c7c121c7d
commit
203a67ee86
7 files changed
+355
-4
No files matched your search
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2026 SHX VPN
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
package org.strongswan.android.ui.compose
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.strongswan.android.R
|
||||
import org.strongswan.android.ui.compose.theme.ShxTheme
|
||||
|
||||
/** Smoke tests for the redesigned Profiles, Routing and Settings screens. */
|
||||
class ScreensSmokeTest
|
||||
{
|
||||
@get:Rule
|
||||
val composeRule = createComposeRule()
|
||||
|
||||
private fun string(id: Int): String =
|
||||
InstrumentationRegistry.getInstrumentation().targetContext.getString(id)
|
||||
|
||||
@Test
|
||||
fun profilesShowsEmptyState()
|
||||
{
|
||||
composeRule.setContent {
|
||||
ShxTheme {
|
||||
ProfilesScreen(
|
||||
state = HomeUiState(),
|
||||
onConnect = {},
|
||||
onDisconnect = {},
|
||||
onEdit = {},
|
||||
onDelete = {},
|
||||
onRestore = {},
|
||||
onShare = {},
|
||||
onAdd = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
composeRule.onNodeWithText(string(R.string.no_profiles)).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun routingShowsHeroAndAdvancedBlock()
|
||||
{
|
||||
composeRule.setContent {
|
||||
ShxTheme {
|
||||
RoutingScreen()
|
||||
}
|
||||
}
|
||||
composeRule.onNodeWithText(string(R.string.routing_title)).assertIsDisplayed()
|
||||
/* advanced block is collapsed until tapped */
|
||||
composeRule.onNodeWithText(string(R.string.routing_advanced).uppercase()).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun settingsOpensSecurityPageAndGoesBack()
|
||||
{
|
||||
composeRule.setContent {
|
||||
ShxTheme {
|
||||
SettingsScreen(profiles = emptyList())
|
||||
}
|
||||
}
|
||||
composeRule.onNodeWithText(string(R.string.settings_section_security)).performClick()
|
||||
composeRule.waitForIdle()
|
||||
composeRule.onNodeWithText(string(R.string.safe_ipv6_title)).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -55,6 +55,8 @@ class VpnProfileDetailActivity : AppCompatActivity()
|
||||
}
|
||||
}
|
||||
|
||||
private val messages = kotlinx.coroutines.flow.MutableSharedFlow<Int>(extraBufferCapacity = 1)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?)
|
||||
{
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -80,13 +82,14 @@ class VpnProfileDetailActivity : AppCompatActivity()
|
||||
state = formViewModel.state,
|
||||
onChange = { formViewModel.state = it },
|
||||
onCancel = { finish() },
|
||||
messages = messages,
|
||||
onSave = {
|
||||
val state = formViewModel.state
|
||||
val checked = state.validate(this)
|
||||
formViewModel.state = checked
|
||||
if (!checked.isValid())
|
||||
{
|
||||
Toast.makeText(this, R.string.alert_text_no_input_gateway, Toast.LENGTH_SHORT).show()
|
||||
messages.tryEmit(R.string.alert_text_no_input_gateway)
|
||||
return@ProfileFormScreen
|
||||
}
|
||||
val profile = checked.toProfile(existing)
|
||||
|
||||
+8
@@ -297,9 +297,16 @@ fun ProfileFormScreen(
|
||||
onChange: (ProfileFormState) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
/* one-shot messages (string resource ids) shown as a snackbar */
|
||||
messages: kotlinx.coroutines.flow.Flow<Int>? = null,
|
||||
)
|
||||
{
|
||||
val context = LocalContext.current
|
||||
val snackbarHostState = remember { androidx.compose.material3.SnackbarHostState() }
|
||||
androidx.compose.runtime.LaunchedEffect(messages)
|
||||
{
|
||||
messages?.collect { snackbarHostState.showSnackbar(context.getString(it)) }
|
||||
}
|
||||
val types = stringArrayResource(R.array.vpn_types)
|
||||
val appsHandling = stringArrayResource(R.array.apps_handling)
|
||||
var typeExpanded by remember { mutableStateOf(false) }
|
||||
@@ -324,6 +331,7 @@ fun ProfileFormScreen(
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { org.strongswan.android.ui.compose.components.ShxSnackbarHost(snackbarHostState) },
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom),
|
||||
),
|
||||
|
||||
+13
-3
@@ -12,6 +12,10 @@ package org.strongswan.android.ui.compose.components
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -52,7 +56,8 @@ fun ShxSection(title: String, content: @Composable () -> Unit)
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = ShxSpacing.lg + ShxSpacing.lg, top = 20.dp, bottom = ShxSpacing.sm),
|
||||
.padding(start = ShxSpacing.lg + ShxSpacing.lg, top = 20.dp, bottom = ShxSpacing.sm)
|
||||
.semantics { heading() },
|
||||
)
|
||||
ShxCard(modifier = Modifier.fillMaxWidth().padding(horizontal = ShxSpacing.lg)) {
|
||||
Column { content() }
|
||||
@@ -157,13 +162,18 @@ fun ShxSettingToggleRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled) { onCheckedChange(!checked) }
|
||||
.toggleable(
|
||||
value = checked,
|
||||
enabled = enabled,
|
||||
role = Role.Switch,
|
||||
onValueChange = onCheckedChange,
|
||||
)
|
||||
.padding(horizontal = ShxSpacing.lg, vertical = 14.dp),
|
||||
)
|
||||
{
|
||||
ShxIconBadge(icon = icon, tint = if (enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
RowLabels(title, subtitle, enabled)
|
||||
Switch(checked = checked, onCheckedChange = onCheckedChange, enabled = enabled)
|
||||
Switch(checked = checked, onCheckedChange = null, enabled = enabled)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -18,6 +18,8 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import org.strongswan.android.ui.compose.theme.ShxSpacing
|
||||
|
||||
/** Card with an uppercase caption and a padded content column (form groups, detail blocks). */
|
||||
@@ -39,6 +41,7 @@ fun ShxSectionCard(
|
||||
text = title.uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.semantics { heading() },
|
||||
)
|
||||
content()
|
||||
}
|
||||
|
||||
@@ -273,4 +273,235 @@
|
||||
<string name="tile_connect">VPN verbinden</string>
|
||||
<string name="tile_disconnect">VPN trennen</string>
|
||||
|
||||
<string name="pref_dynamic_colors_title">Systemfarben</string>
|
||||
<string name="probe_rtt_fmt">%1$d ms</string>
|
||||
<string name="value_no_data">—</string>
|
||||
<string name="value_unreachable">✕</string>
|
||||
<string name="profile_actions">Profilaktionen</string>
|
||||
<string name="action_clear_search">Suche löschen</string>
|
||||
<string name="action_undo">Rückgängig</string>
|
||||
<string name="action_done">Fertig</string>
|
||||
<string name="action_back">Zurück</string>
|
||||
<string name="profile_deleted_snackbar">Profil gelöscht</string>
|
||||
<string name="action_more_options">Weitere Optionen</string>
|
||||
<string name="back">Zurück</string>
|
||||
<string name="pref_dynamic_colors_summary">Farben des Hintergrundbilds übernehmen (Android 12+)</string>
|
||||
<string name="watchdog_title">Verbindungs-Watchdog</string>
|
||||
<string name="watchdog_summary">Die VPN-Verbindung nach dem Beenden oder Absturz des App-Prozesses automatisch wiederherstellen</string>
|
||||
<string name="samsung_sleep_title">Von Ruhe-Apps ausnehmen (Samsung)</string>
|
||||
<string name="samsung_sleep_summary">One UI kann ungenutzte Apps auch bei deaktivierter Akkuoptimierung in den Ruhezustand versetzen. Im Ruhezustand kann die App die VPN-Verbindung nicht selbst wiederherstellen. Öffnen Sie die Systemeinstellungen → Akku → Einschränkungen für Hintergrundnutzung und fügen Sie diese App unter „Apps, die nie in den Ruhezustand versetzt werden“ hinzu.</string>
|
||||
<string name="update_silent_title">Stille Updates</string>
|
||||
<string name="update_silent_summary">Updates über WLAN vorab herunterladen und automatisch installieren (Android 12+). Andernfalls benachrichtigen, sobald das Update bereit ist</string>
|
||||
<string name="updates_channel_name">App-Updates</string>
|
||||
<string name="update_ready_title">Update zur Installation bereit</string>
|
||||
<string name="update_ready_text">Version %1$s wurde heruntergeladen. Tippen Sie, um die App zu aktualisieren.</string>
|
||||
<string name="profile_split_tunnelingv6_hint">Wenn der Server kein IPv6 hat, verhindert dies, dass IPv6 in Dual-Stack-Netzen am Tunnel vorbei austritt</string>
|
||||
<string name="share_profile">VPN-Profil teilen</string>
|
||||
<string name="share_profile_include_password">Passwort in Datei einschließen</string>
|
||||
<string name="share_profile_password_warning">Die Profildatei enthält die Verbindungseinstellungen. Das Passwort wird unverschlüsselt in der Datei gespeichert, und ein in der App gespeichertes Zertifikat wird mit seinem privaten Schlüssel exportiert. Senden Sie die Datei nur an vertrauenswürdige Personen.</string>
|
||||
<string name="share_profile_failed">VPN-Profil konnte nicht exportiert werden</string>
|
||||
<string name="whitelist_analysis_title">Analyse der ISP-Whitelist</string>
|
||||
<string name="whitelist_analysis_summary">Erkennt eine ISP-Whitelist und pausiert die Wiederverbindung, solange ausländische Hosts direkt nicht erreichbar sind</string>
|
||||
<string name="whitelist_active_title">ISP-Whitelist ist aktiv</string>
|
||||
<string name="whitelist_active_subtitle">Prüfungen laufen direkt außerhalb des Tunnels. Die Wiederverbindung ist pausiert, bis die Whitelist aufgehoben wird</string>
|
||||
<string name="whitelist_direct_section">Direkt (außerhalb des Tunnels)</string>
|
||||
<string name="whitelist_foreign_direct">Ausländische Hosts (direkt)</string>
|
||||
<string name="whitelist_domestic_direct">Yandex (direkt)</string>
|
||||
<string name="nav_routing">Routing</string>
|
||||
<string name="routing_title">Routing</string>
|
||||
<string name="routing_section_ru">RU-Netze direkt</string>
|
||||
<string name="routing_rf_quick_title">Apps von RF-Unternehmen ausnehmen</string>
|
||||
<string name="routing_rf_quick_summary">Alle ru.*-Apps, Yandex, VK, Gosuslugi, große Banken, Handel, Telekommunikation, Verkehr und weitere russische Dienste</string>
|
||||
<string name="routing_force_section">Immer über VPN</string>
|
||||
<string name="routing_force_summary">Adressen und Subnetze, die immer durch den Tunnel geleitet werden, auch wenn sie in RU-Bereiche fallen (Userspace-Modus)</string>
|
||||
<string name="routing_force_add">Adresse hinzufügen</string>
|
||||
<string name="routing_force_dialog_title">Adresse oder Subnetz</string>
|
||||
<string name="routing_force_hint">z. B. 130.49.213.176 oder 10.8.0.0/24</string>
|
||||
<string name="routing_force_invalid">Ungültige Adresse oder ungültiges Subnetz</string>
|
||||
<string name="routing_force_save">Speichern</string>
|
||||
<string name="routing_force_delete">Löschen</string>
|
||||
<string name="routing_rules_section">Regelsätze</string>
|
||||
<string name="routing_add_rule">Regelsatz hinzufügen</string>
|
||||
<string name="routing_rule_name">Name des Regelsatzes</string>
|
||||
<string name="routing_rule_subtitle">Apps: %1$d · Präfixe: %2$d · Schlüsselwörter: %3$d</string>
|
||||
<string name="routing_pick_apps">Anwendungen auswählen</string>
|
||||
<string name="routing_apps_count">Ausgewählt: %1$d</string>
|
||||
<string name="routing_editor_title">Regelsatz</string>
|
||||
<string name="routing_prefixes_label">Paket-Präfixe</string>
|
||||
<string name="routing_keywords_label">Schlüsselwörter</string>
|
||||
<string name="routing_details_more">Details</string>
|
||||
<string name="routing_details_less">Details ausblenden</string>
|
||||
<string name="routing_presets_label">Vorlagen</string>
|
||||
<string name="routing_presets_selected">Vorlagen · %1$d ausgewählt</string>
|
||||
<string name="routing_presets_hint">Fertige Gruppen von Diensten</string>
|
||||
<string name="routing_presets_search">Vorlagen durchsuchen</string>
|
||||
<string name="routing_presets_none">Nichts gefunden</string>
|
||||
<string name="routing_preset_prefixes">%1$d Präfixe</string>
|
||||
<string name="routing_preset_keywords">%1$d Schlüsselwörter</string>
|
||||
<string name="routing_preset_no_patterns">keine Muster</string>
|
||||
<string name="routing_empty">Noch keine Regelsätze vorhanden. Legen Sie einen an oder nutzen Sie die Schnellaktion oben.</string>
|
||||
<string name="routing_delete">Regelsatz löschen</string>
|
||||
<string name="routing_delete_snackbar">Regelsatz gelöscht</string>
|
||||
<string name="routing_add_prefix">Paket-Präfix hinzufügen</string>
|
||||
<string name="routing_add_keyword">Schlüsselwort hinzufügen</string>
|
||||
<string name="routing_hero_on">Routing aktiv</string>
|
||||
<string name="routing_hero_off">Routing inaktiv</string>
|
||||
<string name="routing_hero_summary">Sets: %1$d · Apps direkt: %2$d</string>
|
||||
<string name="routing_hero_off_summary">Gesamter Datenverkehr läuft über das VPN</string>
|
||||
<string name="routing_geoip_ipv4">IPv4 · %1$d</string>
|
||||
<string name="routing_geoip_ipv6">IPv6 · %1$d</string>
|
||||
<string name="routing_geoip_updated">Aktualisiert %1$s</string>
|
||||
<string name="routing_geoip_never">Nie aktualisiert</string>
|
||||
<string name="rusplit_toggle_title">RU-Netze direkt verbinden</string>
|
||||
<string name="rusplit_toggle_summary">Datenverkehr zu russischen IP-Adressen umgeht den VPN-Tunnel und nutzt die direkte Verbindung (geoip:ru)</string>
|
||||
<string name="rusplit_update">Liste aktualisieren</string>
|
||||
<string name="rusplit_updating">Wird aktualisiert…</string>
|
||||
<string name="rusplit_updated">Liste aktualisiert: %1$d Bereiche</string>
|
||||
<string name="rusplit_update_failed">Aktualisierung nicht möglich, gespeicherte Version wird verwendet</string>
|
||||
<string name="rusplit_source">Quelle: github.com/MetaCubeX/meta-rules-dat</string>
|
||||
<string name="rusplit_beta_note">Die Liste ist statisch und wird täglich von GitHub aktualisiert. Die Zuordnung erfolgt über IP-Adressen. Dienste hinter ausländischen CDNs sind möglicherweise nicht in der Liste enthalten. Änderungen werden nach einer erneuten Verbindung wirksam.</string>
|
||||
<string name="rusplit_mode_system">Betriebssystem-Routen</string>
|
||||
<string name="rusplit_mode_userspace">Userspace-Routing (Beta)</string>
|
||||
<string name="rusplit_mode_userspace_short">Userspace (Beta)</string>
|
||||
<string name="rusplit_mode_system_desc">Das Komplement der RU-Liste wird als Systemrouten installiert und auf das Routenlimit gerundet. Bewährtes Verfahren, die Liste wird dabei jedoch vergröbert, und Aktualisierungen erfordern eine erneute Verbindung.</string>
|
||||
<string name="rusplit_mode_userspace_desc">Der gesamte Datenverkehr geht in das VPN. Die Entscheidung Direkt/Tunnel wird pro Paket anhand der genauen Liste getroffen. Listenaktualisierungen werden sofort übernommen. TCP und UDP gehen direkt. Ping zu RU-Adressen funktioniert in diesem Modus nicht direkt, anderes ICMP wird verworfen.</string>
|
||||
<string name="route_check_title">Routenprüfung</string>
|
||||
<string name="route_check_hint">Geben Sie eine Domain ein, um zu sehen, auf welche Adressen sie über den DNS des Tunnels und den DNS der direkten Verbindung aufgelöst wird und ob jede Adresse unter den angewendeten Ausnahmen durch den Tunnel läuft oder ihn umgeht.</string>
|
||||
<string name="route_check_field">Domain, z. B. inet.ya.ru</string>
|
||||
<string name="route_check_button">Prüfen</string>
|
||||
<string name="route_check_checking">Wird geprüft…</string>
|
||||
<string name="route_check_no_vpn">Der Tunnel ist nicht verbunden. Die Routenansicht ist nicht verfügbar</string>
|
||||
<string name="route_check_via_vpn_dns">Über den DNS des Tunnels aufgelöst</string>
|
||||
<string name="route_check_via_direct_dns">Über den DNS der direkten Verbindung aufgelöst</string>
|
||||
<string name="route_check_routes">Routen</string>
|
||||
<string name="route_verdict_tunnel">über den Tunnel</string>
|
||||
<string name="route_verdict_direct">direkt (RU-Ausnahme)</string>
|
||||
<string name="route_verdict_dns">direkt (privater DNS-Server)</string>
|
||||
<string name="route_check_resolve_failed">Die Domain konnte auf keinem der beiden Pfade aufgelöst werden</string>
|
||||
<string name="route_check_none">keine</string>
|
||||
<string name="vpn_connection_title">Verbindung</string>
|
||||
<string name="vpn_profiles_title">Profile</string>
|
||||
<string name="login_show_password">Passwort anzeigen</string>
|
||||
<string name="login_hide_password">Passwort ausblenden</string>
|
||||
<string name="nav_connect">Verbinden</string>
|
||||
<string name="nav_profiles">Profile</string>
|
||||
<string name="nav_settings">Einstellungen</string>
|
||||
<string name="status_unprotected">Ungeschützt</string>
|
||||
<string name="status_protected">Geschützt</string>
|
||||
<string name="status_connecting">Verbindung wird hergestellt…</string>
|
||||
<string name="status_choose_profile">Profil auswählen</string>
|
||||
<string name="badge_connected">Verbunden</string>
|
||||
<string name="connection_failed">Verbindung fehlgeschlagen</string>
|
||||
<string name="profiles_title">Profile</string>
|
||||
<string name="search_profiles">Profile durchsuchen</string>
|
||||
<string name="nothing_found">Nichts gefunden</string>
|
||||
<string name="settings_section_general">Allgemein</string>
|
||||
<string name="settings_section_automation">Automatisierung</string>
|
||||
<string name="settings_section_certs">Zertifikate</string>
|
||||
<string name="settings_section_profiles">Profile</string>
|
||||
<string name="settings_section_support">Support</string>
|
||||
<string name="settings_section_security">Sicherheit</string>
|
||||
<string name="auto_connect_title">Vertrauenswürdige Netzwerke</string>
|
||||
<string name="auto_connect_summary">Über Mobilfunk und nicht vertrauenswürdiges WLAN verbinden</string>
|
||||
<string name="auto_connect_profile">Profil für automatische Verbindung</string>
|
||||
<string name="auto_connect_profile_none">Nicht ausgewählt</string>
|
||||
<string name="auto_connect_trusted_title">Vertrauenswürdige WLAN-Netzwerke</string>
|
||||
<string name="auto_connect_trusted_hint">In diesen Netzwerken wird das VPN nur dann automatisch beendet, wenn es durch die Automatisierung gestartet wurde. Eine manuell gestartete Verbindung bleibt bestehen, bis Sie sie trennen. Wenn Sie zu Mobilfunk oder in ein anderes WLAN wechseln, wird das VPN erneut gestartet.</string>
|
||||
<string name="auto_connect_no_networks">Noch keine vertrauenswürdigen Netzwerke</string>
|
||||
<string name="auto_connect_add_network">Netzwerk hinzufügen</string>
|
||||
<string name="auto_connect_add_current">Aktuelles hinzufügen (%1$s)</string>
|
||||
<string name="auto_connect_ssid_label">Netzwerkname (SSID)</string>
|
||||
<string name="auto_connect_remove_network">Vertrauenswürdiges Netzwerk entfernen</string>
|
||||
<string name="auto_connect_location_banner">Damit das VPN in vertrauenswürdigen WLAN-Netzwerken im Hintergrund pausiert werden kann, erlauben Sie den Standortzugriff immer.</string>
|
||||
<string name="auto_connect_location_grant">Erlauben</string>
|
||||
<string name="kill_switch_title">Kill-Switch</string>
|
||||
<string name="kill_switch_summary_on">Always-on-VPN mit Lockdown ist aktiv</string>
|
||||
<string name="kill_switch_summary_off">Nicht aktiviert. Datenverkehr kann außerhalb des Tunnels austreten</string>
|
||||
<string name="safe_ipv6_title">Sicheres IPv6</string>
|
||||
<string name="safe_ipv6_summary">Den IPv6-Verkehr blockieren, wenn der Tunnel kein IPv6 bereitstellt, statt ihn am VPN vorbeizulassen</string>
|
||||
<string name="kill_switch_open_settings">Systemeinstellungen für VPN öffnen</string>
|
||||
<string name="kill_switch_dialog_title">Kill-Switch aktivieren?</string>
|
||||
<string name="kill_switch_dialog_text">Der Kill-Switch ist die Systemoption „Always-on VPN“ mit „Verbindungen ohne VPN blockieren“ für diese App. Fällt der Tunnel aus, wird der gesamte Datenverkehr blockiert, statt auszutreten. Er kann nur in den Systemeinstellungen aktiviert werden. Hinweis: „Always-on VPN“ steht im Konflikt mit der automatischen Verbindung in vertrauenswürdigen Netzwerken, da das System den Tunnel wiederherstellen würde, den die automatische Verbindung im vertrauenswürdigen WLAN pausiert. Verwenden Sie nur eins von beidem.</string>
|
||||
<string name="kill_switch_dialog_enable">Einstellungen öffnen</string>
|
||||
<string name="availability_title">Erreichbarkeit</string>
|
||||
<string name="availability_refresh">Erreichbarkeit erneut prüfen</string>
|
||||
<string name="availability_measuring">Wird gemessen…</string>
|
||||
<string name="probe_target_server">Server (direkt)</string>
|
||||
<string name="probe_target_google">Google</string>
|
||||
<string name="probe_target_youtube">YouTube</string>
|
||||
<string name="probe_target_cloudflare">Cloudflare</string>
|
||||
<string name="health_check_title">Verbindungsprüfung</string>
|
||||
<string name="health_check_summary">Tunnel überwachen und automatisch neu verbinden</string>
|
||||
<string name="health_check_interval">Prüfintervall</string>
|
||||
<string name="health_check_interval_seconds">Alle %1$d s</string>
|
||||
<string name="health_check_interval_minutes">Alle %1$d Min.</string>
|
||||
<string name="health_check_interval_aggressive">Alle 5 s (aggressiver Modus)</string>
|
||||
<string name="health_check_aggressive_title">Aggressiver Modus</string>
|
||||
<string name="health_check_aggressive_summary">Alle 5 s prüfen und ohne Bestätigung neu verbinden. Schnellere Wiederherstellung, aber höherer Akkuverbrauch und die Gefahr von Fehlverbindungen</string>
|
||||
<string name="health_check_adaptive_title">Adaptiver Akkumodus</string>
|
||||
<string name="health_check_adaptive_summary">Seltener prüfen bei ausgeschaltetem Bildschirm, im Energiesparmodus oder bei niedrigem Akkustand</string>
|
||||
<string name="health_check_targets_title">Prüfziele</string>
|
||||
<string name="health_check_server_title">VPN-Server prüfen</string>
|
||||
<string name="health_check_server_summary">Server direkt außerhalb des Tunnels anpingen</string>
|
||||
<string name="health_check_server_down_title">Server nicht erreichbar</string>
|
||||
<string name="health_check_tunnel_dead_title">Internet über VPN nicht erreichbar</string>
|
||||
<string name="health_check_partial_title">Einige Ziele nicht erreichbar</string>
|
||||
<string name="health_check_mode">Wann prüfen</string>
|
||||
<string name="health_check_mode_always">Immer</string>
|
||||
<string name="health_check_mode_with_tunnel">Nur wenn der Tunnel ausgefallen ist</string>
|
||||
<string name="health_check_mode_off">Nie</string>
|
||||
<string name="health_check_mode_short_always">Immer</string>
|
||||
<string name="health_check_mode_short_tunnel_down">Bei Tunnelausfall</string>
|
||||
<string name="health_check_mode_short_never">Nie</string>
|
||||
<string name="health_check_action">Aktion</string>
|
||||
<string name="health_action_reconnect">Neu verbinden</string>
|
||||
<string name="health_action_ignore">Nichts tun</string>
|
||||
<string name="health_check_max_attempts">Verbindungsversuche</string>
|
||||
<string name="health_check_attempt_minus">Versuche verringern</string>
|
||||
<string name="health_check_attempt_plus">Versuche erhöhen</string>
|
||||
<string name="profile_section_basic">Grundlagen</string>
|
||||
<string name="app_version_fmt">SHX VPN %1$s</string>
|
||||
<string name="recents_title">Zuletzt verwendet</string>
|
||||
<string name="add_profile_cta">Profil hinzufügen</string>
|
||||
<string name="connection_details">Verbindungsdetails</string>
|
||||
<string name="traffic_down">Empfang</string>
|
||||
<string name="traffic_up">Senden</string>
|
||||
<string name="traffic_idle">—</string>
|
||||
<string name="duration_label">Dauer</string>
|
||||
<string name="traffic_stat_down">Empfangen %1$s, %2$s</string>
|
||||
<string name="traffic_stat_up">Gesendet %1$s, %2$s</string>
|
||||
<string name="traffic_split_tunnel">Über VPN</string>
|
||||
<string name="traffic_split_direct">Direkt (RU)</string>
|
||||
<string name="traffic_direct_share_fmt">%1$d%% direkt</string>
|
||||
<string name="exit_ip_label">Exit-IP</string>
|
||||
<string name="network_label">Netzwerk</string>
|
||||
<string name="network_wifi">WLAN</string>
|
||||
<string name="network_cellular">Mobile Daten</string>
|
||||
<string name="dead_targets_title">Nicht erreichbare direkte Hosts</string>
|
||||
<string name="dead_targets_summary">Apps verbinden sich weiterhin mit diesen Hosts, doch sie antworten nie. Die Engine lehnt sie für ein Cooldown-Fenster ab. Lange nicht erreichbare Hosts werden bis zum Netzwerkwechsel automatisch über den VPN-Tunnel geleitet. Prüfen Sie die Server oder entfernen Sie die Hosts aus der App, die sie verwendet.</string>
|
||||
<string name="dead_targets_fails_fmt">%1$d fehlgeschlagene Verbindungen</string>
|
||||
<string name="dead_targets_cooldown_fmt">Cooldown %1$s</string>
|
||||
<string name="dead_targets_more_fmt">+%1$d weitere</string>
|
||||
<string name="dead_targets_open">Routing öffnen</string>
|
||||
<string name="dead_targets_via_tunnel">über Tunnel</string>
|
||||
<string name="dead_targets_force">Über VPN</string>
|
||||
<string name="profile_advanced_hint">Kennungen, DNS, MTU, Split-Tunneling</string>
|
||||
<string name="import_profile_title">Profil importieren</string>
|
||||
<string name="extended_diag_title">Erweiterte Diagnose</string>
|
||||
<string name="extended_diag_summary">Regelmäßige Snapshots der Routing-Diagnose während der Verbindung (alle 5 Min.). Erhöht den Akkuverbrauch</string>
|
||||
<string name="home_profile_picker_title">Profil auswählen</string>
|
||||
<string name="home_manage_profiles">Profile verwalten</string>
|
||||
<string name="availability_expand">Details anzeigen</string>
|
||||
<string name="availability_collapse">Details ausblenden</string>
|
||||
<string name="routing_advanced">Erweitert</string>
|
||||
<string name="routing_killswitch_note">%1$d ausgeschlossene App(s) nutzen immer das normale Netzwerk, auch wenn der Tunnel ausfällt. Der Kill-Switch blockiert sie nicht.</string>
|
||||
<string name="settings_page_appearance">Darstellung</string>
|
||||
<string name="settings_quick">Schnelleinstellungen</string>
|
||||
<string name="pref_theme_title">Design</string>
|
||||
<string name="theme_system">System</string>
|
||||
<string name="theme_light">Hell</string>
|
||||
<string name="theme_dark">Dunkel</string>
|
||||
<string name="widget_label">SHX VPN</string>
|
||||
<string name="widget_description">Schutzstatus und Verbinden mit einem Tipp</string>
|
||||
|
||||
</resources>
|
||||
@@ -130,3 +130,18 @@
|
||||
5. Авторизованная проверка `aws s3 cp s3://shx-app/latest.json -` — совпадение versionCode и sha256. Публичный `GET` = `AccessDenied` — норма.
|
||||
|
||||
Релизы: №1 после этапа 2 (versionCode 117, 2.10.0), финальный после этапов 3–4 (118).
|
||||
|
||||
## Редизайн 2.15 (ветка interface-ux-ui-2026): статус по факту
|
||||
|
||||
Этапы 0–4 выше устарели в части ограничений: minSdk уже 23, Expressive-тема включена. Что сделано:
|
||||
|
||||
- **Тулчейн:** AGP 9.1.1, Gradle 9.3.1, Kotlin 2.2.21 (built-in Kotlin, `kotlin-android` убран), compileSdk 37 (targetSdk 36), Java 11, BOM 2026.09.00 (compose 1.12), **material3 1.5.0-beta01**. Библиотеки material3 1.5 объявляют minSdk 24: приложение держит 23 через `tools:overrideLibrary` в манифесте (на API 23 возможны сбои — риск принят). `proguard-android-optimize.txt` вместо non-optimize (AGP 9 отвергает старый файл).
|
||||
- **Тема (этап 1) ✅:** `MaterialExpressiveTheme` + `MotionScheme.expressive()`, тональная палитра с `surfaceContainer*`, токены `ShxSpacing`/`ShxShapes`, выбор темы системная/светлая/тёмная (`ShxThemeSettings.themeMode`), сплэш/окно/системные панели совпадают с Compose-поверхностью (`shx_*` в `colors.xml`).
|
||||
- **Компоненты ✅:** `ShxSectionCard`, `ShxListRow`, `ShxStatusPill`/`ShxConnectedPill`, `ShxEmptyState`, `ShxSnackbarHost`; переходы между вкладками — fade-through на motion scheme, «назад» с любой вкладки ведёт на «Подключение», `LoadingIndicator`/`LinearWavyProgressIndicator`, Snackbar вместо Toast в `ShxApp`, Настройках, форме профиля; хаптика подключения/отключения.
|
||||
- **«Подключение» ✅:** hero-кнопка с морфингом формы (круг → cookie), чип профиля с exit-IP + выбор профиля в bottom sheet (дублирующий список профилей убран), плитки трафика, один слот баннера (ошибка приоритетнее белого списка), «Доступность» свёрнута в сводку. Файл разбит на `home/*`. На ширине ≥ 840dp две колонки.
|
||||
- **«Профили» ✅:** свайп-удаление с Undo, поиск только при > 5 профилей. List-detail **не сделан** (форма профиля остаётся отдельной Activity).
|
||||
- **«Маршрутизация» ✅:** режим/GeoIP/force-префиксы в блоке «Дополнительно»; строка про kill switch и исключённые приложения (исключённые идут мимо VPN, lockdown их не блокирует: они в `addDisallowedApplication`). Разбито на `routing/*`.
|
||||
- **«Настройки» ✅:** корень (быстрые: kill switch, автоподключение) + 5 страниц (оформление, автоматизация, безопасность, обновления, поддержка), ширина ≤ 640dp.
|
||||
- **Системные поверхности:** виджет Glance 2×1 (`ui/widget/ShxWidget.kt`, состояние публикует `VpnStateService`, кнопка идёт через `VpnProfileControlActivity`, как плитка); плитка QS показывает профиль в subtitle. `ProgressStyle` для загрузки обновления и категория плитки connectivity — **не сделаны**.
|
||||
- **Доступность/тексты:** `heading()` у заголовков секций, `toggleable(role = Switch)` у строк-переключателей, liveRegion у статуса подключения, недостающие строки `values-de`.
|
||||
- **Не сделано (вторичные экраны):** ImportScreen как bottom sheet, фильтр уровней и моноширинный шрифт в журнале, поиск по приложениям, карточка разрешения геолокации в «Доверенных сетях». Toast остался в `LogActivity`, `VpnProfileControlActivity`, `UpdateInstallStatusReceiver` (вне Compose-хоста).
|
||||
Reference in new issue
Block a user