android: settings as grouped pages (appearance, automation, security, updates, support), theme mode, snackbars instead of toasts
This commit is contained in:
1 parent
a451386e0a
commit
0c3ea75cac
4 files changed
+535
-300
No files matched your search
+476
-299
@@ -69,6 +69,32 @@ import androidx.preference.PreferenceManager
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.rounded.Brightness6
|
||||||
|
import androidx.compose.material3.SegmentedButton
|
||||||
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
|
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.semantics.heading
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.strongswan.android.ui.compose.components.ShxSnackbarHost
|
||||||
|
import org.strongswan.android.ui.compose.theme.ShxSpacing
|
||||||
|
import org.strongswan.android.ui.compose.theme.ThemeMode
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import org.strongswan.android.R
|
import org.strongswan.android.R
|
||||||
import org.strongswan.android.data.VpnProfile
|
import org.strongswan.android.data.VpnProfile
|
||||||
import org.strongswan.android.logic.StrongSwanApplication
|
import org.strongswan.android.logic.StrongSwanApplication
|
||||||
@@ -124,6 +150,8 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
val updateState by updateManager.state.collectAsStateWithLifecycle()
|
val updateState by updateManager.state.collectAsStateWithLifecycle()
|
||||||
var showUpdateConfirm by remember { mutableStateOf(false) }
|
var showUpdateConfirm by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
val unknownSourcesLauncher = rememberLauncherForActivityResult(
|
val unknownSourcesLauncher = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.StartActivityForResult()
|
ActivityResultContracts.StartActivityForResult()
|
||||||
) {
|
) {
|
||||||
@@ -133,7 +161,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Toast.makeText(context, R.string.update_permission_denied, Toast.LENGTH_LONG).show()
|
scope.launch { snackbarHostState.showSnackbar(context.getString(R.string.update_permission_denied)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val startUpdate: () -> Unit = {
|
val startUpdate: () -> Unit = {
|
||||||
@@ -147,331 +175,352 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pageState by rememberSaveable { mutableStateOf(SettingsPage.ROOT) }
|
||||||
|
/* from a sub page "back" returns to the settings list, not out of the tab */
|
||||||
|
BackHandler(enabled = pageState != SettingsPage.ROOT) { pageState = SettingsPage.ROOT }
|
||||||
|
val effects = MaterialTheme.motionScheme.defaultEffectsSpec<Float>()
|
||||||
|
val spatial = MaterialTheme.motionScheme.defaultSpatialSpec<androidx.compose.ui.unit.IntOffset>()
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize())
|
||||||
|
{
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = pageState,
|
||||||
|
transitionSpec = {
|
||||||
|
val forward = targetState != SettingsPage.ROOT
|
||||||
|
(fadeIn(effects) + slideInHorizontally(spatial) { if (forward) it / 8 else -it / 8 }) togetherWith
|
||||||
|
(fadeOut(effects) + slideOutHorizontally(spatial) { if (forward) -it / 8 else it / 8 })
|
||||||
|
},
|
||||||
|
label = "settingsPage",
|
||||||
|
) { page ->
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(bottom = 24.dp),
|
.padding(bottom = ShxSpacing.xl),
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (showTitle)
|
if (showTitle || page != SettingsPage.ROOT)
|
||||||
{
|
{
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.pref_title), fontWeight = FontWeight.SemiBold) },
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(page.titleRes),
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
modifier = Modifier.semantics { heading() },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
if (page != SettingsPage.ROOT)
|
||||||
|
{
|
||||||
|
IconButton(onClick = { pageState = SettingsPage.ROOT }) {
|
||||||
|
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
colors = androidx.compose.material3.TopAppBarDefaults.topAppBarColors(
|
colors = androidx.compose.material3.TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ShxSection(title = stringResource(R.string.settings_section_general))
|
if (page == SettingsPage.ROOT)
|
||||||
{
|
{
|
||||||
var addedRow = false
|
SettingsRoot(onOpen = { pageState = it })
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
}
|
||||||
|
|
||||||
|
if (page == SettingsPage.APPEARANCE)
|
||||||
|
{
|
||||||
|
ShxSection(title = stringResource(R.string.settings_page_appearance))
|
||||||
{
|
{
|
||||||
var dynamicColors by remember { ShxThemeSettings.load(context); mutableStateOf(ShxThemeSettings.useDynamicColors.value) }
|
ThemeModeRow()
|
||||||
|
var addedRow = false
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
addedRow = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page == SettingsPage.AUTOMATION)
|
||||||
|
{
|
||||||
|
ShxSection(title = stringResource(R.string.settings_section_general))
|
||||||
|
{
|
||||||
|
var addedRow = false
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && managed.isAllowSettingsAccess)
|
||||||
|
{
|
||||||
|
if (addedRow)
|
||||||
|
{
|
||||||
|
ShxSectionDivider()
|
||||||
|
}
|
||||||
|
ShxSettingRow(
|
||||||
|
icon = Icons.Rounded.History,
|
||||||
|
title = stringResource(R.string.pref_default_vpn_profile),
|
||||||
|
subtitle = defaultLabel,
|
||||||
|
onClick = { showDefaultPicker = true },
|
||||||
|
)
|
||||||
|
ShxSectionDivider()
|
||||||
|
ShxSettingToggleRow(
|
||||||
|
icon = Icons.Rounded.BatteryChargingFull,
|
||||||
|
title = stringResource(R.string.pref_power_whitelist_title),
|
||||||
|
subtitle = stringResource(R.string.pref_power_whitelist_summary),
|
||||||
|
checked = ignorePower,
|
||||||
|
onCheckedChange = {
|
||||||
|
ignorePower = it
|
||||||
|
prefs.edit().putBoolean(Constants.PREF_IGNORE_POWER_WHITELIST, it).apply()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (Build.MANUFACTURER.equals("samsung", ignoreCase = true))
|
||||||
|
{ /* One UI puts unused apps to sleep even with battery optimizations
|
||||||
|
* disabled, which breaks watchdog-based recovery of the tunnel */
|
||||||
|
ShxSectionDivider()
|
||||||
|
ShxSettingRow(
|
||||||
|
icon = Icons.Rounded.Bedtime,
|
||||||
|
title = stringResource(R.string.samsung_sleep_title),
|
||||||
|
subtitle = stringResource(R.string.samsung_sleep_summary),
|
||||||
|
onClick = { openSamsungBatterySettings(context) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page == SettingsPage.AUTOMATION)
|
||||||
|
{
|
||||||
|
ShxSection(title = stringResource(R.string.settings_section_automation))
|
||||||
|
{
|
||||||
|
ShxSettingRow(
|
||||||
|
icon = Icons.Rounded.Wifi,
|
||||||
|
title = stringResource(R.string.auto_connect_title),
|
||||||
|
subtitle = stringResource(R.string.auto_connect_summary),
|
||||||
|
onClick = {
|
||||||
|
context.startActivity(Intent(context, TrustedNetworksActivity::class.java))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ShxSectionDivider()
|
||||||
|
ShxSettingRow(
|
||||||
|
icon = Icons.Rounded.NetworkCheck,
|
||||||
|
title = stringResource(R.string.health_check_title),
|
||||||
|
subtitle = stringResource(R.string.health_check_summary),
|
||||||
|
onClick = {
|
||||||
|
context.startActivity(Intent(context, ConnectivityCheckActivity::class.java))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ShxSectionDivider()
|
||||||
|
var whiteListEnabled by remember {
|
||||||
|
mutableStateOf(WhiteListManager.getInstance(context).currentConfig.enabled)
|
||||||
|
}
|
||||||
ShxSettingToggleRow(
|
ShxSettingToggleRow(
|
||||||
icon = Icons.Rounded.Palette,
|
icon = Icons.Rounded.GppMaybe,
|
||||||
title = stringResource(R.string.pref_dynamic_colors_title),
|
title = stringResource(R.string.whitelist_analysis_title),
|
||||||
subtitle = stringResource(R.string.pref_dynamic_colors_summary),
|
subtitle = stringResource(R.string.whitelist_analysis_summary),
|
||||||
checked = dynamicColors,
|
checked = whiteListEnabled,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
dynamicColors = it
|
whiteListEnabled = it
|
||||||
ShxThemeSettings.setUseDynamicColors(context, it)
|
WhiteListManager.getInstance(context).updateConfig { config -> config.copy(enabled = it) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ShxSectionDivider()
|
||||||
|
var shortcutsEnabled by remember {
|
||||||
|
mutableStateOf(VpnShortcutManager.getInstance(context).isEnabled())
|
||||||
|
}
|
||||||
|
ShxSettingToggleRow(
|
||||||
|
icon = Icons.Rounded.Bolt,
|
||||||
|
title = stringResource(R.string.pref_routines_shortcuts_title),
|
||||||
|
subtitle = stringResource(R.string.pref_routines_shortcuts_summary),
|
||||||
|
checked = shortcutsEnabled,
|
||||||
|
onCheckedChange = {
|
||||||
|
shortcutsEnabled = it
|
||||||
|
VpnShortcutManager.getInstance(context).setEnabled(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ShxSectionDivider()
|
||||||
|
var watchdogEnabled by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean(Constants.PREF_WATCHDOG_ENABLED, true))
|
||||||
|
}
|
||||||
|
ShxSettingToggleRow(
|
||||||
|
icon = Icons.Rounded.RestartAlt,
|
||||||
|
title = stringResource(R.string.watchdog_title),
|
||||||
|
subtitle = stringResource(R.string.watchdog_summary),
|
||||||
|
checked = watchdogEnabled,
|
||||||
|
onCheckedChange = {
|
||||||
|
watchdogEnabled = it
|
||||||
|
prefs.edit().putBoolean(Constants.PREF_WATCHDOG_ENABLED, it).apply()
|
||||||
|
if (it)
|
||||||
|
{ /* start watching right away if a connection is expected */
|
||||||
|
WatchdogReceiver.schedule(context)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
addedRow = true
|
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && managed.isAllowSettingsAccess)
|
}
|
||||||
|
|
||||||
|
if (page == SettingsPage.SECURITY)
|
||||||
|
{
|
||||||
|
ShxSection(title = stringResource(R.string.settings_section_security))
|
||||||
{
|
{
|
||||||
if (addedRow)
|
KillSwitchRow()
|
||||||
|
ShxSectionDivider()
|
||||||
|
var safeIpv6 by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean(Constants.PREF_SAFE_IPV6, false))
|
||||||
|
}
|
||||||
|
ShxSettingToggleRow(
|
||||||
|
icon = Icons.Rounded.GppGood,
|
||||||
|
title = stringResource(R.string.safe_ipv6_title),
|
||||||
|
subtitle = stringResource(R.string.safe_ipv6_summary),
|
||||||
|
checked = safeIpv6,
|
||||||
|
onCheckedChange = {
|
||||||
|
safeIpv6 = it
|
||||||
|
prefs.edit().putBoolean(Constants.PREF_SAFE_IPV6, it).apply()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page == SettingsPage.SECURITY)
|
||||||
|
{
|
||||||
|
ShxSection(title = stringResource(R.string.settings_section_certs))
|
||||||
|
{
|
||||||
|
ShxSettingRow(
|
||||||
|
icon = Icons.Rounded.VerifiedUser,
|
||||||
|
title = stringResource(R.string.trusted_certs_title),
|
||||||
|
onClick = {
|
||||||
|
context.startActivity(Intent(context, TrustedCertificatesActivity::class.java))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ShxSectionDivider()
|
||||||
|
ShxSettingRow(
|
||||||
|
icon = Icons.Rounded.Cached,
|
||||||
|
title = stringResource(R.string.crl_cache),
|
||||||
|
onClick = { showCrl = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page == SettingsPage.SUPPORT)
|
||||||
|
{
|
||||||
|
if (managed.isAllowProfileImport)
|
||||||
|
{
|
||||||
|
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 {
|
||||||
|
mutableStateOf(prefs.getBoolean(Constants.PREF_EXTENDED_DIAG, false))
|
||||||
|
}
|
||||||
|
ShxSettingToggleRow(
|
||||||
|
icon = Icons.Rounded.BugReport,
|
||||||
|
title = stringResource(R.string.extended_diag_title),
|
||||||
|
subtitle = stringResource(R.string.extended_diag_summary),
|
||||||
|
checked = extendedDiag,
|
||||||
|
onCheckedChange = {
|
||||||
|
extendedDiag = it
|
||||||
|
prefs.edit().putBoolean(Constants.PREF_EXTENDED_DIAG, it).apply()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page == SettingsPage.UPDATES)
|
||||||
|
{
|
||||||
|
ShxSection(title = stringResource(R.string.settings_section_updates))
|
||||||
|
{
|
||||||
|
var autoUpdate by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean(Constants.PREF_AUTO_UPDATE_CHECK, true))
|
||||||
|
}
|
||||||
|
ShxSettingToggleRow(
|
||||||
|
icon = Icons.Rounded.Autorenew,
|
||||||
|
title = stringResource(R.string.update_auto_check_title),
|
||||||
|
subtitle = stringResource(R.string.update_auto_check_summary),
|
||||||
|
checked = autoUpdate,
|
||||||
|
onCheckedChange = {
|
||||||
|
autoUpdate = it
|
||||||
|
prefs.edit().putBoolean(Constants.PREF_AUTO_UPDATE_CHECK, it).apply()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ShxSectionDivider()
|
||||||
|
var silentUpdates by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean(Constants.PREF_SILENT_UPDATES, false))
|
||||||
|
}
|
||||||
|
ShxSettingToggleRow(
|
||||||
|
icon = Icons.Rounded.CloudSync,
|
||||||
|
title = stringResource(R.string.update_silent_title),
|
||||||
|
subtitle = stringResource(R.string.update_silent_summary),
|
||||||
|
checked = silentUpdates,
|
||||||
|
onCheckedChange = {
|
||||||
|
silentUpdates = it
|
||||||
|
prefs.edit().putBoolean(Constants.PREF_SILENT_UPDATES, it).apply()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ShxSectionDivider()
|
||||||
|
ShxSettingRow(
|
||||||
|
icon = Icons.Rounded.SystemUpdateAlt,
|
||||||
|
title = stringResource(R.string.update_check_title),
|
||||||
|
subtitle = updateStatusText(updateState),
|
||||||
|
onClick = {
|
||||||
|
when (updateState)
|
||||||
|
{
|
||||||
|
is UpdateState.Checking, is UpdateState.Downloading -> Unit
|
||||||
|
else -> updateManager.checkNow()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
val updateInfo = (updateState as? UpdateState.Available)?.info
|
||||||
|
?: (updateState as? UpdateState.Downloading)?.info
|
||||||
|
if (updateInfo != null)
|
||||||
{
|
{
|
||||||
ShxSectionDivider()
|
ShxSectionDivider()
|
||||||
}
|
ShxSettingRow(
|
||||||
ShxSettingRow(
|
icon = Icons.Rounded.CloudDownload,
|
||||||
icon = Icons.Rounded.History,
|
title = stringResource(R.string.update_download_install),
|
||||||
title = stringResource(R.string.pref_default_vpn_profile),
|
subtitle = when (val state = updateState)
|
||||||
subtitle = defaultLabel,
|
|
||||||
onClick = { showDefaultPicker = true },
|
|
||||||
)
|
|
||||||
ShxSectionDivider()
|
|
||||||
ShxSettingToggleRow(
|
|
||||||
icon = Icons.Rounded.BatteryChargingFull,
|
|
||||||
title = stringResource(R.string.pref_power_whitelist_title),
|
|
||||||
subtitle = stringResource(R.string.pref_power_whitelist_summary),
|
|
||||||
checked = ignorePower,
|
|
||||||
onCheckedChange = {
|
|
||||||
ignorePower = it
|
|
||||||
prefs.edit().putBoolean(Constants.PREF_IGNORE_POWER_WHITELIST, it).apply()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (Build.MANUFACTURER.equals("samsung", ignoreCase = true))
|
|
||||||
{ /* One UI puts unused apps to sleep even with battery optimizations
|
|
||||||
* disabled, which breaks watchdog-based recovery of the tunnel */
|
|
||||||
ShxSectionDivider()
|
|
||||||
ShxSettingRow(
|
|
||||||
icon = Icons.Rounded.Bedtime,
|
|
||||||
title = stringResource(R.string.samsung_sleep_title),
|
|
||||||
subtitle = stringResource(R.string.samsung_sleep_summary),
|
|
||||||
onClick = { openSamsungBatterySettings(context) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ShxSection(title = stringResource(R.string.settings_section_automation))
|
|
||||||
{
|
|
||||||
ShxSettingRow(
|
|
||||||
icon = Icons.Rounded.Wifi,
|
|
||||||
title = stringResource(R.string.auto_connect_title),
|
|
||||||
subtitle = stringResource(R.string.auto_connect_summary),
|
|
||||||
onClick = {
|
|
||||||
context.startActivity(Intent(context, TrustedNetworksActivity::class.java))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ShxSectionDivider()
|
|
||||||
ShxSettingRow(
|
|
||||||
icon = Icons.Rounded.NetworkCheck,
|
|
||||||
title = stringResource(R.string.health_check_title),
|
|
||||||
subtitle = stringResource(R.string.health_check_summary),
|
|
||||||
onClick = {
|
|
||||||
context.startActivity(Intent(context, ConnectivityCheckActivity::class.java))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ShxSectionDivider()
|
|
||||||
var whiteListEnabled by remember {
|
|
||||||
mutableStateOf(WhiteListManager.getInstance(context).currentConfig.enabled)
|
|
||||||
}
|
|
||||||
ShxSettingToggleRow(
|
|
||||||
icon = Icons.Rounded.GppMaybe,
|
|
||||||
title = stringResource(R.string.whitelist_analysis_title),
|
|
||||||
subtitle = stringResource(R.string.whitelist_analysis_summary),
|
|
||||||
checked = whiteListEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
whiteListEnabled = it
|
|
||||||
WhiteListManager.getInstance(context).updateConfig { config -> config.copy(enabled = it) }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ShxSectionDivider()
|
|
||||||
var shortcutsEnabled by remember {
|
|
||||||
mutableStateOf(VpnShortcutManager.getInstance(context).isEnabled())
|
|
||||||
}
|
|
||||||
ShxSettingToggleRow(
|
|
||||||
icon = Icons.Rounded.Bolt,
|
|
||||||
title = stringResource(R.string.pref_routines_shortcuts_title),
|
|
||||||
subtitle = stringResource(R.string.pref_routines_shortcuts_summary),
|
|
||||||
checked = shortcutsEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
shortcutsEnabled = it
|
|
||||||
VpnShortcutManager.getInstance(context).setEnabled(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ShxSectionDivider()
|
|
||||||
var watchdogEnabled by remember {
|
|
||||||
mutableStateOf(prefs.getBoolean(Constants.PREF_WATCHDOG_ENABLED, true))
|
|
||||||
}
|
|
||||||
ShxSettingToggleRow(
|
|
||||||
icon = Icons.Rounded.RestartAlt,
|
|
||||||
title = stringResource(R.string.watchdog_title),
|
|
||||||
subtitle = stringResource(R.string.watchdog_summary),
|
|
||||||
checked = watchdogEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
watchdogEnabled = it
|
|
||||||
prefs.edit().putBoolean(Constants.PREF_WATCHDOG_ENABLED, it).apply()
|
|
||||||
if (it)
|
|
||||||
{ /* start watching right away if a connection is expected */
|
|
||||||
WatchdogReceiver.schedule(context)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ShxSection(title = stringResource(R.string.settings_section_security))
|
|
||||||
{
|
|
||||||
/* re-read on every ON_RESUME so the summary reflects changes made in system settings */
|
|
||||||
var lockdownActive by remember { mutableStateOf(context.isVpnLockdownEnabled()) }
|
|
||||||
var showKillSwitchDialog by remember { mutableStateOf(false) }
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
|
||||||
DisposableEffect(lifecycleOwner) {
|
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
|
||||||
if (event == Lifecycle.Event.ON_RESUME)
|
|
||||||
{
|
|
||||||
lockdownActive = context.isVpnLockdownEnabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lifecycleOwner.lifecycle.addObserver(observer)
|
|
||||||
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
|
||||||
}
|
|
||||||
if (showKillSwitchDialog)
|
|
||||||
{
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showKillSwitchDialog = false },
|
|
||||||
title = { Text(stringResource(R.string.kill_switch_dialog_title)) },
|
|
||||||
text = { Text(stringResource(R.string.kill_switch_dialog_text)) },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
showKillSwitchDialog = false
|
|
||||||
context.startActivity(Intent(Settings.ACTION_VPN_SETTINGS))
|
|
||||||
}) { Text(stringResource(R.string.kill_switch_dialog_enable)) }
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showKillSwitchDialog = false }) {
|
|
||||||
Text(stringResource(android.R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ShxSettingRow(
|
|
||||||
icon = Icons.Rounded.Security,
|
|
||||||
title = stringResource(R.string.kill_switch_title),
|
|
||||||
subtitle = stringResource(
|
|
||||||
if (lockdownActive) R.string.kill_switch_summary_on
|
|
||||||
else R.string.kill_switch_summary_off
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
/* explain what this actually toggles before sending the user to system settings */
|
|
||||||
if (lockdownActive) context.startActivity(Intent(Settings.ACTION_VPN_SETTINGS))
|
|
||||||
else showKillSwitchDialog = true
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ShxSectionDivider()
|
|
||||||
var safeIpv6 by remember {
|
|
||||||
mutableStateOf(prefs.getBoolean(Constants.PREF_SAFE_IPV6, false))
|
|
||||||
}
|
|
||||||
ShxSettingToggleRow(
|
|
||||||
icon = Icons.Rounded.GppGood,
|
|
||||||
title = stringResource(R.string.safe_ipv6_title),
|
|
||||||
subtitle = stringResource(R.string.safe_ipv6_summary),
|
|
||||||
checked = safeIpv6,
|
|
||||||
onCheckedChange = {
|
|
||||||
safeIpv6 = it
|
|
||||||
prefs.edit().putBoolean(Constants.PREF_SAFE_IPV6, it).apply()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ShxSection(title = stringResource(R.string.settings_section_certs))
|
|
||||||
{
|
|
||||||
ShxSettingRow(
|
|
||||||
icon = Icons.Rounded.VerifiedUser,
|
|
||||||
title = stringResource(R.string.trusted_certs_title),
|
|
||||||
onClick = {
|
|
||||||
context.startActivity(Intent(context, TrustedCertificatesActivity::class.java))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ShxSectionDivider()
|
|
||||||
ShxSettingRow(
|
|
||||||
icon = Icons.Rounded.Cached,
|
|
||||||
title = stringResource(R.string.crl_cache),
|
|
||||||
onClick = { showCrl = true },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (managed.isAllowProfileImport)
|
|
||||||
{
|
|
||||||
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))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
mutableStateOf(prefs.getBoolean(Constants.PREF_EXTENDED_DIAG, false))
|
|
||||||
}
|
|
||||||
ShxSettingToggleRow(
|
|
||||||
icon = Icons.Rounded.BugReport,
|
|
||||||
title = stringResource(R.string.extended_diag_title),
|
|
||||||
subtitle = stringResource(R.string.extended_diag_summary),
|
|
||||||
checked = extendedDiag,
|
|
||||||
onCheckedChange = {
|
|
||||||
extendedDiag = it
|
|
||||||
prefs.edit().putBoolean(Constants.PREF_EXTENDED_DIAG, it).apply()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ShxSection(title = stringResource(R.string.settings_section_updates))
|
|
||||||
{
|
|
||||||
var autoUpdate by remember {
|
|
||||||
mutableStateOf(prefs.getBoolean(Constants.PREF_AUTO_UPDATE_CHECK, true))
|
|
||||||
}
|
|
||||||
ShxSettingToggleRow(
|
|
||||||
icon = Icons.Rounded.Autorenew,
|
|
||||||
title = stringResource(R.string.update_auto_check_title),
|
|
||||||
subtitle = stringResource(R.string.update_auto_check_summary),
|
|
||||||
checked = autoUpdate,
|
|
||||||
onCheckedChange = {
|
|
||||||
autoUpdate = it
|
|
||||||
prefs.edit().putBoolean(Constants.PREF_AUTO_UPDATE_CHECK, it).apply()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ShxSectionDivider()
|
|
||||||
var silentUpdates by remember {
|
|
||||||
mutableStateOf(prefs.getBoolean(Constants.PREF_SILENT_UPDATES, false))
|
|
||||||
}
|
|
||||||
ShxSettingToggleRow(
|
|
||||||
icon = Icons.Rounded.CloudSync,
|
|
||||||
title = stringResource(R.string.update_silent_title),
|
|
||||||
subtitle = stringResource(R.string.update_silent_summary),
|
|
||||||
checked = silentUpdates,
|
|
||||||
onCheckedChange = {
|
|
||||||
silentUpdates = it
|
|
||||||
prefs.edit().putBoolean(Constants.PREF_SILENT_UPDATES, it).apply()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ShxSectionDivider()
|
|
||||||
ShxSettingRow(
|
|
||||||
icon = Icons.Rounded.SystemUpdateAlt,
|
|
||||||
title = stringResource(R.string.update_check_title),
|
|
||||||
subtitle = updateStatusText(updateState),
|
|
||||||
onClick = {
|
|
||||||
when (updateState)
|
|
||||||
{
|
|
||||||
is UpdateState.Checking, is UpdateState.Downloading -> Unit
|
|
||||||
else -> updateManager.checkNow()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
val updateInfo = (updateState as? UpdateState.Available)?.info
|
|
||||||
?: (updateState as? UpdateState.Downloading)?.info
|
|
||||||
if (updateInfo != null)
|
|
||||||
{
|
|
||||||
ShxSectionDivider()
|
|
||||||
ShxSettingRow(
|
|
||||||
icon = Icons.Rounded.CloudDownload,
|
|
||||||
title = stringResource(R.string.update_download_install),
|
|
||||||
subtitle = when (val state = updateState)
|
|
||||||
{
|
|
||||||
is UpdateState.Downloading -> stringResource(R.string.update_downloading_fmt, state.progress)
|
|
||||||
else -> stringResource(R.string.update_available_fmt, updateInfo.versionName)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
if (updateState !is UpdateState.Downloading)
|
|
||||||
{
|
{
|
||||||
showUpdateConfirm = true
|
is UpdateState.Downloading -> stringResource(R.string.update_downloading_fmt, state.progress)
|
||||||
}
|
else -> stringResource(R.string.update_available_fmt, updateInfo.versionName)
|
||||||
},
|
},
|
||||||
)
|
onClick = {
|
||||||
|
if (updateState !is UpdateState.Downloading)
|
||||||
|
{
|
||||||
|
showUpdateConfirm = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
if (page == SettingsPage.ROOT || page == SettingsPage.UPDATES) Text(
|
||||||
text = stringResource(R.string.app_version_fmt, versionName),
|
text = stringResource(R.string.app_version_fmt, versionName),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -481,6 +530,12 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
ShxSnackbarHost(
|
||||||
|
hostState = snackbarHostState,
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (showDefaultPicker)
|
if (showDefaultPicker)
|
||||||
{
|
{
|
||||||
@@ -534,7 +589,7 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
if (files.isEmpty())
|
if (files.isEmpty())
|
||||||
{
|
{
|
||||||
Toast.makeText(context, R.string.clear_crl_cache_msg_none, Toast.LENGTH_SHORT).show()
|
scope.launch { snackbarHostState.showSnackbar(context.getString(R.string.clear_crl_cache_msg_none)) }
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -654,3 +709,125 @@ private fun openSamsungBatterySettings(context: Context)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class SettingsPage(val titleRes: Int)
|
||||||
|
{
|
||||||
|
ROOT(R.string.pref_title),
|
||||||
|
APPEARANCE(R.string.settings_page_appearance),
|
||||||
|
AUTOMATION(R.string.settings_section_automation),
|
||||||
|
SECURITY(R.string.settings_section_security),
|
||||||
|
UPDATES(R.string.settings_section_updates),
|
||||||
|
SUPPORT(R.string.settings_section_support),
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Settings landing list: two quick rows on top, then the five groups. */
|
||||||
|
@Composable
|
||||||
|
private fun SettingsRoot(onOpen: (SettingsPage) -> Unit)
|
||||||
|
{
|
||||||
|
val context = LocalContext.current
|
||||||
|
ShxSection(title = stringResource(R.string.settings_quick))
|
||||||
|
{
|
||||||
|
KillSwitchRow()
|
||||||
|
ShxSectionDivider()
|
||||||
|
ShxSettingRow(
|
||||||
|
icon = Icons.Rounded.Wifi,
|
||||||
|
title = stringResource(R.string.auto_connect_title),
|
||||||
|
subtitle = stringResource(R.string.auto_connect_summary),
|
||||||
|
onClick = { context.startActivity(Intent(context, TrustedNetworksActivity::class.java)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ShxSection(title = stringResource(R.string.pref_title))
|
||||||
|
{
|
||||||
|
ShxSettingRow(icon = Icons.Rounded.Palette, title = stringResource(R.string.settings_page_appearance), onClick = { onOpen(SettingsPage.APPEARANCE) })
|
||||||
|
ShxSectionDivider()
|
||||||
|
ShxSettingRow(icon = Icons.Rounded.Bolt, title = stringResource(R.string.settings_section_automation), onClick = { onOpen(SettingsPage.AUTOMATION) })
|
||||||
|
ShxSectionDivider()
|
||||||
|
ShxSettingRow(icon = Icons.Rounded.Security, title = stringResource(R.string.settings_section_security), onClick = { onOpen(SettingsPage.SECURITY) })
|
||||||
|
ShxSectionDivider()
|
||||||
|
ShxSettingRow(icon = Icons.Rounded.SystemUpdateAlt, title = stringResource(R.string.settings_section_updates), onClick = { onOpen(SettingsPage.UPDATES) })
|
||||||
|
ShxSectionDivider()
|
||||||
|
ShxSettingRow(icon = Icons.Rounded.Description, title = stringResource(R.string.settings_section_support), onClick = { onOpen(SettingsPage.SUPPORT) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** System / light / dark selector, applied immediately through [ShxThemeSettings]. */
|
||||||
|
@Composable
|
||||||
|
private fun ThemeModeRow()
|
||||||
|
{
|
||||||
|
val context = LocalContext.current
|
||||||
|
val mode by ShxThemeSettings.themeMode.collectAsStateWithLifecycle()
|
||||||
|
remember { ShxThemeSettings.load(context) }
|
||||||
|
Column(Modifier.padding(horizontal = ShxSpacing.lg, vertical = ShxSpacing.md))
|
||||||
|
{
|
||||||
|
Text(stringResource(R.string.pref_theme_title), style = MaterialTheme.typography.bodyLarge)
|
||||||
|
Spacer(Modifier.height(ShxSpacing.sm))
|
||||||
|
val options = listOf(
|
||||||
|
ThemeMode.SYSTEM to R.string.theme_system,
|
||||||
|
ThemeMode.LIGHT to R.string.theme_light,
|
||||||
|
ThemeMode.DARK to R.string.theme_dark,
|
||||||
|
)
|
||||||
|
SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth())
|
||||||
|
{
|
||||||
|
options.forEachIndexed { index, (value, label) ->
|
||||||
|
SegmentedButton(
|
||||||
|
selected = mode == value,
|
||||||
|
onClick = { ShxThemeSettings.setThemeMode(context, value) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(index, options.size),
|
||||||
|
) { Text(stringResource(label)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kill switch status row; explains and opens the system "Always-on VPN" settings. */
|
||||||
|
@Composable
|
||||||
|
private fun KillSwitchRow()
|
||||||
|
{
|
||||||
|
val context = LocalContext.current
|
||||||
|
/* re-read on every ON_RESUME so the summary reflects changes made in system settings */
|
||||||
|
var lockdownActive by remember { mutableStateOf(context.isVpnLockdownEnabled()) }
|
||||||
|
var showKillSwitchDialog by remember { mutableStateOf(false) }
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
DisposableEffect(lifecycleOwner) {
|
||||||
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
|
if (event == Lifecycle.Event.ON_RESUME)
|
||||||
|
{
|
||||||
|
lockdownActive = context.isVpnLockdownEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleOwner.lifecycle.addObserver(observer)
|
||||||
|
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||||
|
}
|
||||||
|
if (showKillSwitchDialog)
|
||||||
|
{
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showKillSwitchDialog = false },
|
||||||
|
title = { Text(stringResource(R.string.kill_switch_dialog_title)) },
|
||||||
|
text = { Text(stringResource(R.string.kill_switch_dialog_text)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showKillSwitchDialog = false
|
||||||
|
context.startActivity(Intent(Settings.ACTION_VPN_SETTINGS))
|
||||||
|
}) { Text(stringResource(R.string.kill_switch_dialog_enable)) }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showKillSwitchDialog = false }) {
|
||||||
|
Text(stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ShxSettingRow(
|
||||||
|
icon = Icons.Rounded.Security,
|
||||||
|
title = stringResource(R.string.kill_switch_title),
|
||||||
|
subtitle = stringResource(
|
||||||
|
if (lockdownActive) R.string.kill_switch_summary_on
|
||||||
|
else R.string.kill_switch_summary_off
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
/* explain what this actually toggles before sending the user to system settings */
|
||||||
|
if (lockdownActive) context.startActivity(Intent(Settings.ACTION_VPN_SETTINGS))
|
||||||
|
else showKillSwitchDialog = true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
+47
-1
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
package org.strongswan.android.ui.compose.theme
|
package org.strongswan.android.ui.compose.theme
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -27,6 +28,9 @@ import androidx.compose.material3.lightColorScheme
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
@@ -205,9 +209,21 @@ val LocalShxStatusColors = staticCompositionLocalOf { LightStatusColors }
|
|||||||
* Process-wide holder of the "use system colors" preference, so flipping it in
|
* Process-wide holder of the "use system colors" preference, so flipping it in
|
||||||
* Settings re-themes every open Compose surface without recreating activities.
|
* Settings re-themes every open Compose surface without recreating activities.
|
||||||
*/
|
*/
|
||||||
|
enum class ThemeMode
|
||||||
|
{
|
||||||
|
SYSTEM,
|
||||||
|
LIGHT,
|
||||||
|
DARK,
|
||||||
|
}
|
||||||
|
|
||||||
object ShxThemeSettings
|
object ShxThemeSettings
|
||||||
{
|
{
|
||||||
private const val KEY_DYNAMIC_COLORS = "pref_use_dynamic_colors"
|
private const val KEY_DYNAMIC_COLORS = "pref_use_dynamic_colors"
|
||||||
|
private const val KEY_THEME_MODE = "pref_theme_mode"
|
||||||
|
|
||||||
|
private val _themeMode = MutableStateFlow(ThemeMode.SYSTEM)
|
||||||
|
|
||||||
|
val themeMode: StateFlow<ThemeMode> = _themeMode.asStateFlow()
|
||||||
|
|
||||||
private val _useDynamicColors = MutableStateFlow(false)
|
private val _useDynamicColors = MutableStateFlow(false)
|
||||||
|
|
||||||
@@ -227,10 +243,18 @@ object ShxThemeSettings
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
_useDynamicColors.value = prefs(context).getBoolean(KEY_DYNAMIC_COLORS, false)
|
_useDynamicColors.value = prefs(context).getBoolean(KEY_DYNAMIC_COLORS, false)
|
||||||
|
_themeMode.value = ThemeMode.entries.getOrElse(prefs(context).getInt(KEY_THEME_MODE, 0)) { ThemeMode.SYSTEM }
|
||||||
loaded = true
|
loaded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setThemeMode(context: Context, mode: ThemeMode)
|
||||||
|
{
|
||||||
|
load(context)
|
||||||
|
prefs(context).edit().putInt(KEY_THEME_MODE, mode.ordinal).apply()
|
||||||
|
_themeMode.value = mode
|
||||||
|
}
|
||||||
|
|
||||||
fun setUseDynamicColors(context: Context, enabled: Boolean)
|
fun setUseDynamicColors(context: Context, enabled: Boolean)
|
||||||
{
|
{
|
||||||
load(context)
|
load(context)
|
||||||
@@ -268,9 +292,31 @@ private val ShxTypography = buildTypography()
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ShxTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit)
|
fun ShxTheme(content: @Composable () -> Unit)
|
||||||
{
|
{
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val themeMode by remember { ShxThemeSettings.load(context); ShxThemeSettings.themeMode }
|
||||||
|
.collectAsStateWithLifecycle()
|
||||||
|
val darkTheme = when (themeMode)
|
||||||
|
{
|
||||||
|
ThemeMode.LIGHT -> false
|
||||||
|
ThemeMode.DARK -> true
|
||||||
|
ThemeMode.SYSTEM -> isSystemInDarkTheme()
|
||||||
|
}
|
||||||
|
/* keep system bar icons readable when the chosen theme differs from the system one */
|
||||||
|
val view = LocalView.current
|
||||||
|
if (!view.isInEditMode)
|
||||||
|
{
|
||||||
|
SideEffect {
|
||||||
|
val window = (view.context as? Activity)?.window
|
||||||
|
if (window != null)
|
||||||
|
{
|
||||||
|
val controller = WindowCompat.getInsetsController(window, view)
|
||||||
|
controller.isAppearanceLightStatusBars = !darkTheme
|
||||||
|
controller.isAppearanceLightNavigationBars = !darkTheme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
val useDynamicColors by remember { ShxThemeSettings.load(context); ShxThemeSettings.useDynamicColors }
|
val useDynamicColors by remember { ShxThemeSettings.load(context); ShxThemeSettings.useDynamicColors }
|
||||||
.collectAsStateWithLifecycle()
|
.collectAsStateWithLifecycle()
|
||||||
val colors = when
|
val colors = when
|
||||||
|
|||||||
@@ -493,4 +493,10 @@
|
|||||||
<string name="availability_collapse">Скрыть детали</string>
|
<string name="availability_collapse">Скрыть детали</string>
|
||||||
<string name="routing_advanced">Дополнительно</string>
|
<string name="routing_advanced">Дополнительно</string>
|
||||||
<string name="routing_killswitch_note">Исключённые приложения (%1$d) всегда идут мимо VPN, в том числе при обрыве туннеля: kill switch их не блокирует.</string>
|
<string name="routing_killswitch_note">Исключённые приложения (%1$d) всегда идут мимо VPN, в том числе при обрыве туннеля: kill switch их не блокирует.</string>
|
||||||
|
<string name="settings_page_appearance">Оформление</string>
|
||||||
|
<string name="settings_quick">Быстрые настройки</string>
|
||||||
|
<string name="pref_theme_title">Тема</string>
|
||||||
|
<string name="theme_system">Системная</string>
|
||||||
|
<string name="theme_light">Светлая</string>
|
||||||
|
<string name="theme_dark">Тёмная</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -498,4 +498,10 @@
|
|||||||
<string name="availability_collapse">Hide details</string>
|
<string name="availability_collapse">Hide details</string>
|
||||||
<string name="routing_advanced">Advanced</string>
|
<string name="routing_advanced">Advanced</string>
|
||||||
<string name="routing_killswitch_note">%1$d excluded app(s) always use the normal network, even when the tunnel drops: the kill switch does not block them.</string>
|
<string name="routing_killswitch_note">%1$d excluded app(s) always use the normal network, even when the tunnel drops: the kill switch does not block them.</string>
|
||||||
|
<string name="settings_page_appearance">Appearance</string>
|
||||||
|
<string name="settings_quick">Quick settings</string>
|
||||||
|
<string name="pref_theme_title">Theme</string>
|
||||||
|
<string name="theme_system">System</string>
|
||||||
|
<string name="theme_light">Light</string>
|
||||||
|
<string name="theme_dark">Dark</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in new issue
Block a user