add more ui changes, androidtv improvements

This commit is contained in:
Zane Schepke 2024-10-29 05:16:39 -04:00
parent 553279ea76
commit 0784c96011
52 changed files with 1293 additions and 1457 deletions

View File

@ -151,6 +151,7 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat)
implementation(libs.material)
// test
testImplementation(libs.junit)

View File

@ -40,6 +40,12 @@
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<uses-feature
android:name="android.hardware.gamepad"
android:required="false"/>
<uses-feature android:name="android.hardware.wifi"
android:required="false"/>
<queries>
<intent>

View File

@ -26,6 +26,7 @@ class DataStoreManager(
val currentSSID = stringPreferencesKey("CURRENT_SSID")
val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED")
val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED")
val wildcardsEnabled = booleanPreferencesKey("WILDCARDS_ENABLED")
val theme = stringPreferencesKey("THEME")
}

View File

@ -7,6 +7,7 @@ data class GeneralState(
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
val isWildcardsEnabled: Boolean = IS_WILDCARDS_ENABLED,
val theme: Theme = Theme.AUTOMATIC
) {
companion object {
@ -14,5 +15,6 @@ data class GeneralState(
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false
const val IS_TUNNEL_STATS_EXPANDED = false
const val IS_WILDCARDS_ENABLED = false
}
}

View File

@ -13,6 +13,10 @@ interface AppStateRepository {
suspend fun setPinLockEnabled(enabled: Boolean)
suspend fun isWildcardsEnabled(): Boolean
suspend fun setWildcardsEnabled(enabled: Boolean)
suspend fun isBatteryOptimizationDisableShown(): Boolean
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)

View File

@ -29,6 +29,14 @@ class DataStoreAppStateRepository(
dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled)
}
override suspend fun isWildcardsEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.wildcardsEnabled) ?: GeneralState.IS_WILDCARDS_ENABLED
}
override suspend fun setWildcardsEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.wildcardsEnabled, enabled)
}
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown)
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
@ -84,6 +92,7 @@ class DataStoreAppStateRepository(
pref[DataStoreManager.pinLockEnabled]
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
isWildcardsEnabled = pref[DataStoreManager.wildcardsEnabled] ?: GeneralState.IS_WILDCARDS_ENABLED,
theme = getTheme()
)
} catch (e: IllegalArgumentException) {

View File

@ -37,34 +37,17 @@ class AppViewModel
constructor(
private val appDataRepository: AppDataRepository,
private val tunnelService: Provider<TunnelService>,
private val rootShell: Provider<RootShell>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
private val _appUiState = MutableStateFlow(AppUiState())
val appUiState = _appUiState.onStart {
_appUiState.update {
it.copy(
isRooted = isRooted(),
isKernelAvailable = isKernelSupported(),
)
}
}.stateIn(
viewModelScope + ioDispatcher,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
AppUiState(),
)
val uiState =
combine(
appDataRepository.settings.getSettingsFlow(),
appDataRepository.tunnels.getTunnelConfigsFlow(),
tunnelService.get().vpnState,
appDataRepository.appState.generalStateFlow,
appUiState,
) { settings, tunnels, tunnelState, generalState, appUiState ->
appUiState.copy(
) { settings, tunnels, tunnelState, generalState ->
AppUiState(
settings,
tunnels,
tunnelState,
@ -73,14 +56,13 @@ constructor(
}.stateIn(
viewModelScope + ioDispatcher,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
_appUiState.value,
AppUiState(),
)
private val _isAppReady = MutableStateFlow<Boolean>(false)
val isAppReady = _isAppReady.asStateFlow()
init {
viewModelScope.launch {
initPin()
initAutoTunnel()
@ -116,14 +98,6 @@ constructor(
if (settings.isAutoTunnelEnabled) ServiceManager.startWatcherService(WireGuardAutoTunnel.instance)
}
fun setTunnels(tunnels: TunnelConfigs) = viewModelScope.launch(ioDispatcher) {
_appUiState.emit(
_appUiState.value.copy(
tunnels = tunnels,
),
)
}
fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) {
PinManager.clearPin()
appDataRepository.appState.setPinLockEnabled(false)
@ -133,20 +107,7 @@ constructor(
appDataRepository.appState.setPinLockEnabled(true)
}
private suspend fun isKernelSupported(): Boolean {
return withContext(ioDispatcher) {
WgQuickBackend.hasKernelSupport()
}
}
private suspend fun isRooted(): Boolean {
return try {
withContext(ioDispatcher) {
rootShell.get().start()
}
true
} catch (_: Exception) {
false
}
fun setLocationDisclosureShown() = viewModelScope.launch {
appDataRepository.appState.setLocationDisclosureShown(true)
}
}

View File

@ -8,14 +8,10 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark
@ -31,14 +27,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.zaneschepke.wireguardautotunnel.R
@ -50,9 +44,8 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusRequester
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
@ -65,6 +58,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
@ -104,7 +98,7 @@ class MainActivity : AppCompatActivity() {
setContent {
val appUiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycle = this.lifecycle)
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val rootItemFocusRequester = remember { FocusRequester() }
LaunchedEffect(appUiState.vpnState.status) {
val context = this@MainActivity
@ -121,122 +115,109 @@ class MainActivity : AppCompatActivity() {
}
}
CompositionLocalProvider(LocalNavController provides navController) {
SnackbarControllerProvider { host ->
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme){
val focusRequester = remember { FocusRequester() }
Scaffold(
contentWindowInsets = WindowInsets(0.dp),
snackbarHost = {
SnackbarHost(host) { snackbarData: SnackbarData ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp,
),
)
}
},
modifier =
Modifier
.focusable()
.focusProperties {
if (navBackStackEntry?.isCurrentRoute(Route.Lock) == true) {
Unit
} else {
up = focusRequester
CompositionLocalProvider(LocalFocusRequester provides rootItemFocusRequester) {
CompositionLocalProvider(LocalNavController provides navController) {
SnackbarControllerProvider { host ->
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
Scaffold(
contentWindowInsets = WindowInsets(0.dp),
snackbarHost = {
SnackbarHost(host) { snackbarData: SnackbarData ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp,
),
)
}
},
bottomBar = {
BottomNavBar(
navController,
listOf(
BottomNavItem(
name = stringResource(R.string.tunnels),
route = Route.Main,
icon = Icons.Rounded.Home,
bottomBar = {
BottomNavBar(
navController,
listOf(
BottomNavItem(
name = stringResource(R.string.tunnels),
route = Route.Main,
icon = Icons.Rounded.Home,
),
BottomNavItem(
name = stringResource(R.string.settings),
route = Route.Settings,
icon = Icons.Rounded.Settings,
),
BottomNavItem(
name = stringResource(R.string.support),
route = Route.Support,
icon = Icons.Rounded.QuestionMark,
),
),
BottomNavItem(
name = stringResource(R.string.settings),
route = Route.Settings,
icon = Icons.Rounded.Settings,
),
BottomNavItem(
name = stringResource(R.string.support),
route = Route.Support,
icon = Icons.Rounded.QuestionMark,
),
),
)
},
) {
Box(modifier = Modifier.fillMaxSize().padding(it)) {
NavHost(
navController,
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
startDestination = (if (appUiState.generalState.isPinLockEnabled == true) Route.Lock else Route.Main),
) {
composable<Route.Main> {
MainScreen(
focusRequester = focusRequester,
uiState = appUiState,
)
}
composable<Route.Settings> {
SettingsScreen(
appViewModel = viewModel,
uiState = appUiState,
focusRequester = focusRequester,
)
}
composable<Route.AutoTunnel> {
AutoTunnelScreen(
appUiState,
)
}
composable<Route.Appearance> {
AppearanceScreen()
}
composable<Route.Language> {
LanguageScreen(localeStorage)
}
composable<Route.Display> {
DisplayScreen(appUiState)
}
composable<Route.Support> {
SupportScreen(
focusRequester = focusRequester,
appUiState = appUiState,
)
}
composable<Route.Logs> {
LogsScreen()
}
composable<Route.Config> {
val args = it.toRoute<Route.Config>()
ConfigScreen(
focusRequester = focusRequester,
tunnelId = args.id,
)
}
composable<Route.Option> {
val args = it.toRoute<Route.Option>()
OptionsScreen(
tunnelId = args.id,
focusRequester = focusRequester,
appUiState = appUiState,
)
}
composable<Route.Lock> {
PinLockScreen(
appViewModel = viewModel,
)
}
composable<Route.Scanner> {
ScannerScreen()
)
},
) {
Box(modifier = Modifier.fillMaxSize().padding(it)) {
NavHost(
navController,
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
startDestination = (if (appUiState.generalState.isPinLockEnabled == true) Route.Lock else Route.Main),
) {
composable<Route.Main> {
MainScreen(
uiState = appUiState,
)
}
composable<Route.Settings> {
SettingsScreen(
appViewModel = viewModel,
uiState = appUiState,
)
}
composable<Route.LocationDisclosure> {
LocationDisclosureScreen(viewModel, appUiState)
}
composable<Route.AutoTunnel> {
AutoTunnelScreen(
appUiState,
)
}
composable<Route.Appearance> {
AppearanceScreen()
}
composable<Route.Language> {
LanguageScreen(localeStorage)
}
composable<Route.Display> {
DisplayScreen(appUiState)
}
composable<Route.Support> {
SupportScreen()
}
composable<Route.Logs> {
LogsScreen()
}
composable<Route.Config> {
val args = it.toRoute<Route.Config>()
ConfigScreen(
tunnelId = args.id,
)
}
composable<Route.Option> {
val args = it.toRoute<Route.Option>()
OptionsScreen(
tunnelId = args.id,
appUiState = appUiState,
)
}
composable<Route.Lock> {
PinLockScreen(
appViewModel = viewModel,
)
}
composable<Route.Scanner> {
ScannerScreen()
}
}
}
}
@ -267,3 +248,4 @@ class MainActivity : AppCompatActivity() {
tunnelService.cancelStatsJob()
}
}

View File

@ -12,6 +12,9 @@ sealed class Route {
@Serializable
data object AutoTunnel : Route()
@Serializable
data object LocationDisclosure : Route()
@Serializable
data object Appearance : Route()

View File

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -17,7 +18,7 @@ fun ClickableIconButton(onClick: () -> Unit, onIconClick: () -> Unit, text: Stri
onClick = onClick,
enabled = enabled,
) {
Text(text, Modifier.weight(1f, false))
Text(text, Modifier.weight(1f, false), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary)
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Icon(
imageVector = icon,
@ -30,7 +31,7 @@ fun ClickableIconButton(onClick: () -> Unit, onIconClick: () -> Unit, text: Stri
if (enabled) {
onIconClick()
}
},
},
)
}
}

View File

@ -31,12 +31,10 @@ fun ExpandingRowListItem(
trailing: @Composable () -> Unit,
isExpanded: Boolean,
expanded: @Composable () -> Unit = {},
focusRequester: FocusRequester,
) {
Box(
modifier =
Modifier
.focusRequester(focusRequester)
.animateContentSize()
.clip(RoundedCornerShape(30.dp))
.combinedClickable(

View File

@ -2,16 +2,24 @@ package com.zaneschepke.wireguardautotunnel.ui.common.button
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
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.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@ -27,63 +35,61 @@ fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, lea
1.dp,
MaterialTheme.colorScheme.primary
) else null
val interactionSource =
androidx.compose.runtime.remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
androidx.compose.material3.Card(
Card(
modifier =
Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.clickable(interactionSource = interactionSource, indication = null) {
onClick()
},
shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp),
.height(IntrinsicSize.Min),
shape = RoundedCornerShape(8.dp),
border = border,
colors = androidx.compose.material3.CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
Column(
modifier =
Modifier
.padding(horizontal = 8.dp.scaledWidth(), vertical = 10.dp.scaledHeight())
.padding(end = 16.dp.scaledWidth()).padding(start = 8.dp.scaledWidth())
.fillMaxSize(),
verticalArrangement = androidx.compose.foundation.layout.Arrangement.Center,
horizontalAlignment = androidx.compose.ui.Alignment.Companion.Start,
) {
androidx.compose.foundation.layout.Row(
verticalAlignment = androidx.compose.ui.Alignment.Companion.CenterVertically,
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(16.dp.scaledWidth()),
) {
androidx.compose.foundation.layout.Row(
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(
16.dp.scaledWidth()
),
verticalAlignment = androidx.compose.ui.Alignment.Companion.CenterVertically,
modifier = Modifier.padding(vertical = if (description == null) 10.dp.scaledHeight() else 0.dp),
) {
leadingIcon?.let {
Icon(
leadingIcon,
leadingIcon.name,
Modifier.Companion.size(iconSize.scaledWidth()),
if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
)
}
Column {
Text(
title,
style = MaterialTheme.typography.titleMedium
)
description?.let {
Text(
description,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
}
}
Box(modifier = Modifier.clickable { onClick() }
.fillMaxWidth()) {
Column(
modifier =
Modifier
.padding(horizontal = 8.dp.scaledWidth(), vertical = 10.dp.scaledHeight())
.padding(end = 16.dp.scaledWidth()).padding(start = 8.dp.scaledWidth())
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start,
) {
Row(
verticalAlignment = Alignment.Companion.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp.scaledWidth()),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(
16.dp.scaledWidth()
),
verticalAlignment = Alignment.Companion.CenterVertically,
modifier = Modifier.padding(vertical = if (description == null) 10.dp.scaledHeight() else 0.dp),
) {
leadingIcon?.let {
Icon(
leadingIcon,
leadingIcon.name,
Modifier.size(iconSize),
if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
)
}
Column {
Text(
title,
style = MaterialTheme.typography.titleMedium
)
description?.let {
Text(
description,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
}
}
}
}
}

View File

@ -8,11 +8,11 @@ import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@Composable
fun ScaledSwitch(checked: Boolean, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true) {
fun ScaledSwitch(checked: Boolean, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true, modifier: Modifier = Modifier) {
Switch(
checked,
{ onClick(it) },
Modifier.scale((52.dp.scaledHeight() / 52.dp)),
enabled = enabled
modifier.scale((52.dp.scaledHeight() / 52.dp)),
enabled = enabled,
)
}

View File

@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.vector.ImageVector
data class SelectionItem(

View File

@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -10,8 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowRight
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
@ -27,67 +24,65 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
items.mapIndexed { index, it ->
Box(
contentAlignment = Alignment.Center,
modifier = (it.onClick?.let {
Modifier
.clickable {
it()
}
} ?: Modifier).fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
items.mapIndexed { index, item ->
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.then(item.onClick?.let { Modifier.clickable { it() }} ?: Modifier)
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(start = 16.dp.scaledWidth())
.weight(4f, false)
.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
) {
it.leadingIcon?.let { icon ->
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize.scaledWidth()),
)
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(start = if (it.leadingIcon != null) 16.dp.scaledWidth() else 0.dp)
.padding(vertical = if (it.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()),
.padding(start = 16.dp.scaledWidth())
.weight(4f, false)
.fillMaxWidth(),
) {
it.title()
it.description?.let {
item.leadingIcon?.let { icon ->
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize),
)
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier
.fillMaxWidth()
.padding(start = if (item.leadingIcon != null) 16.dp.scaledWidth() else 0.dp)
.padding(vertical = if (item.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()),
) {
item.title()
item.description?.let {
it()
}
}
}
item.trailing?.let {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier
.padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth())
.weight(1f),
) {
it()
}
}
}
it.trailing?.let {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier
.padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth())
.weight(1f),
) {
it()
}
}
}
if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
}
if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
}
}
}

View File

@ -32,7 +32,6 @@ fun SubmitConfigurationTextBox(
value: String?,
label: String,
hint: String,
focusRequester: FocusRequester,
isErrorValue: (value: String?) -> Boolean,
onSubmit: (value: String) -> Unit,
keyboardOptions: KeyboardOptions = KeyboardOptions(
@ -50,8 +49,7 @@ fun SubmitConfigurationTextBox(
OutlinedTextField(
isError = isErrorValue(stateValue),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
.fillMaxWidth(),
value = stateValue,
singleLine = true,
interactionSource = interactionSource,

View File

@ -0,0 +1,22 @@
package com.zaneschepke.wireguardautotunnel.ui.common.label
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@Composable
fun GroupLabel(title: String) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
Text(
title,
style = MaterialTheme.typography.titleMedium,
)
}
}

View File

@ -0,0 +1,33 @@
package com.zaneschepke.wireguardautotunnel.ui.common.label
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun VersionLabel() {
val clipboardManager = LocalClipboardManager.current
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
Text(
"${stringResource(R.string.version)}: ${BuildConfig.VERSION_NAME}",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.clickable {
clipboardManager.setText(AnnotatedString(BuildConfig.VERSION_NAME))
}
)
}
}

View File

@ -8,11 +8,21 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
import timber.log.Timber
@Composable
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
@ -20,15 +30,16 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
val navBackStackEntry by navController.currentBackStackEntryAsState()
showBottomBar = bottomNavItems.any {
navBackStackEntry?.isCurrentRoute(it.route) == true
navBackStackEntry?.isCurrentRoute(it.route::class) == true
}
if (showBottomBar) {
NavigationBar(
containerColor = MaterialTheme.colorScheme.surface,
) {
bottomNavItems.forEach { item ->
val selected = navBackStackEntry.isCurrentRoute(item.route)
bottomNavItems.forEachIndexed { index, item ->
val selected = navBackStackEntry.isCurrentRoute(item.route::class)
NavigationBarItem(
selected = selected,
onClick = {

View File

@ -5,10 +5,11 @@ import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import com.zaneschepke.wireguardautotunnel.ui.Route
import kotlin.reflect.KClass
@SuppressLint("RestrictedApi")
fun NavBackStackEntry?.isCurrentRoute(route: Route): Boolean {
fun <T : Route> NavBackStackEntry?.isCurrentRoute(cls: KClass<T>): Boolean {
return this?.destination?.hierarchy?.any {
it.hasRoute(route = route::class)
it.hasRoute(route = cls)
} == true
}

View File

@ -1,8 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui.common.navigation
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.focus.FocusRequester
import androidx.navigation.NavHostController
val LocalNavController = compositionLocalOf<NavHostController> {
error("NavController was not provided")
}
val LocalFocusRequester = compositionLocalOf<FocusRequester> { error("FocusRequester is not provided") }

View File

@ -11,14 +11,14 @@ import androidx.compose.runtime.Composable
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}) {
fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}, showBack: Boolean = true) {
val navController = LocalNavController.current
CenterAlignedTopAppBar(
title = {
Text(title)
},
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
if(showBack) IconButton(onClick = { navController.popBackStack() }) {
val icon = Icons.AutoMirrored.Outlined.ArrowBack
Icon(
imageVector = icon,

View File

@ -72,7 +72,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import kotlinx.coroutines.delay
@Composable
fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
fun ConfigScreen(tunnelId: Int) {
val viewModel = hiltViewModel<ConfigViewModel, ConfigViewModel.ConfigViewModelFactory> { factory ->
factory.create(tunnelId)
}
@ -102,18 +102,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
}
}
LaunchedEffect(Unit) {
if (!uiState.loading && context.isRunningOnTv()) {
delay(Constants.FOCUS_REQUEST_DELAY)
kotlin.runCatching {
focusRequester.requestFocus()
}.onFailure {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
}
}
LaunchedEffect(Unit) {
delay(2_000L)
viewModel.cleanUpUninstalledApps()
@ -194,7 +182,7 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
}
},
) {
Column(Modifier.padding(top = 24.dp.scaledHeight()).padding(it)) {
Column(Modifier.padding(it)) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
@ -219,7 +207,7 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
Modifier.fillMaxWidth(fillMaxWidth)
}
)
.padding(bottom = 10.dp),
.padding(bottom = 10.dp.scaledHeight()).padding(top = 24.dp.scaledHeight()),
) {
Column(
horizontalAlignment = Alignment.Start,
@ -237,7 +225,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
stringResource(id = R.string.show_amnezia_properties),
checked = derivedConfigType.value == ConfigType.AMNEZIA,
onCheckChanged = { configType = if (it) ConfigType.AMNEZIA else ConfigType.WIREGUARD },
modifier = Modifier.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.tunnelName,
@ -248,7 +235,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
OutlinedTextField(
modifier =
@ -362,7 +348,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMinSize,
@ -376,7 +361,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMaxSize,
@ -390,7 +374,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketJunkSize,
@ -401,7 +384,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketJunkSize,
@ -415,7 +397,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketMagicHeader,
@ -429,7 +410,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketMagicHeader,
@ -443,7 +423,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.underloadPacketMagicHeader,
@ -457,7 +436,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.transportPacketMagicHeader,
@ -471,7 +449,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
}
Row(

View File

@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint
import android.net.VpnService
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@ -9,23 +8,22 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -33,7 +31,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
@ -47,6 +44,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.NestedScrollListener
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.AutoTunnelRowItem
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel
@ -58,12 +56,10 @@ import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import kotlinx.coroutines.delay
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, focusRequester: FocusRequester) {
fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) {
val context = LocalContext.current
val navController = LocalNavController.current
val snackbar = SnackbarController.current
@ -73,6 +69,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
var isFabVisible by rememberSaveable { mutableStateOf(true) }
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val isRunningOnTv = remember { context.isRunningOnTv() }
val nestedScrollConnection = remember {
NestedScrollListener({ isFabVisible = false }, { isFabVisible = true })
@ -86,18 +83,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
},
)
LaunchedEffect(Unit) {
if (context.isRunningOnTv()) {
delay(Constants.FOCUS_REQUEST_DELAY)
runCatching {
focusRequester.requestFocus()
}.onFailure {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
}
}
val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = {
snackbar.showMessage(
context.getString(R.string.error_no_file_explorer),
@ -149,20 +134,37 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
selectedTunnel = null
},
)
}.windowInsetsPadding(WindowInsets.systemBars),
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
ScrollDismissFab({
if(!isRunningOnTv) ScrollDismissFab({
val icon = Icons.Filled.Add
Icon(
imageVector = icon,
contentDescription = icon.name,
tint = MaterialTheme.colorScheme.onPrimary,
)
}, focusRequester, isVisible = isFabVisible, onClick = {
}, isVisible = isFabVisible, onClick = {
showBottomSheet = true
})
},
topBar = {
if(isRunningOnTv) TopNavBar(
showBack = false,
title = stringResource(R.string.app_name),
trailing = {
IconButton(onClick = {
showBottomSheet = true
}) {
val icon = Icons.Outlined.Add
Icon(
imageVector = icon,
contentDescription = icon.name,
)
}
}
)
}
) {
TunnelImportSheet(
showBottomSheet,
@ -180,7 +182,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize()
.fillMaxSize().padding(it)
.overscroll(ScrollableDefaults.overscrollEffect())
.nestedScroll(nestedScrollConnection),
state = rememberLazyListState(0, uiState.tunnels.count()),
@ -192,10 +194,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
item {
GettingStartedLabel(onClick = { context.openWebUrl(it) })
}
}
if (uiState.settings.isAutoTunnelEnabled) {
} else {
item {
AutoTunnelRowItem(uiState.settings, { viewModel.onToggleAutoTunnelingPause() }, focusRequester)
AutoTunnelRowItem(uiState.settings, { viewModel.onToggleAutoTunnel(context) })
}
}
items(
@ -218,7 +219,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
onDelete = { showDeleteTunnelAlertDialog = true },
onCopy = { viewModel.onCopyTunnel(tunnel) },
onSwitchClick = { onTunnelToggle(it, tunnel) },
focusRequester = focusRequester,
)
}
}

View File

@ -160,6 +160,20 @@ constructor(
}
}
fun onToggleAutoTunnel(context: Context) = viewModelScope.launch {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) {
ServiceManager.stopWatcherService(context)
} else {
ServiceManager.startWatcherService(context)
}
appDataRepository.settings.save(
settings.copy(
isAutoTunnelEnabled = !settings.isAutoTunnelEnabled,
),
)
}
private suspend fun saveTunnelsFromZipUri(uri: Uri, context: Context) {
ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
generateSequence { zip.nextEntry }
@ -186,13 +200,6 @@ constructor(
saveTunnelConfigFromStream(stream, name)
}
fun onToggleAutoTunnelingPause() = viewModelScope.launch {
val settings = appDataRepository.settings.getSettings()
appDataRepository.settings.save(
settings.copy(isAutoTunnelPaused = !settings.isAutoTunnelPaused),
)
}
private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(tunnelConfig)
}

View File

@ -16,31 +16,20 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@Composable
fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit, focusRequester: FocusRequester) {
fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) {
val context = LocalContext.current
val itemFocusRequester = remember { FocusRequester() }
val autoTunnelingLabel =
buildAnnotatedString {
append(stringResource(id = R.string.auto_tunneling))
append(": ")
if (settings.isAutoTunnelPaused) {
append(
stringResource(id = R.string.paused),
)
} else {
append(
stringResource(id = R.string.active),
)
}
}
ExpandingRowListItem(
leading = {
val icon = Icons.Rounded.Bolt
@ -49,23 +38,23 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit, focusRequester:
icon.name,
modifier =
Modifier
.size(iconSize).scale(1.5f),
.size(16.dp.scaledHeight()).scale(1.5f),
tint =
if (settings.isAutoTunnelPaused) {
if (!settings.isAutoTunnelEnabled) {
Color.Gray
} else {
SilverTree
},
)
},
text = autoTunnelingLabel.text,
text = stringResource(R.string.auto_tunneling),
trailing = {
TextButton(
modifier = Modifier.focusRequester(itemFocusRequester),
onClick = { onToggle() },
) {
Text(stringResource(id = if (settings.isAutoTunnelPaused) R.string.resume else R.string.pause))
}
ScaledSwitch(
settings.isAutoTunnelEnabled,
onClick = {
onToggle()
}
)
},
onClick = {
if (context.isRunningOnTv()) {
@ -73,6 +62,5 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit, focusRequester:
}
},
isExpanded = false,
focusRequester = focusRequester,
)
}

View File

@ -14,14 +14,13 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp
@Composable
fun ScrollDismissFab(icon: @Composable () -> Unit, focusRequester: FocusRequester, isVisible: Boolean, onClick: () -> Unit) {
fun ScrollDismissFab(icon: @Composable () -> Unit, isVisible: Boolean, onClick: () -> Unit) {
AnimatedVisibility(
visible = isVisible,
enter = slideInVertically(initialOffsetY = { it * 2 }),
exit = slideOutVertically(targetOffsetY = { it * 2 }),
modifier =
Modifier
.focusRequester(focusRequester)
.focusGroup(),
) {
FloatingActionButton(

View File

@ -13,7 +13,6 @@ import androidx.compose.material.icons.rounded.Smartphone
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -23,16 +22,18 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@Composable
fun TunnelRowItem(
@ -46,7 +47,6 @@ fun TunnelRowItem(
onCopy: () -> Unit,
onDelete: () -> Unit,
onSwitchClick: (checked: Boolean) -> Unit,
focusRequester: FocusRequester,
) {
val leadingIconColor = if (!isActive) Color.Gray else vpnState.statistics.asColor()
val context = LocalContext.current
@ -69,7 +69,7 @@ fun TunnelRowItem(
icon,
icon.name,
tint = leadingIconColor,
modifier = Modifier.size(iconSize),
modifier = Modifier.size(16.dp.scaledHeight()),
)
},
text = tunnel.name,
@ -89,7 +89,6 @@ fun TunnelRowItem(
},
isExpanded = expanded && isActive,
expanded = { if (isActive && expanded) TunnelStatisticsRow(vpnState.statistics, tunnel) },
focusRequester = focusRequester,
trailing = {
if (
isSelected &&
@ -143,7 +142,6 @@ fun TunnelRowItem(
)
}
IconButton(
modifier = Modifier.focusRequester(focusRequester),
onClick = {
if (isActive) {
onClick()
@ -181,21 +179,17 @@ fun TunnelRowItem(
icon.name,
)
}
Switch(
ScaledSwitch(
modifier = Modifier.focusRequester(itemFocusRequester),
checked = isActive,
onCheckedChange = { checked ->
onSwitchClick(checked)
},
onClick = onSwitchClick
)
}
} else {
Switch(
ScaledSwitch(
modifier = Modifier.focusRequester(itemFocusRequester),
checked = isActive,
onCheckedChange = { checked ->
onSwitchClick(checked)
},
onClick = onSwitchClick
)
}
}

View File

@ -1,101 +1,74 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.PhoneAndroid
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.Star
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import kotlinx.coroutines.delay
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), focusRequester: FocusRequester, appUiState: AppUiState, tunnelId: Int) {
val scrollState = rememberScrollState()
val context = LocalContext.current
fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiState: AppUiState, tunnelId: Int) {
val navController = LocalNavController.current
val config = appUiState.tunnels.first { it.id == tunnelId }
val interactionSource = remember { MutableInteractionSource() }
val focusManager = LocalFocusManager.current
val screenPadding = 5.dp
val fillMaxWidth = .85f
var currentText by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
if (context.isRunningOnTv()) {
delay(Constants.FOCUS_REQUEST_DELAY)
kotlin.runCatching {
focusRequester.requestFocus()
}.onFailure {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
}
LaunchedEffect(config.tunnelNetworks) {
currentText = ""
}
fun saveTrustedSSID() {
if (currentText.isNotEmpty()) {
optionsViewModel.onSaveRunSSID(currentText, config)
currentText = ""
}
}
Scaffold(
topBar = {
TopNavBar(config.name, trailing = {
@ -111,219 +84,121 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), focusReq
)
}
})
},
}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize().padding(it)
.verticalScroll(scrollState)
.clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
.fillMaxSize()
.padding(it)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(
if (context.isRunningOnTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
}
)
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.general),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.set_primary_tunnel),
enabled = true,
checked = config.isPrimaryTunnel,
modifier =
Modifier
.focusRequester(focusRequester),
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel(config) },
)
}
}
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(
if (context.isRunningOnTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
}
)
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.auto_tunneling),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.mobile_data_tunnel),
enabled = true,
checked = config.isMobileDataTunnel,
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
)
Column {
FlowRow(
modifier =
Modifier
.padding(screenPadding)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
config.tunnelNetworks.forEach { ssid ->
ClickableIconButton(
onClick = {
if (context.isRunningOnTv()) {
focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid, config)
}
},
onIconClick = {
if (context.isRunningOnTv()) focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid, config)
},
text = ssid,
icon = Icons.Filled.Close,
enabled = true,
)
}
if (config.tunnelNetworks.isEmpty()) {
Text(
stringResource(R.string.no_wifi_names_configured),
fontStyle = FontStyle.Italic,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
OutlinedTextField(
enabled = true,
value = currentText,
onValueChange = { currentText = it },
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
supportingText = { WildcardSupportingLabel { context.openWebUrl(it) } },
modifier =
Modifier
.padding(
start = screenPadding,
top = 5.dp,
bottom = 10.dp,
),
maxLines = 1,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
trailingIcon = {
if (currentText != "") {
IconButton(onClick = { saveTrustedSSID() }) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription = stringResource(R.string.save_changes),
tint = MaterialTheme.colorScheme.primary,
GroupLabel(stringResource(R.string.auto_tunneling))
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Star,
title = { Text(stringResource(R.string.primary_tunnel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
description = {
Text(
stringResource(R.string.set_primary_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
config.isPrimaryTunnel,
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
)
},
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) }
),
SelectionItem(
Icons.Outlined.PhoneAndroid,
title = { Text(stringResource(R.string.mobile_tunnel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
description = {
Text(
stringResource(R.string.mobile_data_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
config.isMobileDataTunnel,
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
)
},
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) }
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = config.isPingEnabled,
onClick = { optionsViewModel.onToggleRestartOnPing(config) },
)
},
onClick = { optionsViewModel.onToggleRestartOnPing(config) }
),
SelectionItem(
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.weight(4f, false)
.fillMaxWidth(),
) {
val icon = Icons.Outlined.Security
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize),
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp.scaledWidth())
.padding(vertical = 6.dp.scaledHeight()),
) {
Text(
stringResource(R.string.use_tunnel_on_wifi_name),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
}
}
},
)
ConfigurationToggle(
stringResource(R.string.restart_on_ping),
enabled = !appUiState.settings.isPingEnabled,
checked = config.isPingEnabled || appUiState.settings.isPingEnabled,
onCheckChanged = { optionsViewModel.onToggleRestartOnPing(config) },
)
if (config.isPingEnabled || appUiState.settings.isPingEnabled) {
SubmitConfigurationTextBox(
config.pingIp,
stringResource(R.string.set_custom_ping_ip),
stringResource(R.string.default_ping_ip),
focusRequester,
isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingIp = it.ifBlank { null }),
)
},
)
fun isSecondsError(seconds: String?): Boolean {
return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 } ?: false
}
SubmitConfigurationTextBox(
config.pingInterval?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_internal),
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
focusRequester,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
),
isErrorValue = ::isSecondsError,
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
)
},
},
description = {
TrustedNetworkTextBox(
config.tunnelNetworks, onDelete = { optionsViewModel.onDeleteRunSSID(it, config) },
currentText = currentText,
onSave = { optionsViewModel.onSaveRunSSID(it, config) },
onValueChange = { currentText = it },
supporting = { if(appUiState.generalState.isWildcardsEnabled) {
WildcardsLabel()
}}
)
SubmitConfigurationTextBox(
config.pingCooldown?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_cooldown),
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
focusRequester,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
isErrorValue = ::isSecondsError,
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
}
}
}
}
},
)
)
)
}
}
}

View File

@ -32,6 +32,7 @@ constructor(
}
fun onSaveRunSSID(ssid: String, tunnelConfig: TunnelConfig) = viewModelScope.launch {
if(ssid.isBlank()) return@launch
val trimmed = ssid.trim()
val tunnelsWithName = appDataRepository.tunnels.findByTunnelNetworksName(trimmed)

View File

@ -11,6 +11,7 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -23,15 +24,9 @@ import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
import androidx.compose.material.icons.filled.AppShortcut
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.Restore
import androidx.compose.material.icons.filled.VpnLock
import androidx.compose.material.icons.outlined.AdminPanelSettings
import androidx.compose.material.icons.outlined.AppShortcut
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.FolderZip
@ -39,7 +34,6 @@ import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.Pin
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.VpnLock
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -49,7 +43,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
@ -58,7 +52,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
@ -66,13 +59,15 @@ import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusRequester
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDisclosure
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings
@ -87,13 +82,14 @@ import xyz.teamgravity.pin_lock_compose.PinManager
ExperimentalLayoutApi::class,
)
@Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, uiState: AppUiState, focusRequester: FocusRequester) {
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, uiState: AppUiState) {
val context = LocalContext.current
val navController = LocalNavController.current
val focusManager = LocalFocusManager.current
val snackbar = SnackbarController.current
val rootFocusRequester = LocalFocusRequester.current
val isRunningOnTv = remember { context.isRunningOnTv() }
val scrollState = rememberScrollState()
val interactionSource = remember { MutableInteractionSource() }
val settingsUiState by viewModel.uiState.collectAsStateWithLifecycle()
@ -119,9 +115,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == RESULT_OK)
if (accepted) {
viewModel.onToggleAutoTunnel(context)
} else {
if (!accepted) {
showVpnPermissionDialog = true
}
},
@ -141,20 +135,23 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
startForResult.launch(intent)
}
fun handleAutoTunnelToggle() {
if (!uiState.generalState.isBatteryOptimizationDisableShown &&
!isBatteryOptimizationsDisabled() && !context.isRunningOnTv()
) {
return requestBatteryOptimizationsDisabled()
}
val intent = if (!uiState.settings.isKernelEnabled) {
VpnService.prepare(context)
} else {
null
}
if (intent != null) return vpnActivityResultState.launch(intent)
viewModel.onToggleAutoTunnel(context)
}
// fun handleAutoTunnelToggle() {
// if (!uiState.generalState.isBatteryOptimizationDisableShown &&
// !isBatteryOptimizationsDisabled() && !isRunningOnTv
// ) {
// return requestBatteryOptimizationsDisabled()
// }
// val intent = if (!uiState.settings.isKernelEnabled) {
// VpnService.prepare(context)
// } else {
// null
// }
// if (intent != null) return vpnActivityResultState.launch(intent)
// viewModel.onToggleAutoTunnel(context)
// }
// fun checkFineLocationGranted() {
// isBackgroundLocationGranted =
@ -188,18 +185,6 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
// if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
// checkFineLocationGranted()
// }
if (!uiState.generalState.isLocationDisclosureShown) {
BackgroundLocationDisclosure(
onDismiss = { viewModel.setLocationDisclosureShown() },
onAttest = {
context.launchAppSettings()
viewModel.setLocationDisclosureShown()
},
scrollState,
focusRequester,
)
return
}
BackgroundLocationDialog(
showLocationDialog,
@ -207,11 +192,11 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
onAttest = { showLocationDialog = false },
)
LocationServicesDialog(
showLocationServicesAlertDialog,
onDismiss = { showVpnPermissionDialog = false },
onAttest = { handleAutoTunnelToggle() },
)
// LocationServicesDialog(
// showLocationServicesAlertDialog,
// onDismiss = { showVpnPermissionDialog = false },
// onAttest = { handleAutoTunnelToggle() },
// )
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
@ -243,121 +228,139 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
Modifier
.verticalScroll(rememberScrollState())
.fillMaxSize()
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()).clickable(
.padding(top = topPadding)
.padding(bottom = 40.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth())
.then(if(!isRunningOnTv) Modifier.clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
}.windowInsetsPadding(WindowInsets.systemBars),
} else Modifier)
) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Bolt,
title = { Text(stringResource(R.string.auto_tunneling), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
title = { Text(stringResource(R.string.auto_tunneling), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
description = {
Text(
"Configure on demand tunnel rules",
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
stringResource(R.string.on_demand_rules),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
onClick = {
if(!uiState.generalState.isLocationDisclosureShown) return@SelectionItem navController.navigate(Route.LocationDisclosure)
navController.navigate(Route.AutoTunnel)
},
trailing = {
val icon = Icons.AutoMirrored.Outlined.ArrowForward
Icon(icon, icon.name)
}
),
),
ForwardButton(Modifier.focusable().focusRequester(rootFocusRequester)) { navController.navigate(Route.AutoTunnel) }
},
)
)
)
SurfaceSelectionGroupButton(
buildList {
if (!isRunningOnTv) addAll(
listOf(
SelectionItem(
Icons.Filled.AppShortcut,
{
ScaledSwitch(
uiState.settings.isShortcutsEnabled,
onClick = { viewModel.onToggleShortcutsEnabled() },
)
},
title = {
Text(
stringResource(R.string.enabled_app_shortcuts),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface))
},
onClick = { viewModel.onToggleShortcutsEnabled() }
),
SelectionItem(
Icons.Outlined.VpnLock,
{
ScaledSwitch(
enabled = !(
(
uiState.settings.isTunnelOnWifiEnabled ||
uiState.settings.isTunnelOnEthernetEnabled ||
uiState.settings.isTunnelOnMobileDataEnabled
) &&
uiState.settings.isAutoTunnelEnabled
),
onClick = { viewModel.onToggleAlwaysOnVPN() },
checked = uiState.settings.isAlwaysOnVpnEnabled,
)
},
title = {
Text(
stringResource(R.string.always_on_vpn_support),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface))
},
onClick = { viewModel.onToggleAlwaysOnVPN() }
),
SelectionItem(
Icons.Outlined.AdminPanelSettings,
title = {
Text(
stringResource(R.string.kill_switch),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface))
},
onClick = {
context.launchVpnSettings()
},
trailing = {
ForwardButton { context.launchVpnSettings() }
},
)
)
)
add(
SelectionItem(
Icons.Outlined.Restore,
{
ScaledSwitch(
uiState.settings.isRestoreOnBootEnabled,
onClick = { viewModel.onToggleRestartAtBoot() },
)
},
title = {
Text(
stringResource(R.string.restart_at_boot),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface))
},
onClick = { viewModel.onToggleRestartAtBoot() }
)
)
}
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Filled.AppShortcut,
{
ScaledSwitch(
uiState.settings.isShortcutsEnabled,
onClick = { viewModel.onToggleShortcutsEnabled() },
)
},
title = {
Text(stringResource(R.string.enabled_app_shortcuts), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface))
},
),
SelectionItem(
Icons.Outlined.VpnLock,
{
ScaledSwitch(
enabled = !(
(
uiState.settings.isTunnelOnWifiEnabled ||
uiState.settings.isTunnelOnEthernetEnabled ||
uiState.settings.isTunnelOnMobileDataEnabled
) &&
uiState.settings.isAutoTunnelEnabled
),
onClick = { viewModel.onToggleAlwaysOnVPN() },
checked = uiState.settings.isAlwaysOnVpnEnabled,
)
},
title = {
Text(stringResource(R.string.always_on_vpn_support), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface))
},
),
SelectionItem(
Icons.Outlined.AdminPanelSettings,
title = { Text(stringResource(R.string.kill_switch), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = {
context.launchVpnSettings()
},
trailing = {
val icon = Icons.AutoMirrored.Outlined.ArrowForward
Icon(icon, icon.name)
}
),
SelectionItem(
Icons.Outlined.Restore,
{
ScaledSwitch(
uiState.settings.isRestoreOnBootEnabled,
onClick = { viewModel.onToggleRestartAtBoot() },
)
},
title = { Text(stringResource(R.string.restart_at_boot), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
),
listOf(SelectionItem(
Icons.AutoMirrored.Outlined.ViewQuilt,
title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = {
navController.navigate(Route.Appearance)
},
trailing = {
ForwardButton { navController.navigate(Route.Appearance) }
},
),
)
SurfaceSelectionGroupButton(
mutableListOf(
SelectionItem(
Icons.AutoMirrored.Outlined.ViewQuilt,
title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = {
navController.navigate(Route.Appearance)
},
trailing = {
val icon = Icons.AutoMirrored.Outlined.ArrowForward
Icon(icon, icon.name)
}
),
SelectionItem(
Icons.Outlined.Notifications,
title = { Text(stringResource(R.string.notifications), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
title = { Text(stringResource(R.string.notifications), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = {
context.launchNotificationSettings()
},
trailing = {
val icon = Icons.AutoMirrored.Outlined.ArrowForward
Icon(icon, icon.name)
}
ForwardButton { context.launchNotificationSettings() }
},
),
SelectionItem(
Icons.Outlined.Pin,
title = { Text(stringResource(R.string.enable_app_lock), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
title = { Text(stringResource(R.string.enable_app_lock), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
trailing = {
ScaledSwitch(
uiState.generalState.isPinLockEnabled,
@ -370,52 +373,61 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
}
},
)
}
),
),
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Code,
title = { Text(stringResource(R.string.kernel), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
description = {
Text(
"Use kernel backend (root only)",
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
uiState.settings.isKernelEnabled,
onClick = { viewModel.onToggleKernelMode() },
enabled = !(
uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled ||
(uiState.vpnState.status == TunnelState.UP) ||
!settingsUiState.isKernelAvailable
),
)
},
),
),
)
onClick = { if (uiState.generalState.isPinLockEnabled) {
appViewModel.onPinLockDisabled()
} else {
PinManager.initialize(context)
navController.navigate(Route.Lock)
} }
)
))
SurfaceSelectionGroupButton(
if(!isRunningOnTv) SurfaceSelectionGroupButton(listOf(
SelectionItem(
Icons.Outlined.Code,
title = { Text(stringResource(R.string.kernel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
description = {
Text(
stringResource(R.string.use_kernel),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
uiState.settings.isKernelEnabled,
onClick = { viewModel.onToggleKernelMode() },
enabled = !(
uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled ||
(uiState.vpnState.status == TunnelState.UP) ||
!settingsUiState.isKernelAvailable
),
)
},
onClick = {
viewModel.onToggleKernelMode()
}
),
))
if(!isRunningOnTv) SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.FolderZip,
title = { Text(stringResource(R.string.export_configs), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
title = { Text(stringResource(R.string.export_configs), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = {
if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required)
showAuthPrompt = true
},
trailing = {},
),
),
)
)
// Surface(
// tonalElevation = 2.dp,
// shadowElevation = 2.dp,

View File

@ -60,24 +60,6 @@ constructor(
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
}
fun onToggleAutoTunnel(context: Context) = viewModelScope.launch {
with(settings.value) {
var isAutoTunnelPaused = this.isAutoTunnelPaused
if (isAutoTunnelEnabled) {
ServiceManager.stopWatcherService(context)
} else {
ServiceManager.startWatcherService(context)
isAutoTunnelPaused = false
}
appDataRepository.settings.save(
copy(
isAutoTunnelEnabled = !isAutoTunnelEnabled,
isAutoTunnelPaused = isAutoTunnelPaused,
),
)
}
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(

View File

@ -2,70 +2,71 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Contrast
import androidx.compose.material.icons.outlined.Translate
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun AppearanceScreen() {
val navController = LocalNavController.current
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Translate,
title = { Text(stringResource(R.string.language), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = { navController.navigate(Route.Language) },
trailing = {
val icon = Icons.AutoMirrored.Outlined.ArrowForward
Icon(icon, icon.name)
}
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.appearance))
}
){
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize().padding(it)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Translate,
title = { Text(stringResource(R.string.language), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = { navController.navigate(Route.Language) },
trailing = {
ForwardButton { navController.navigate(Route.Language) }
}
),
),
),
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Contrast,
title = { Text(stringResource(R.string.display_theme), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = { navController.navigate(Route.Display) },
trailing = {
val icon = Icons.AutoMirrored.Outlined.ArrowForward
Icon(icon, icon.name)
}
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Contrast,
title = { Text(stringResource(R.string.display_theme), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = { navController.navigate(Route.Display) },
trailing = {
ForwardButton { navController.navigate(Route.Display) }
}
),
),
),
)
)
}
}
}

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -16,6 +17,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@ -23,37 +25,43 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun DisplayScreen(appUiState: AppUiState, viewModel: DisplayViewModel = hiltViewModel()) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.display_theme))
}
) {
IconSurfaceButton(
title = stringResource(R.string.automatic),
onClick = {
viewModel.onThemeChange(Theme.AUTOMATIC)
},
selected = appUiState.generalState.theme == Theme.AUTOMATIC,
)
IconSurfaceButton(
title = stringResource(R.string.light),
onClick = { viewModel.onThemeChange(Theme.LIGHT) },
selected = appUiState.generalState.theme == Theme.LIGHT,
)
IconSurfaceButton(
title = stringResource(R.string.dark),
onClick = { viewModel.onThemeChange(Theme.DARK) },
selected = appUiState.generalState.theme == Theme.DARK,
)
IconSurfaceButton(
title = stringResource(R.string.dynamic),
onClick = { viewModel.onThemeChange(Theme.DYNAMIC) },
selected = appUiState.generalState.theme == Theme.DYNAMIC,
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize()
.padding(it)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
IconSurfaceButton(
title = stringResource(R.string.automatic),
onClick = {
viewModel.onThemeChange(Theme.AUTOMATIC)
},
selected = appUiState.generalState.theme == Theme.AUTOMATIC,
)
IconSurfaceButton(
title = stringResource(R.string.light),
onClick = { viewModel.onThemeChange(Theme.LIGHT) },
selected = appUiState.generalState.theme == Theme.LIGHT,
)
IconSurfaceButton(
title = stringResource(R.string.dark),
onClick = { viewModel.onThemeChange(Theme.DARK) },
selected = appUiState.generalState.theme == Theme.DARK,
)
IconSurfaceButton(
title = stringResource(R.string.dynamic),
onClick = { viewModel.onThemeChange(Theme.DYNAMIC) },
selected = appUiState.generalState.theme == Theme.DYNAMIC,
)
}
}
}

View File

@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
@ -24,6 +26,7 @@ import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.SelectedLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.extensions.navigateAndForget
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@ -63,44 +66,50 @@ fun LanguageScreen(localeStorage: LocaleStorage) {
navController.navigateAndForget(Route.Main)
}
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()).windowInsetsPadding(WindowInsets.navigationBars),
) {
item {
SelectionItemButton(
buttonText = stringResource(R.string.automatic),
onClick = {
onChangeLocale(LocaleUtil.OPTION_PHONE_LANGUAGE)
},
trailing = {
if (currentLocale.value == LocaleUtil.OPTION_PHONE_LANGUAGE) {
SelectedLabel()
}
},
ripple = false,
)
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.language))
}
items(sortedLocales, key = { it }) { locale ->
SelectionItemButton(
buttonText = locale.getDisplayLanguage(locale).capitalize(locale) +
if (locale.toLanguageTag().contains("-")) " (${locale.getDisplayCountry(locale).capitalize(locale)})" else "",
onClick = {
onChangeLocale(locale.toLanguageTag())
},
trailing = {
if (locale.toLanguageTag() == currentLocale.value) {
SelectedLabel()
}
},
ripple = false,
)
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize().padding(it)
.padding(horizontal = 24.dp.scaledWidth()).windowInsetsPadding(WindowInsets.navigationBars),
) {
item {
Box(modifier = Modifier.padding(top = 24.dp.scaledHeight())) {
SelectionItemButton(
buttonText = stringResource(R.string.automatic),
onClick = {
onChangeLocale(LocaleUtil.OPTION_PHONE_LANGUAGE)
},
trailing = {
if (currentLocale.value == LocaleUtil.OPTION_PHONE_LANGUAGE) {
SelectedLabel()
}
},
ripple = false,
)
}
}
items(sortedLocales, key = { it }) { locale ->
SelectionItemButton(
buttonText = locale.getDisplayLanguage(locale).capitalize(locale) +
if (locale.toLanguageTag().contains("-")) " (${locale.getDisplayCountry(locale).capitalize(locale)})" else "",
onClick = {
onChangeLocale(locale.toLanguageTag())
},
trailing = {
if (locale.toLanguageTag() == currentLocale.value) {
SelectedLabel()
}
},
ripple = false,
)
}
}
}
}

View File

@ -4,34 +4,21 @@ import android.Manifest
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.isImeVisible
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Filter1
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.SignalCellular4Bar
import androidx.compose.material.icons.outlined.Wifi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -45,8 +32,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ -54,16 +39,15 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.isLocationServicesEnabled
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@ -96,137 +80,190 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
}
Scaffold(
contentWindowInsets = WindowInsets(0.dp),
topBar = {
TopNavBar(stringResource(R.string.auto_tunneling))
},
}
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.verticalScroll(rememberScrollState())
.fillMaxSize()
.padding(top = 24.dp.scaledHeight()).padding(it)
.padding(it)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
SurfaceSelectionGroupButton(
mutableListOf(
SelectionItem(
Icons.Outlined.Wifi,
title = { Text(stringResource(R.string.tunnel_on_wifi), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
description = {
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnWifiEnabled,
onClick = { checked ->
if (!checked || uiState.isRooted) viewModel.onToggleTunnelOnWifi().also { return@ScaledSwitch }
onAutoTunnelWifiChecked()
},
)
},
),
SelectionItem(
Icons.Outlined.SignalCellular4Bar,
title = {
Text(
stringResource(R.string.tunnel_mobile_data),
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnMobileDataEnabled,
onClick = { viewModel.onToggleTunnelOnMobileData() },
)
},
),
SelectionItem(
Icons.Outlined.SettingsEthernet,
title = {
Text(
stringResource(R.string.tunnel_on_ethernet),
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnEthernetEnabled,
onClick = { viewModel.onToggleTunnelOnEthernet() },
)
},
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = uiState.settings.isPingEnabled,
onClick = { viewModel.onToggleRestartOnPing() },
)
},
),
).apply {
buildList {
add(
SelectionItem(
Icons.Outlined.Wifi,
title = {
Text(
stringResource(R.string.tunnel_on_wifi),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)
)
},
description = {
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnWifiEnabled,
onClick = {
if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi()
.also { return@ScaledSwitch }
onAutoTunnelWifiChecked()
},
)
},
onClick = {
if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi()
.also { return@SelectionItem }
onAutoTunnelWifiChecked()
}
)
)
if (uiState.settings.isTunnelOnWifiEnabled) {
add(1,
SelectionItem(
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
) {
addAll(
listOf(
SelectionItem(
Icons.Outlined.Filter1,
title = {
Text(
stringResource(R.string.use_wildcards),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)
)
},
description = {
LearnMoreLinkLabel({context.openWebUrl(it)}, stringResource(id = R.string.docs_wildcards))
},
trailing = {
ScaledSwitch(
checked = uiState.generalState.isWildcardsEnabled,
onClick = {
viewModel.onToggleWildcards()
},
)
},
onClick = {
viewModel.onToggleWildcards()
}
),
SelectionItem(
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.weight(4f, false)
.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
) {
val icon = Icons.Outlined.Security
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize.scaledWidth()),
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp.scaledWidth())
.padding(vertical = 6.dp.scaledHeight()),
.weight(4f, false)
.fillMaxWidth(),
) {
Text(
"Trusted wifi names",
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
val icon = Icons.Outlined.Security
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize),
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp.scaledWidth())
.padding(vertical = 6.dp.scaledHeight()),
) {
Text(
stringResource(R.string.trusted_wifi_names),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
}
}
}
}
},
description = {
TrustedNetworkTextBox(
uiState.settings.trustedNetworkSSIDs, onDelete = viewModel::onDeleteTrustedSSID,
currentText = currentText,
onSave = viewModel::onSaveTrustedSSID,
onValueChange = { currentText = it }
},
description = {
TrustedNetworkTextBox(
uiState.settings.trustedNetworkSSIDs, onDelete = viewModel::onDeleteTrustedSSID,
currentText = currentText,
onSave = viewModel::onSaveTrustedSSID,
onValueChange = { currentText = it },
supporting = { if(uiState.generalState.isWildcardsEnabled) {
WildcardsLabel()
}}
)
},
)
))
}
}
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.SignalCellular4Bar,
title = {
Text(
stringResource(R.string.tunnel_mobile_data),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnMobileDataEnabled,
onClick = { viewModel.onToggleTunnelOnMobileData() },
)
},
onClick = {
viewModel.onToggleTunnelOnMobileData()
}
),
SelectionItem(
Icons.Outlined.SettingsEthernet,
title = {
Text(
stringResource(R.string.tunnel_on_ethernet),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnEthernetEnabled,
onClick = { viewModel.onToggleTunnelOnEthernet() },
)
},
onClick = {
viewModel.onToggleTunnelOnEthernet()
}
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = uiState.settings.isPingEnabled,
onClick = { viewModel.onToggleRestartOnPing() },
)
},
onClick = {
viewModel.onToggleRestartOnPing()
}
)
)
}
},
)
)
}
}
}

View File

@ -18,7 +18,6 @@ class AutoTunnelViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
) : ViewModel() {
private val settings = appDataRepository.settings.getSettingsFlow()
@ -44,6 +43,13 @@ constructor(
}
}
fun onToggleWildcards() = viewModelScope.launch {
val wildcards = appDataRepository.appState.isWildcardsEnabled()
appDataRepository.appState.setWildcardsEnabled(
!wildcards
)
}
fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(

View File

@ -16,6 +16,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@ -25,32 +26,28 @@ import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun TrustedNetworkTextBox(trustedNetworks: List<String>, onDelete: (ssid: String) -> Unit, currentText: String, onSave : (ssid: String) -> Unit, onValueChange: (network: String) -> Unit) {
fun TrustedNetworkTextBox(trustedNetworks: List<String>, onDelete: (ssid: String) -> Unit, currentText: String, onSave : (ssid: String) -> Unit, onValueChange: (network: String) -> Unit, supporting: @Composable () -> Unit) {
val context = LocalContext.current
Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())){
FlowRow(
modifier =
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally),
) {
trustedNetworks.forEach { ssid ->
ClickableIconButton(
onClick = {
if (context.isRunningOnTv()) {
//focusRequester.requestFocus()
onDelete(ssid)
}
},
onIconClick = {
//if (context.isRunningOnTv()) focusRequester.requestFocus()
onDelete(ssid)
},
text = ssid,
@ -59,17 +56,18 @@ fun TrustedNetworkTextBox(trustedNetworks: List<String>, onDelete: (ssid: String
}
}
CustomTextField(
textStyle = MaterialTheme.typography.bodySmall,
value = currentText,
onValueChange = onValueChange,
label = { Text(stringResource(R.string.add_trusted_ssid)) },
label = { Text(stringResource(R.string.add_wifi_name)) },
containerColor = MaterialTheme.colorScheme.surface,
supportingText = supporting,
modifier =
Modifier
.padding(
top = 5.dp,
bottom = 10.dp,
).fillMaxWidth().padding(end = 16.dp.scaledWidth()),
supportingText = { WildcardSupportingLabel { context.openWebUrl(it)} },
singleLine = true,
keyboardOptions =
KeyboardOptions(
@ -94,7 +92,7 @@ fun TrustedNetworkTextBox(trustedNetworks: List<String>, onDelete: (ssid: String
}
}
},
)
}
}

View File

@ -0,0 +1,17 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun WildcardsLabel() {
Text(
stringResource(R.string.wildcards_active),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline, fontStyle = FontStyle.Italic),
)
}

View File

@ -1,88 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.LocationOff
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
@Composable
fun BackgroundLocationDisclosure(onDismiss: () -> Unit, onAttest: () -> Unit, scrollState: ScrollState, focusRequester: FocusRequester) {
val context = LocalContext.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize()
.verticalScroll(scrollState),
) {
Icon(
Icons.Rounded.LocationOff,
contentDescription = stringResource(id = R.string.map),
modifier =
Modifier
.padding(30.dp)
.size(128.dp),
)
Text(
stringResource(R.string.prominent_background_location_title),
textAlign = TextAlign.Center,
modifier = Modifier.padding(30.dp),
fontSize = 20.sp,
)
Text(
stringResource(R.string.prominent_background_location_message),
textAlign = TextAlign.Center,
modifier = Modifier.padding(30.dp),
fontSize = 15.sp,
)
Row(
modifier =
if (context.isRunningOnTv()) {
Modifier
.fillMaxWidth()
.padding(10.dp)
} else {
Modifier
.fillMaxWidth()
.padding(30.dp)
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
TextButton(onClick = { onDismiss() }) {
Text(stringResource(id = R.string.no_thanks))
}
TextButton(
modifier = Modifier.focusRequester(focusRequester),
onClick = {
onAttest()
},
) {
Text(stringResource(id = R.string.turn_on))
}
}
}
}

View File

@ -0,0 +1,22 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@Composable
fun ForwardButton(modifier: Modifier = Modifier.focusable(), onClick: () -> Unit) {
IconButton(
modifier = modifier,
onClick = onClick
) {
val icon = Icons.AutoMirrored.Outlined.ArrowForward
Icon(icon, icon.name, Modifier.size(iconSize))
}
}

View File

@ -12,18 +12,18 @@ import androidx.compose.ui.text.withStyle
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun WildcardSupportingLabel(onClick: (url: String) -> Unit) {
fun LearnMoreLinkLabel(onClick: (url: String) -> Unit, url : String) {
// TODO update link when docs are fully updated
val gettingStarted =
buildAnnotatedString {
pushStringAnnotation(
tag = "details",
annotation = stringResource(id = R.string.docs_wildcards),
annotation = url,
)
withStyle(
style = SpanStyle(color = MaterialTheme.colorScheme.primary),
) {
append(stringResource(id = R.string.wildcard_supported))
append(stringResource(id = R.string.learn_more))
}
pop()
}

View File

@ -0,0 +1,95 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material.icons.rounded.PermScanWifi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun LocationDisclosureScreen(appViewModel: AppViewModel, appUiState: AppUiState) {
val context = LocalContext.current
val navController = LocalNavController.current
LaunchedEffect(Unit, appUiState) {
if(appUiState.generalState.isLocationDisclosureShown) navController.goFromRoot(Route.AutoTunnel)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier.fillMaxSize().padding(top = topPadding).padding(horizontal = 24.dp.scaledWidth()),
) {
val icon = Icons.Rounded.PermScanWifi
Icon(
icon,
contentDescription = icon.name,
modifier = Modifier
.padding(30.dp.scaledHeight())
.size(128.dp.scaledHeight()),
)
Text(
stringResource(R.string.prominent_background_location_title),
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
)
Text(
stringResource(R.string.prominent_background_location_message),
style = MaterialTheme.typography.bodyLarge
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.LocationOn,
title = { Text(stringResource(R.string.launch_app_settings), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = { context.launchAppSettings().also {
appViewModel.setLocationDisclosureShown()
} },
trailing = {
ForwardButton { context.launchAppSettings().also {
appViewModel.setLocationDisclosureShown()
} }
}
),
),
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
title = { Text(stringResource(R.string.skip), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = { appViewModel.setLocationDisclosureShown() },
trailing = {
ForwardButton { appViewModel.setLocationDisclosureShown() }
}
),
),
)
}
}

View File

@ -1,314 +1,131 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowForward
import androidx.compose.material.icons.rounded.Book
import androidx.compose.material.icons.rounded.FormatListNumbered
import androidx.compose.material.icons.rounded.Mail
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material.icons.filled.Book
import androidx.compose.material.icons.filled.LineStyle
import androidx.compose.material.icons.filled.Mail
import androidx.compose.material.icons.filled.Policy
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun SupportScreen(focusRequester: FocusRequester, appUiState: AppUiState) {
fun SupportScreen() {
val context = LocalContext.current
val navController = LocalNavController.current
val fillMaxWidth = .85f
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
.verticalScroll(rememberScrollState())
.focusable(),
) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
(
if (context.isRunningOnTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
}
)
.padding(bottom = 25.dp),
Modifier
.fillMaxSize()
.padding(top = topPadding)
.padding(horizontal = 24.dp.scaledWidth()),
) {
Column(modifier = Modifier.padding(20.dp)) {
val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward
Text(
stringResource(R.string.thank_you),
textAlign = TextAlign.Start,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 20.dp),
fontSize = 16.sp,
)
Text(
stringResource(id = R.string.support_help_text),
textAlign = TextAlign.Start,
fontSize = 16.sp,
modifier = Modifier.padding(bottom = 20.dp),
)
TextButton(
onClick = {
context.openWebUrl(
context.resources.getString(R.string.docs_url),
)
},
modifier =
Modifier
.padding(vertical = 5.dp)
.focusRequester(focusRequester),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Row {
val icon = Icons.Rounded.Book
Icon(icon, icon.name)
Text(
stringResource(id = R.string.docs_description),
textAlign = TextAlign.Justify,
modifier =
Modifier
.padding(start = 10.dp)
.weight(
weight = 1.0f,
fill = false,
),
softWrap = true,
)
GroupLabel(stringResource(R.string.thank_you))
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Filled.Book,
title = { Text(stringResource(R.string.docs_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
trailing = {
ForwardButton { context.openWebUrl(context.getString(R.string.docs_url)) }
},
onClick = {
context.openWebUrl(context.getString(R.string.docs_url))
}
Icon(
forwardIcon,
forwardIcon.name,
)
}
}
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground,
)
TextButton(
onClick = {
context.openWebUrl(
context.resources.getString(R.string.telegram_url),
)
},
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Row {
val icon = ImageVector.vectorResource(R.drawable.telegram)
Icon(
icon,
icon.name,
Modifier.size(25.dp),
)
Text(
stringResource(id = R.string.chat_description),
textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp),
)
}
Icon(
forwardIcon,
forwardIcon.name,
)
}
}
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground,
)
TextButton(
onClick = {
context.openWebUrl(
context.resources.getString(R.string.github_url),
)
},
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Row {
val icon = ImageVector.vectorResource(R.drawable.github)
Icon(
imageVector = icon,
icon.name,
Modifier.size(25.dp),
)
Text(
stringResource(id = R.string.open_issue),
textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp),
)
}
Icon(
forwardIcon,
forwardIcon.name,
)
}
}
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground,
)
TextButton(
onClick = { context.launchSupportEmail() },
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Row {
val icon = Icons.Rounded.Mail
Icon(icon, icon.name)
Text(
stringResource(id = R.string.email_description),
textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp),
)
}
Icon(
forwardIcon,
forwardIcon.name,
)
}
}
if (!context.isRunningOnTv()) {
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground,
)
TextButton(
onClick = { navController.navigate(Route.Logs) },
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Row {
val icon = Icons.Rounded.FormatListNumbered
Icon(icon, icon.name)
Text(
stringResource(id = R.string.read_logs),
textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp),
)
),
SelectionItem(
Icons.Filled.LineStyle,
title = { Text(stringResource(R.string.read_logs),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
trailing = {
ForwardButton {
navController.navigate(Route.Logs)
}
Icon(
Icons.AutoMirrored.Rounded.ArrowForward,
stringResource(id = R.string.go),
)
},
onClick = {
navController.navigate(Route.Logs)
}
}
}
}
}
Spacer(modifier = Modifier.weight(1f))
Text(
stringResource(id = R.string.privacy_policy),
style = TextStyle(textDecoration = TextDecoration.Underline),
fontSize = 16.sp,
modifier =
Modifier.clickable {
context.openWebUrl(
context.resources.getString(R.string.privacy_policy_url),
),
SelectionItem(
Icons.Filled.Policy,
title = { Text(stringResource(R.string.privacy_policy), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
trailing = {
ForwardButton { context.openWebUrl(context.getString(R.string.privacy_policy_url)) }
},
onClick = {
context.openWebUrl(context.getString(R.string.privacy_policy_url))
}
),
)
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
ImageVector.vectorResource(R.drawable.telegram),
title = { Text(stringResource(R.string.chat_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
trailing = {
ForwardButton {
context.openWebUrl(context.getString(R.string.telegram_url))
}
},
onClick = {
context.openWebUrl(context.getString(R.string.telegram_url))
}
),
SelectionItem(
ImageVector.vectorResource(R.drawable.github),
title = { Text(stringResource(R.string.open_issue), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
trailing = {
ForwardButton {
context.openWebUrl(context.getString(R.string.github_url))
}
},
onClick = {
context.openWebUrl(context.getString(R.string.github_url))
}
),
SelectionItem(
Icons.Filled.Mail,
title = { Text(stringResource(R.string.email_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
trailing = {
ForwardButton {
context.launchSupportEmail()
}
},
onClick = {
context.launchSupportEmail()
}
),
)
},
)
Row(
horizontalArrangement = Arrangement.spacedBy(25.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(25.dp),
) {
val version =
buildAnnotatedString {
append(stringResource(id = R.string.version))
append(": ")
append(BuildConfig.VERSION_NAME)
}
val mode =
buildAnnotatedString {
append(stringResource(R.string.mode))
append(": ")
when (appUiState.settings.isKernelEnabled) {
true -> append(stringResource(id = R.string.kernel))
false -> append(stringResource(id = R.string.userspace))
}
}
Text(version.text, modifier = Modifier.focusable())
Text(mode.text)
)
VersionLabel()
}
}
}

View File

@ -3,14 +3,16 @@ package com.zaneschepke.wireguardautotunnel.ui.theme
import androidx.compose.ui.graphics.Color
val OffWhite = Color(0xFFE5E1E5)
val LightGrey = Color(0xFF8D9D9F)
val LightGrey = Color(0xFFCAC4D0)
val Aqua = Color(0xFF76BEBD)
val SilverTree = Color(0xFF6DB58B)
val Plantation = Color(0xFF264A49)
val Shark = Color(0xFF21272A)
val BalticSea = Color(0xFF1C1B1F)
val Brick = Color(0xFFCE4257)
val Corn = Color(0xFFFBEC5D)
val Straw = Color(0xFFD4C483)
sealed class ThemeColors(
val background: Color,
@ -19,7 +21,7 @@ sealed class ThemeColors(
val secondary: Color,
val onSurface: Color,
) {
// TODO fix light theme colors
data object Light : ThemeColors(
background = LightGrey,
surface = OffWhite,

View File

@ -4,3 +4,4 @@ import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
val iconSize = 24.dp.scaledHeight()
val topPadding = 80.dp.scaledHeight()

View File

@ -49,12 +49,18 @@ fun WireguardAutoTunnelTheme(
content: @Composable () -> Unit,
) {
val context = LocalContext.current
val isDark = isSystemInDarkTheme()
var isDark = isSystemInDarkTheme()
val autoTheme = if(isDark) DarkColorScheme else LightColorScheme
val colorScheme = when(theme) {
Theme.AUTOMATIC -> autoTheme
Theme.DARK -> DarkColorScheme
Theme.LIGHT -> LightColorScheme
Theme.DARK -> {
isDark = true
DarkColorScheme
}
Theme.LIGHT -> {
isDark = false
LightColorScheme
}
Theme.DYNAMIC -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (isDark) {
@ -72,7 +78,7 @@ fun WireguardAutoTunnelTheme(
WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = Color.Transparent.toArgb()
window.navigationBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = isDark
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = !isDark
}
}

View File

@ -18,6 +18,7 @@ val inter = FontFamily(
val Typography =
Typography(
bodyLarge = TextStyle(
fontFamily = inter,
fontWeight = FontWeight.Normal,
fontSize = 16.sp.scaled(),
lineHeight = 24.sp.scaled(),
@ -26,12 +27,13 @@ val Typography =
bodySmall = TextStyle(
fontFamily = inter,
fontWeight = FontWeight.Normal,
fontSize = 13.sp.scaled(),
fontSize = 12.sp.scaled(),
lineHeight = 20.sp.scaled(),
letterSpacing = 1.sp,
color = LightGrey,
),
bodyMedium = TextStyle(
fontFamily = inter,
fontSize = 14.sp.scaled(),
lineHeight = 20.sp.scaled(),
fontWeight = FontWeight(400),
@ -54,7 +56,7 @@ val Typography =
titleMedium = TextStyle(
fontFamily = inter,
fontWeight = FontWeight.Bold,
fontSize = 17.sp.scaled(),
fontSize = 16.sp.scaled(),
lineHeight = 21.sp.scaled(),
letterSpacing = 0.sp,
),

View File

@ -66,6 +66,7 @@ fun Context.resizeWidth(dp: Dp): Dp {
}
fun Context.launchNotificationSettings() {
if(isRunningOnTv()) return launchAppSettings()
val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)

View File

@ -5,7 +5,7 @@ import com.wireguard.android.util.RootShell
import com.wireguard.config.Peer
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.ui.theme.Corn
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
@ -58,7 +58,7 @@ fun TunnelStatistics?.asColor(): Color {
?.let { statuses ->
when {
statuses.all { it == HandshakeStatus.HEALTHY } -> SilverTree
statuses.any { it == HandshakeStatus.STALE } -> Corn
statuses.any { it == HandshakeStatus.STALE } -> Straw
statuses.all { it == HandshakeStatus.NOT_STARTED } -> Color.Gray
else -> Color.Gray
}

View File

@ -3,8 +3,10 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
fun NavController.navigateAndForget(route: Route) {
navigate(route) {
@ -12,6 +14,22 @@ fun NavController.navigateAndForget(route: Route) {
}
}
fun NavController.goFromRoot(route: Route) {
if (currentBackStackEntry?.isCurrentRoute(route::class) == true) return
this.navigate(route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
restoreState = true
}
}
fun Dp.scaledHeight(): Dp {
return WireGuardAutoTunnel.instance.resizeHeight(this)
}

View File

@ -23,7 +23,7 @@
<string name="enable_auto_tunnel">Start auto-tunneling</string>
<string name="disable_auto_tunnel">Stop auto-tunneling</string>
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
<string name="privacy_policy">View Privacy Policy</string>
<string name="privacy_policy">View privacy policy</string>
<string name="okay">Okay</string>
<string name="tunnel_on_ethernet">Tunnel on ethernet</string>
<string name="prominent_background_location_message">This feature requires background location permission to enable Wi-Fi SSID monitoring even while the application is closed. For more details, please see the Privacy Policy linked on the Support screen.</string>
@ -193,7 +193,6 @@
<string name="set_custom_ping_internal">Ping interval (sec)</string>
<string name="optional_default">"optional, default: "</string>
<string name="set_custom_ping_cooldown">Ping restart cooldown (sec)</string>
<string name="wildcard_supported">Learn about supported wildcards.</string>
<string name="details">details</string>
<string name="show_amnezia_properties">Show Amnezia properties</string>
<string name="never">never</string>
@ -211,4 +210,14 @@
<string name="language">Language</string>
<string name="display_theme">Display theme</string>
<string name="selected">Selected</string>
<string name="trusted_wifi_names">Trusted wifi names</string>
<string name="add_wifi_name">Add wifi name</string>
<string name="on_demand_rules">On demand tunnel rules</string>
<string name="primary_tunnel">Primary tunnel</string>
<string name="mobile_tunnel">Mobile data tunnel</string>
<string name="skip">Skip</string>
<string name="launch_app_settings">Launch app settings</string>
<string name="use_wildcards">Use name wildcards</string>
<string name="learn_more">Learn more</string>
<string name="wildcards_active">Wildcards active</string>
</resources>