android: Home scenario hints as in the Final 2026 mockup (trusted network pill, rule suggestion), profile update from the error and expiry cards, a stop button on errors
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1a6fb85259
commit
dcf8ace6e1
6 files changed
+282
-3
No files matched your search
+24
-1
@@ -263,6 +263,14 @@ fun HomeScreen(
|
|||||||
val today by produceState<org.strongswan.android.logic.stats.DayTraffic?>(null, historyChanges) {
|
val today by produceState<org.strongswan.android.logic.stats.DayTraffic?>(null, historyChanges) {
|
||||||
value = withContext(Dispatchers.IO) { org.strongswan.android.logic.stats.TrafficHistory.today(context) }
|
value = withContext(Dispatchers.IO) { org.strongswan.android.logic.stats.TrafficHistory.today(context) }
|
||||||
}
|
}
|
||||||
|
val ssid by viewModel.ssid.collectAsStateWithLifecycle()
|
||||||
|
var hintTick by remember { mutableStateOf(0) }
|
||||||
|
val hint by produceState<org.strongswan.android.ui.compose.home.ScenarioHint?>(null, ssid, state.vpnState, hintTick) {
|
||||||
|
value = withContext(Dispatchers.IO) { org.strongswan.android.ui.compose.home.ScenarioHints.hint(context, ssid) }
|
||||||
|
}
|
||||||
|
val extras by remember { org.strongswan.android.logic.profiles.ProfileExtras.flow(context) }.collectAsStateWithLifecycle()
|
||||||
|
val shownUuid = (state.activeProfile ?: selected)?.getUUID()?.toString()
|
||||||
|
val hasLink = shownUuid != null && extras[shownUuid]?.source == org.strongswan.android.logic.profiles.ProfileSourceType.LINK
|
||||||
val model = HomeModel(
|
val model = HomeModel(
|
||||||
state = state,
|
state = state,
|
||||||
traffic = traffic,
|
traffic = traffic,
|
||||||
@@ -280,6 +288,8 @@ fun HomeScreen(
|
|||||||
certExpiry = certExpiry,
|
certExpiry = certExpiry,
|
||||||
connectStep = connectStep,
|
connectStep = connectStep,
|
||||||
today = today,
|
today = today,
|
||||||
|
hint = hint,
|
||||||
|
hasLink = hasLink,
|
||||||
)
|
)
|
||||||
val actions = HomeActions(
|
val actions = HomeActions(
|
||||||
connect = { selected?.let { viewModel.connect(context, it) } },
|
connect = { selected?.let { viewModel.connect(context, it) } },
|
||||||
@@ -320,8 +330,21 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
details = { viewModel.openDetails(true) },
|
details = { viewModel.openDetails(true) },
|
||||||
certUpdate = { ShxNavigation.requestAddServer() },
|
certUpdate = {
|
||||||
|
if (hasLink && shownUuid != null) context.startActivity(org.strongswan.android.ui.ServerActivity.intent(context, shownUuid, checkUpdate = true))
|
||||||
|
else ShxNavigation.requestAddServer()
|
||||||
|
},
|
||||||
|
updateProfile = { shownUuid?.let { context.startActivity(org.strongswan.android.ui.ServerActivity.intent(context, it, checkUpdate = true)) } },
|
||||||
stats = { context.startActivity(Intent(context, org.strongswan.android.ui.StatsActivity::class.java)) },
|
stats = { context.startActivity(Intent(context, org.strongswan.android.ui.StatsActivity::class.java)) },
|
||||||
|
scenarios = { context.startActivity(Intent(context, org.strongswan.android.ui.ScenariosActivity::class.java)) },
|
||||||
|
suggestYes = {
|
||||||
|
ssid?.let { org.strongswan.android.ui.compose.home.ScenarioHints.accept(context, it, selected?.getUUID()?.toString()) }
|
||||||
|
hintTick++
|
||||||
|
},
|
||||||
|
suggestNo = {
|
||||||
|
ssid?.let { org.strongswan.android.ui.compose.home.ScenarioHints.decline(context, it) }
|
||||||
|
hintTick++
|
||||||
|
},
|
||||||
certLater = {
|
certLater = {
|
||||||
certAlias?.let { snoozeCertExpiry(context, it) }
|
certAlias?.let { snoozeCertExpiry(context, it) }
|
||||||
certSnoozes++
|
certSnoozes++
|
||||||
|
|||||||
+9
@@ -179,6 +179,10 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V
|
|||||||
/* (Wi-Fi up, mobile data up) for the "no internet" checklist */
|
/* (Wi-Fi up, mobile data up) for the "no internet" checklist */
|
||||||
private val _networkKinds = MutableStateFlow(false to false)
|
private val _networkKinds = MutableStateFlow(false to false)
|
||||||
val networkKinds: StateFlow<Pair<Boolean, Boolean>> = _networkKinds
|
val networkKinds: StateFlow<Pair<Boolean, Boolean>> = _networkKinds
|
||||||
|
|
||||||
|
/* the current Wi-Fi network (null off Wi-Fi or without location access) */
|
||||||
|
private val _ssid = MutableStateFlow<String?>(null)
|
||||||
|
val ssid: StateFlow<String?> = _ssid
|
||||||
private val underlyingNetworks = HashMap<Network, NetworkCapabilities?>()
|
private val underlyingNetworks = HashMap<Network, NetworkCapabilities?>()
|
||||||
private val networkCallback = object : ConnectivityManager.NetworkCallback()
|
private val networkCallback = object : ConnectivityManager.NetworkCallback()
|
||||||
{
|
{
|
||||||
@@ -216,6 +220,8 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V
|
|||||||
val caps = underlyingNetworks.values.filterNotNull()
|
val caps = underlyingNetworks.values.filterNotNull()
|
||||||
_networkKinds.value = caps.any { it.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } to
|
_networkKinds.value = caps.any { it.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } to
|
||||||
caps.any { it.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) }
|
caps.any { it.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) }
|
||||||
|
val wifi = caps.firstOrNull { it.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) }
|
||||||
|
_ssid.value = wifi?.let { org.strongswan.android.logic.autoconnect.SsidReader.from(getApplication(), it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* true while «Fastest» pings the servers */
|
/* true while «Fastest» pings the servers */
|
||||||
@@ -436,6 +442,7 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V
|
|||||||
|
|
||||||
fun connect(context: Context, profile: VpnProfile)
|
fun connect(context: Context, profile: VpnProfile)
|
||||||
{
|
{
|
||||||
|
org.strongswan.android.ui.compose.home.ScenarioHints.manualOn(context)
|
||||||
val intent = Intent(context, VpnProfileControlActivity::class.java)
|
val intent = Intent(context, VpnProfileControlActivity::class.java)
|
||||||
intent.action = VpnProfileControlActivity.START_PROFILE
|
intent.action = VpnProfileControlActivity.START_PROFILE
|
||||||
intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_UUID, profile.getUUID().toString())
|
intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_UUID, profile.getUUID().toString())
|
||||||
@@ -444,6 +451,8 @@ class HomeViewModel(app: Application) : AndroidViewModel(app), VpnStateService.V
|
|||||||
|
|
||||||
fun disconnect()
|
fun disconnect()
|
||||||
{
|
{
|
||||||
|
/* counted per Wi-Fi for the «turn it off here automatically?» suggestion */
|
||||||
|
org.strongswan.android.ui.compose.home.ScenarioHints.manualOff(getApplication(), if (_networkKinds.value.first) _ssid.value else null)
|
||||||
service?.disconnect()
|
service?.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-2
@@ -15,6 +15,10 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -66,6 +70,10 @@ internal data class HomeModel(
|
|||||||
val connectStep: Int = 1,
|
val connectStep: Int = 1,
|
||||||
/* today's traffic from the history */
|
/* today's traffic from the history */
|
||||||
val today: org.strongswan.android.logic.stats.DayTraffic? = null,
|
val today: org.strongswan.android.logic.stats.DayTraffic? = null,
|
||||||
|
/* network rules while off (F_HomeScenario / F_ScenarioSuggest) */
|
||||||
|
val hint: ScenarioHint? = null,
|
||||||
|
/* the shown profile came from a link (it can be re-read) */
|
||||||
|
val hasLink: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
/** What Home can do. */
|
/** What Home can do. */
|
||||||
@@ -90,6 +98,10 @@ internal class HomeActions(
|
|||||||
val certUpdate: () -> Unit = {},
|
val certUpdate: () -> Unit = {},
|
||||||
val certLater: () -> Unit = {},
|
val certLater: () -> Unit = {},
|
||||||
val stats: () -> Unit = {},
|
val stats: () -> Unit = {},
|
||||||
|
val scenarios: () -> Unit = {},
|
||||||
|
val suggestYes: () -> Unit = {},
|
||||||
|
val suggestNo: () -> Unit = {},
|
||||||
|
val updateProfile: () -> Unit = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Two-letter badge of a server: the exit country when known, else the profile initials. */
|
/** Two-letter badge of a server: the exit country when known, else the profile initials. */
|
||||||
@@ -199,7 +211,18 @@ internal fun HomeContent(m: HomeModel, a: HomeActions)
|
|||||||
else ->
|
else ->
|
||||||
{
|
{
|
||||||
HeroBlob(heroLookIdle(), ShxIcons.Power, stringResource(R.string.connect), a.connect, enabled = profile != null)
|
HeroBlob(heroLookIdle(), ShxIcons.Power, stringResource(R.string.connect), a.connect, enabled = profile != null)
|
||||||
HeroCaption(stringResource(R.string.home_state_off), stringResource(R.string.home_state_off_status))
|
val hint = m.hint
|
||||||
|
when
|
||||||
|
{
|
||||||
|
hint?.trustedHere == true -> Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp))
|
||||||
|
{
|
||||||
|
Text(stringResource(R.string.home_state_off), style = ShxType.heroState, color = c.text)
|
||||||
|
ScenarioPill(stringResource(R.string.hint_scenario_pill, hint.ssid), a.scenarios)
|
||||||
|
Text(stringResource(if (hint.autoOnLeave) R.string.hint_on_leave else R.string.hint_trusted_off), style = ShxType.sub, color = c.textSub)
|
||||||
|
}
|
||||||
|
hint?.manualOffHere == true -> HeroCaption(stringResource(R.string.home_state_off), stringResource(R.string.hint_manual_off, hint.ssid), dot = c.iconMuted)
|
||||||
|
else -> HeroCaption(stringResource(R.string.home_state_off), stringResource(R.string.home_state_off_status))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,6 +239,8 @@ internal fun HomeContent(m: HomeModel, a: HomeActions)
|
|||||||
connected && m.exit?.ip != null ->
|
connected && m.exit?.ip != null ->
|
||||||
if (m.serverRttMs != null) stringResource(R.string.home_server_ip_rtt, maskIp(m.exit.ip), m.serverRttMs)
|
if (m.serverRttMs != null) stringResource(R.string.home_server_ip_rtt, maskIp(m.exit.ip), m.serverRttMs)
|
||||||
else stringResource(R.string.home_server_ip, maskIp(m.exit.ip))
|
else stringResource(R.string.home_server_ip, maskIp(m.exit.ip))
|
||||||
|
!connected && !connecting && m.hint?.autoOnLeave == true -> stringResource(R.string.hint_server_on_leave, m.hint.ssid)
|
||||||
|
!connected && !connecting && m.hint?.manualOffHere == true -> stringResource(R.string.hint_last_server)
|
||||||
else -> profile?.gateway ?: ""
|
else -> profile?.gateway ?: ""
|
||||||
}
|
}
|
||||||
ServerCard(serverCode(profile, m.exit), name, details, stringResource(R.string.home_server_change), a.pickServer)
|
ServerCard(serverCode(profile, m.exit), name, details, stringResource(R.string.home_server_change), a.pickServer)
|
||||||
@@ -296,13 +321,27 @@ internal fun HomeContent(m: HomeModel, a: HomeActions)
|
|||||||
HomeProblem.SERVER_CERT -> R.string.home_problem_cert_text
|
HomeProblem.SERVER_CERT -> R.string.home_problem_cert_text
|
||||||
else -> R.string.home_problem_generic_text
|
else -> R.string.home_problem_generic_text
|
||||||
}),
|
}),
|
||||||
primary = if (auth) stringResource(R.string.home_problem_edit_profile) to a.editProfile else stringResource(R.string.retry) to a.retry,
|
primary = when
|
||||||
|
{
|
||||||
|
/* F_ErrAuth: a profile from a link gets its new version */
|
||||||
|
(auth || m.problem == HomeProblem.SERVER_CERT) && m.hasLink -> stringResource(R.string.home_problem_update_profile) to a.updateProfile
|
||||||
|
auth -> stringResource(R.string.home_problem_edit_profile) to a.editProfile
|
||||||
|
else -> stringResource(R.string.retry) to a.retry
|
||||||
|
},
|
||||||
secondary = stringResource(R.string.show_log) to a.showLog,
|
secondary = stringResource(R.string.show_log) to a.showLog,
|
||||||
)
|
)
|
||||||
if (s.profiles.size > 1)
|
if (s.profiles.size > 1)
|
||||||
{
|
{
|
||||||
ShxButton(stringResource(R.string.home_other_server), a.pickServer, style = ShxButtonStyle.TONAL, height = 52.dp, modifier = Modifier.fillMaxWidth())
|
ShxButton(stringResource(R.string.home_other_server), a.pickServer, style = ShxButtonStyle.TONAL, height = 52.dp, modifier = Modifier.fillMaxWidth())
|
||||||
}
|
}
|
||||||
|
/* the retries go on until stopped: a way out without the notification */
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth().height(44.dp).clip(RoundedCornerShape(14.dp)).clickable(role = Role.Button, onClick = a.disconnect),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Text(stringResource(R.string.home_stop_vpn), style = ShxType.body.copy(fontWeight = FontWeight.Bold), color = c.accentText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
connected ->
|
connected ->
|
||||||
{
|
{
|
||||||
@@ -357,6 +396,9 @@ internal fun HomeContent(m: HomeModel, a: HomeActions)
|
|||||||
QuickTile(ShxIcons.Globe, stringResource(R.string.home_action_switch), a.pickServer, Modifier.weight(1f))
|
QuickTile(ShxIcons.Globe, stringResource(R.string.home_action_switch), a.pickServer, Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m.hint?.takeIf { it.suggest > 0 }?.let { h ->
|
||||||
|
ScenarioSuggestCard(h.ssid, h.suggest, a.suggestYes, a.suggestNo)
|
||||||
|
}
|
||||||
/* F_HomeScenario: today's traffic stays visible while off */
|
/* F_HomeScenario: today's traffic stays visible while off */
|
||||||
m.today?.takeIf { it.total > 0 }?.let { today ->
|
m.today?.takeIf { it.total > 0 }?.let { today ->
|
||||||
val share = if (today.direct > 0) (today.vpn * 100 / today.total).toInt() else null
|
val share = if (today.direct > 0) (today.vpn * 100 / today.total).toInt() else null
|
||||||
|
|||||||
+181
@@ -0,0 +1,181 @@
|
|||||||
|
/*
|
||||||
|
* 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.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
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.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.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
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.preference.PreferenceManager
|
||||||
|
import org.strongswan.android.R
|
||||||
|
import org.strongswan.android.logic.autoconnect.AutoConnectManager
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxIcons
|
||||||
|
import org.strongswan.android.ui.compose.theme.LocalShxColors
|
||||||
|
import org.strongswan.android.ui.compose.theme.ShxType
|
||||||
|
|
||||||
|
/** What Home says about the network rules while the VPN is off (F_HomeScenario / F_ScenarioSuggest). */
|
||||||
|
internal data class ScenarioHint(
|
||||||
|
val ssid: String,
|
||||||
|
/** off because this Wi-Fi is trusted */
|
||||||
|
val trustedHere: Boolean,
|
||||||
|
/** and it comes back on its own when leaving */
|
||||||
|
val autoOnLeave: Boolean,
|
||||||
|
/** the user switched it off by hand here last time */
|
||||||
|
val manualOffHere: Boolean,
|
||||||
|
/** how often by hand here, when worth suggesting a rule (≥ 3), else 0 */
|
||||||
|
val suggest: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Remembers the manual switch-offs per Wi-Fi network for the suggestion. */
|
||||||
|
object ScenarioHints
|
||||||
|
{
|
||||||
|
private const val PREF_COUNT = "pref_manual_off_count_"
|
||||||
|
private const val PREF_DECLINED = "pref_manual_off_declined_"
|
||||||
|
private const val PREF_LAST = "pref_manual_off_last"
|
||||||
|
internal const val SUGGEST_AFTER = 3
|
||||||
|
|
||||||
|
/** The user turned the VPN off on [ssid]. */
|
||||||
|
fun manualOff(context: Context, ssid: String?)
|
||||||
|
{
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
if (ssid == null)
|
||||||
|
{
|
||||||
|
prefs.edit().remove(PREF_LAST).apply()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prefs.edit().putInt(PREF_COUNT + ssid, prefs.getInt(PREF_COUNT + ssid, 0) + 1).putString(PREF_LAST, ssid).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The user turned it on again: the «off by hand» line goes away. */
|
||||||
|
fun manualOn(context: Context)
|
||||||
|
{
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().remove(PREF_LAST).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decline(context: Context, ssid: String)
|
||||||
|
{
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(PREF_DECLINED + ssid, true).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** «Yes, create the scenario»: the network becomes trusted and both rules are on. */
|
||||||
|
fun accept(context: Context, ssid: String, profileUuid: String?)
|
||||||
|
{
|
||||||
|
AutoConnectManager.getInstance(context).updateConfig {
|
||||||
|
it.copy(
|
||||||
|
enabled = true, parkOnTrusted = true, connectOnUntrusted = true,
|
||||||
|
trustedSsids = it.trustedSsids + ssid, profileUuid = it.profileUuid ?: profileUuid,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
decline(context, ssid)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun hint(context: Context, ssid: String?): ScenarioHint?
|
||||||
|
{
|
||||||
|
ssid ?: return null
|
||||||
|
val config = AutoConnectManager.getInstance(context).currentConfig
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val trusted = config.enabled && config.parkOnTrusted && config.trustedSsids.any { it.equals(ssid, ignoreCase = true) }
|
||||||
|
val count = prefs.getInt(PREF_COUNT + ssid, 0)
|
||||||
|
val suggest = if (!trusted && count >= SUGGEST_AFTER && !prefs.getBoolean(PREF_DECLINED + ssid, false)) count else 0
|
||||||
|
return ScenarioHint(
|
||||||
|
ssid = ssid,
|
||||||
|
trustedHere = trusted,
|
||||||
|
autoOnLeave = trusted && config.connectOnUntrusted,
|
||||||
|
manualOffHere = prefs.getString(PREF_LAST, null) == ssid,
|
||||||
|
suggest = suggest,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** «Scenario · Home_5G» pill under the hero (F_HomeScenario). */
|
||||||
|
@Composable
|
||||||
|
internal fun ScenarioPill(text: String, onClick: () -> Unit)
|
||||||
|
{
|
||||||
|
val c = LocalShxColors.current
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.height(40.dp).clip(RoundedCornerShape(20.dp)).background(c.accentContainer)
|
||||||
|
.clickable(role = Role.Button, onClick = onClick).padding(start = 14.dp, end = 8.dp),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Icon(ShxIcons.Wifi, contentDescription = null, tint = c.onAccentContainer, modifier = Modifier.size(18.dp))
|
||||||
|
Text(text, style = ShxType.body.copy(fontWeight = FontWeight.Bold), color = c.onAccentContainer, maxLines = 1)
|
||||||
|
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(24.dp).clip(CircleShape).background(c.card))
|
||||||
|
{
|
||||||
|
Icon(ShxIcons.Chevron, contentDescription = null, tint = c.onAccentContainer, modifier = Modifier.size(14.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** «Turn the VPN off at home automatically?» (F_ScenarioSuggest). */
|
||||||
|
@Composable
|
||||||
|
internal fun ScenarioSuggestCard(ssid: String, count: Int, onYes: () -> Unit, onNo: () -> Unit)
|
||||||
|
{
|
||||||
|
val c = LocalShxColors.current
|
||||||
|
val shape = RoundedCornerShape(24.dp)
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth().clip(shape).background(c.accentSoft).border(1.dp, c.accentBorder, shape).padding(16.dp),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp))
|
||||||
|
{
|
||||||
|
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(40.dp).clip(RoundedCornerShape(14.dp)).background(c.accent))
|
||||||
|
{
|
||||||
|
Icon(ShxIcons.Wifi, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp))
|
||||||
|
}
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(3.dp))
|
||||||
|
{
|
||||||
|
Text(stringResource(R.string.suggest_title), style = ShxType.cardTitle, color = c.text)
|
||||||
|
Text(stringResource(R.string.suggest_text, count, ssid), style = ShxType.note.copy(fontSize = 13.sp, lineHeight = 18.sp, fontWeight = FontWeight.Normal), color = c.onAccentContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp))
|
||||||
|
{
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.weight(2f).height(44.dp).clip(RoundedCornerShape(14.dp)).background(c.accent).clickable(role = Role.Button, onClick = onYes),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Text(stringResource(R.string.suggest_yes), style = ShxType.body.copy(fontWeight = FontWeight.ExtraBold), color = Color.White, maxLines = 1)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.weight(1f).height(44.dp).clip(RoundedCornerShape(14.dp)).border(1.dp, c.outline, RoundedCornerShape(14.dp)).clickable(role = Role.Button, onClick = onNo),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Text(stringResource(R.string.suggest_no), style = ShxType.body.copy(fontWeight = FontWeight.Bold), color = c.text, maxLines = 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1280,4 +1280,16 @@
|
|||||||
<string name="scenario_often">Часто открываете</string>
|
<string name="scenario_often">Часто открываете</string>
|
||||||
<string name="scenario_all_apps">Все приложения</string>
|
<string name="scenario_all_apps">Все приложения</string>
|
||||||
<string name="scenario_done">Готово</string>
|
<string name="scenario_done">Готово</string>
|
||||||
|
<string name="hint_scenario_pill">Сценарий · %1$s</string>
|
||||||
|
<string name="hint_on_leave">Включится сам, когда уйдёте из этой сети</string>
|
||||||
|
<string name="hint_trusted_off">В доверенной сети VPN не нужен</string>
|
||||||
|
<string name="hint_server_on_leave">подключится при выходе из %1$s</string>
|
||||||
|
<string name="hint_manual_off">Выключен вручную · %1$s</string>
|
||||||
|
<string name="hint_last_server">последний сервер</string>
|
||||||
|
<string name="suggest_title">Выключать VPN здесь автоматически?</string>
|
||||||
|
<string name="suggest_text">Вы уже %1$d-й раз выключаете его в сети %2$s. Вне этой сети VPN будет включаться сам.</string>
|
||||||
|
<string name="suggest_yes">Да, создать сценарий</string>
|
||||||
|
<string name="suggest_no">Не надо</string>
|
||||||
|
<string name="home_problem_update_profile">Обновить профиль</string>
|
||||||
|
<string name="home_stop_vpn">Отключить VPN</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1266,4 +1266,16 @@
|
|||||||
<string name="scenario_often">Often opened</string>
|
<string name="scenario_often">Often opened</string>
|
||||||
<string name="scenario_all_apps">All apps</string>
|
<string name="scenario_all_apps">All apps</string>
|
||||||
<string name="scenario_done">Done</string>
|
<string name="scenario_done">Done</string>
|
||||||
|
<string name="hint_scenario_pill">Scenario · %1$s</string>
|
||||||
|
<string name="hint_on_leave">Turns on by itself when you leave this network</string>
|
||||||
|
<string name="hint_trusted_off">Not needed in a trusted network</string>
|
||||||
|
<string name="hint_server_on_leave">connects when you leave %1$s</string>
|
||||||
|
<string name="hint_manual_off">Off by hand · %1$s</string>
|
||||||
|
<string name="hint_last_server">last server</string>
|
||||||
|
<string name="suggest_title">Turn the VPN off here automatically?</string>
|
||||||
|
<string name="suggest_text">This is the %1$d. time you turn it off in %2$s. Away from here the VPN turns on by itself.</string>
|
||||||
|
<string name="suggest_yes">Yes, create the scenario</string>
|
||||||
|
<string name="suggest_no">No thanks</string>
|
||||||
|
<string name="home_problem_update_profile">Update profile</string>
|
||||||
|
<string name="home_stop_vpn">Turn VPN off</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in new issue
Block a user