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.compose.ui.tooling.preview)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.material)
// test // test
testImplementation(libs.junit) testImplementation(libs.junit)

View File

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

View File

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

View File

@ -7,6 +7,7 @@ data class GeneralState(
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT, val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED, val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
val isWildcardsEnabled: Boolean = IS_WILDCARDS_ENABLED,
val theme: Theme = Theme.AUTOMATIC val theme: Theme = Theme.AUTOMATIC
) { ) {
companion object { companion object {
@ -14,5 +15,6 @@ data class GeneralState(
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false const val PIN_LOCK_ENABLED_DEFAULT = false
const val IS_TUNNEL_STATS_EXPANDED = 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 setPinLockEnabled(enabled: Boolean)
suspend fun isWildcardsEnabled(): Boolean
suspend fun setWildcardsEnabled(enabled: Boolean)
suspend fun isBatteryOptimizationDisableShown(): Boolean suspend fun isBatteryOptimizationDisableShown(): Boolean
suspend fun setBatteryOptimizationDisableShown(shown: Boolean) suspend fun setBatteryOptimizationDisableShown(shown: Boolean)

View File

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

View File

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

View File

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

View File

@ -12,6 +12,9 @@ sealed class Route {
@Serializable @Serializable
data object AutoTunnel : Route() data object AutoTunnel : Route()
@Serializable
data object LocationDisclosure : Route()
@Serializable @Serializable
data object Appearance : Route() 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.foundation.layout.size
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -17,7 +18,7 @@ fun ClickableIconButton(onClick: () -> Unit, onIconClick: () -> Unit, text: Stri
onClick = onClick, onClick = onClick,
enabled = enabled, 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)) Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Icon( Icon(
imageVector = icon, imageVector = icon,
@ -30,7 +31,7 @@ fun ClickableIconButton(onClick: () -> Unit, onIconClick: () -> Unit, text: Stri
if (enabled) { if (enabled) {
onIconClick() onIconClick()
} }
}, },
) )
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -10,8 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape 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.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@ -27,67 +24,65 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable @Composable
fun SurfaceSelectionGroupButton(items: List<SelectionItem>) { 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()
) { Card(
Row( modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, shape = RoundedCornerShape(8.dp),
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()), 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( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
.padding(start = 16.dp.scaledWidth())
.weight(4f, false)
.fillMaxWidth(),
) { ) {
it.leadingIcon?.let { icon -> Row(
Icon( verticalAlignment = Alignment.CenterVertically,
icon,
icon.name,
modifier = Modifier.size(iconSize.scaledWidth()),
)
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .padding(start = 16.dp.scaledWidth())
.padding(start = if (it.leadingIcon != null) 16.dp.scaledWidth() else 0.dp) .weight(4f, false)
.padding(vertical = if (it.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()), .fillMaxWidth(),
) { ) {
it.title() item.leadingIcon?.let { icon ->
it.description?.let { 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()
} }
} }
} }
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?, value: String?,
label: String, label: String,
hint: String, hint: String,
focusRequester: FocusRequester,
isErrorValue: (value: String?) -> Boolean, isErrorValue: (value: String?) -> Boolean,
onSubmit: (value: String) -> Unit, onSubmit: (value: String) -> Unit,
keyboardOptions: KeyboardOptions = KeyboardOptions( keyboardOptions: KeyboardOptions = KeyboardOptions(
@ -50,8 +49,7 @@ fun SubmitConfigurationTextBox(
OutlinedTextField( OutlinedTextField(
isError = isErrorValue(stateValue), isError = isErrorValue(stateValue),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth(),
.focusRequester(focusRequester),
value = stateValue, value = stateValue,
singleLine = true, singleLine = true,
interactionSource = interactionSource, 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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue 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.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import timber.log.Timber
@Composable @Composable
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) { fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
@ -20,15 +30,16 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
showBottomBar = bottomNavItems.any { showBottomBar = bottomNavItems.any {
navBackStackEntry?.isCurrentRoute(it.route) == true navBackStackEntry?.isCurrentRoute(it.route::class) == true
} }
if (showBottomBar) { if (showBottomBar) {
NavigationBar( NavigationBar(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
) { ) {
bottomNavItems.forEach { item -> bottomNavItems.forEachIndexed { index, item ->
val selected = navBackStackEntry.isCurrentRoute(item.route) val selected = navBackStackEntry.isCurrentRoute(item.route::class)
NavigationBarItem( NavigationBarItem(
selected = selected, selected = selected,
onClick = { onClick = {

View File

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

View File

@ -1,8 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui.common.navigation package com.zaneschepke.wireguardautotunnel.ui.common.navigation
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.focus.FocusRequester
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
val LocalNavController = compositionLocalOf<NavHostController> { val LocalNavController = compositionLocalOf<NavHostController> {
error("NavController was not provided") 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}) { fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}, showBack: Boolean = true) {
val navController = LocalNavController.current val navController = LocalNavController.current
CenterAlignedTopAppBar( CenterAlignedTopAppBar(
title = { title = {
Text(title) Text(title)
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) { if(showBack) IconButton(onClick = { navController.popBackStack() }) {
val icon = Icons.AutoMirrored.Outlined.ArrowBack val icon = Icons.AutoMirrored.Outlined.ArrowBack
Icon( Icon(
imageVector = icon, imageVector = icon,

View File

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

View File

@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint
import android.net.VpnService import android.net.VpnService
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts 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.ScrollableDefaults
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll import androidx.compose.foundation.overscroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -33,7 +31,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext 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.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController 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.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.AutoTunnelRowItem import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.AutoTunnelRowItem
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel 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.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import kotlinx.coroutines.delay
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, focusRequester: FocusRequester) { fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val snackbar = SnackbarController.current val snackbar = SnackbarController.current
@ -73,6 +69,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
var isFabVisible by rememberSaveable { mutableStateOf(true) } var isFabVisible by rememberSaveable { mutableStateOf(true) }
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) } var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val isRunningOnTv = remember { context.isRunningOnTv() }
val nestedScrollConnection = remember { val nestedScrollConnection = remember {
NestedScrollListener({ isFabVisible = false }, { isFabVisible = true }) 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 = { val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = {
snackbar.showMessage( snackbar.showMessage(
context.getString(R.string.error_no_file_explorer), context.getString(R.string.error_no_file_explorer),
@ -149,20 +134,37 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
selectedTunnel = null selectedTunnel = null
}, },
) )
}.windowInsetsPadding(WindowInsets.systemBars), },
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
floatingActionButton = { floatingActionButton = {
ScrollDismissFab({ if(!isRunningOnTv) ScrollDismissFab({
val icon = Icons.Filled.Add val icon = Icons.Filled.Add
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = icon.name, contentDescription = icon.name,
tint = MaterialTheme.colorScheme.onPrimary, tint = MaterialTheme.colorScheme.onPrimary,
) )
}, focusRequester, isVisible = isFabVisible, onClick = { }, isVisible = isFabVisible, onClick = {
showBottomSheet = true 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( TunnelImportSheet(
showBottomSheet, showBottomSheet,
@ -180,7 +182,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize().padding(it)
.overscroll(ScrollableDefaults.overscrollEffect()) .overscroll(ScrollableDefaults.overscrollEffect())
.nestedScroll(nestedScrollConnection), .nestedScroll(nestedScrollConnection),
state = rememberLazyListState(0, uiState.tunnels.count()), state = rememberLazyListState(0, uiState.tunnels.count()),
@ -192,10 +194,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
item { item {
GettingStartedLabel(onClick = { context.openWebUrl(it) }) GettingStartedLabel(onClick = { context.openWebUrl(it) })
} }
} } else {
if (uiState.settings.isAutoTunnelEnabled) {
item { item {
AutoTunnelRowItem(uiState.settings, { viewModel.onToggleAutoTunnelingPause() }, focusRequester) AutoTunnelRowItem(uiState.settings, { viewModel.onToggleAutoTunnel(context) })
} }
} }
items( items(
@ -218,7 +219,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
onDelete = { showDeleteTunnelAlertDialog = true }, onDelete = { showDeleteTunnelAlertDialog = true },
onCopy = { viewModel.onCopyTunnel(tunnel) }, onCopy = { viewModel.onCopyTunnel(tunnel) },
onSwitchClick = { onTunnelToggle(it, 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) { private suspend fun saveTunnelsFromZipUri(uri: Uri, context: Context) {
ZipInputStream(getInputStreamFromUri(uri, context)).use { zip -> ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
generateSequence { zip.nextEntry } generateSequence { zip.nextEntry }
@ -186,13 +200,6 @@ constructor(
saveTunnelConfigFromStream(stream, name) 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 { private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(tunnelConfig) 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.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem 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.SilverTree
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@Composable @Composable
fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit, focusRequester: FocusRequester) { fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val itemFocusRequester = remember { FocusRequester() } 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( ExpandingRowListItem(
leading = { leading = {
val icon = Icons.Rounded.Bolt val icon = Icons.Rounded.Bolt
@ -49,23 +38,23 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit, focusRequester:
icon.name, icon.name,
modifier = modifier =
Modifier Modifier
.size(iconSize).scale(1.5f), .size(16.dp.scaledHeight()).scale(1.5f),
tint = tint =
if (settings.isAutoTunnelPaused) { if (!settings.isAutoTunnelEnabled) {
Color.Gray Color.Gray
} else { } else {
SilverTree SilverTree
}, },
) )
}, },
text = autoTunnelingLabel.text, text = stringResource(R.string.auto_tunneling),
trailing = { trailing = {
TextButton( ScaledSwitch(
modifier = Modifier.focusRequester(itemFocusRequester), settings.isAutoTunnelEnabled,
onClick = { onToggle() }, onClick = {
) { onToggle()
Text(stringResource(id = if (settings.isAutoTunnelPaused) R.string.resume else R.string.pause)) }
} )
}, },
onClick = { onClick = {
if (context.isRunningOnTv()) { if (context.isRunningOnTv()) {
@ -73,6 +62,5 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit, focusRequester:
} }
}, },
isExpanded = false, isExpanded = false,
focusRequester = focusRequester,
) )
} }

View File

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

View File

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

View File

@ -1,101 +1,74 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.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.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.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.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.key.Key
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.platform.LocalFocusManager 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.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.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox 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.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle 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.util.Constants import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), focusRequester: FocusRequester, appUiState: AppUiState, tunnelId: Int) { fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiState: AppUiState, tunnelId: Int) {
val scrollState = rememberScrollState()
val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val config = appUiState.tunnels.first { it.id == tunnelId } 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("") } var currentText by remember { mutableStateOf("") }
LaunchedEffect(Unit) { LaunchedEffect(config.tunnelNetworks) {
if (context.isRunningOnTv()) { currentText = ""
delay(Constants.FOCUS_REQUEST_DELAY)
kotlin.runCatching {
focusRequester.requestFocus()
}.onFailure {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
}
} }
fun saveTrustedSSID() {
if (currentText.isNotEmpty()) {
optionsViewModel.onSaveRunSSID(currentText, config)
currentText = ""
}
}
Scaffold( Scaffold(
topBar = { topBar = {
TopNavBar(config.name, trailing = { TopNavBar(config.name, trailing = {
@ -111,219 +84,121 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), focusReq
) )
} }
}) })
}, }
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier = modifier =
Modifier Modifier
.fillMaxSize().padding(it) .fillMaxSize()
.verticalScroll(scrollState) .padding(it)
.clickable( .padding(top = 24.dp.scaledHeight())
indication = null, .padding(horizontal = 24.dp.scaledWidth()),
interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
) { ) {
Surface( GroupLabel(stringResource(R.string.auto_tunneling))
tonalElevation = 2.dp, SurfaceSelectionGroupButton(
shadowElevation = 2.dp, listOf(
shape = RoundedCornerShape(12.dp), SelectionItem(
color = MaterialTheme.colorScheme.surface, Icons.Outlined.Star,
modifier = title = { Text(stringResource(R.string.primary_tunnel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
( description = {
if (context.isRunningOnTv()) { Text(
Modifier stringResource(R.string.set_primary_tunnel),
.height(IntrinsicSize.Min) style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
.fillMaxWidth(fillMaxWidth) )
.padding(top = 10.dp) },
} else { trailing = {
Modifier ScaledSwitch(
.fillMaxWidth(fillMaxWidth) config.isPrimaryTunnel,
.padding(top = 20.dp) onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
} )
) },
.padding(bottom = 10.dp), onClick = { optionsViewModel.onTogglePrimaryTunnel(config) }
) { ),
Column( SelectionItem(
horizontalAlignment = Alignment.Start, Icons.Outlined.PhoneAndroid,
verticalArrangement = Arrangement.Top, title = { Text(stringResource(R.string.mobile_tunnel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
modifier = Modifier.padding(15.dp), description = {
) { Text(
SectionTitle( stringResource(R.string.mobile_data_tunnel),
title = stringResource(id = R.string.general), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
padding = screenPadding, )
) },
ConfigurationToggle( trailing = {
stringResource(R.string.set_primary_tunnel), ScaledSwitch(
enabled = true, config.isMobileDataTunnel,
checked = config.isPrimaryTunnel, onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
modifier = )
Modifier },
.focusRequester(focusRequester), onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) }
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel(config) }, ),
) SelectionItem(
} Icons.Outlined.NetworkPing,
} title = {
Surface( Text(
tonalElevation = 2.dp, stringResource(R.string.restart_on_ping),
shadowElevation = 2.dp, style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
shape = RoundedCornerShape(12.dp), )
color = MaterialTheme.colorScheme.surface, },
modifier = trailing = {
( ScaledSwitch(
if (context.isRunningOnTv()) { checked = config.isPingEnabled,
Modifier onClick = { optionsViewModel.onToggleRestartOnPing(config) },
.height(IntrinsicSize.Min) )
.fillMaxWidth(fillMaxWidth) },
.padding(top = 10.dp) onClick = { optionsViewModel.onToggleRestartOnPing(config) }
} else { ),
Modifier SelectionItem(
.fillMaxWidth(fillMaxWidth) title = {
.padding(top = 20.dp) Row(
} verticalAlignment = Alignment.CenterVertically,
) modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
.padding(bottom = 10.dp), ) {
) { Row(
Column( verticalAlignment = Alignment.CenterVertically,
horizontalAlignment = Alignment.Start, modifier = Modifier
verticalArrangement = Arrangement.Top, .weight(4f, false)
modifier = Modifier.padding(15.dp), .fillMaxWidth(),
) { ) {
SectionTitle( val icon = Icons.Outlined.Security
title = stringResource(id = R.string.auto_tunneling), Icon(
padding = screenPadding, icon,
) icon.name,
ConfigurationToggle( modifier = Modifier.size(iconSize),
stringResource(R.string.mobile_data_tunnel), )
enabled = true, Column(
checked = config.isMobileDataTunnel, horizontalAlignment = Alignment.Start,
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel(config) }, verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
) modifier = Modifier
Column { .fillMaxWidth()
FlowRow( .padding(start = 16.dp.scaledWidth())
modifier = .padding(vertical = 6.dp.scaledHeight()),
Modifier ) {
.padding(screenPadding) Text(
.fillMaxWidth(), stringResource(R.string.use_tunnel_on_wifi_name),
horizontalArrangement = Arrangement.spacedBy(5.dp), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
) {
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,
) )
} }
} }
},
)
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), description = {
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})", TrustedNetworkTextBox(
focusRequester, config.tunnelNetworks, onDelete = { optionsViewModel.onDeleteRunSSID(it, config) },
keyboardOptions = KeyboardOptions( currentText = currentText,
keyboardType = KeyboardType.Number, onSave = { optionsViewModel.onSaveRunSSID(it, config) },
imeAction = ImeAction.Done, onValueChange = { currentText = it },
), supporting = { if(appUiState.generalState.isWildcardsEnabled) {
isErrorValue = ::isSecondsError, WildcardsLabel()
onSubmit = { }}
optionsViewModel.saveTunnelChanges(
config.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
)
},
) )
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 { fun onSaveRunSSID(ssid: String, tunnelConfig: TunnelConfig) = viewModelScope.launch {
if(ssid.isBlank()) return@launch
val trimmed = ssid.trim() val trimmed = ssid.trim()
val tunnelsWithName = appDataRepository.tunnels.findByTunnelNetworksName(trimmed) 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.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.automirrored.outlined.ViewQuilt
import androidx.compose.material.icons.filled.AppShortcut 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.AdminPanelSettings
import androidx.compose.material.icons.outlined.AppShortcut
import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.FolderZip 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.Pin
import androidx.compose.material.icons.outlined.Restore import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.VpnLock import androidx.compose.material.icons.outlined.VpnLock
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -49,7 +43,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -58,7 +52,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel 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.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton 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.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog 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.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.screens.settings.components.LocationServicesDialog
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings
@ -87,13 +82,14 @@ import xyz.teamgravity.pin_lock_compose.PinManager
ExperimentalLayoutApi::class, ExperimentalLayoutApi::class,
) )
@Composable @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 context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val snackbar = SnackbarController.current val snackbar = SnackbarController.current
val rootFocusRequester = LocalFocusRequester.current
val isRunningOnTv = remember { context.isRunningOnTv() }
val scrollState = rememberScrollState()
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val settingsUiState by viewModel.uiState.collectAsStateWithLifecycle() val settingsUiState by viewModel.uiState.collectAsStateWithLifecycle()
@ -119,9 +115,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
ActivityResultContracts.StartActivityForResult(), ActivityResultContracts.StartActivityForResult(),
onResult = { onResult = {
val accepted = (it.resultCode == RESULT_OK) val accepted = (it.resultCode == RESULT_OK)
if (accepted) { if (!accepted) {
viewModel.onToggleAutoTunnel(context)
} else {
showVpnPermissionDialog = true showVpnPermissionDialog = true
} }
}, },
@ -141,20 +135,23 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
startForResult.launch(intent) startForResult.launch(intent)
} }
fun handleAutoTunnelToggle() { // fun handleAutoTunnelToggle() {
if (!uiState.generalState.isBatteryOptimizationDisableShown && // if (!uiState.generalState.isBatteryOptimizationDisableShown &&
!isBatteryOptimizationsDisabled() && !context.isRunningOnTv() // !isBatteryOptimizationsDisabled() && !isRunningOnTv
) { // ) {
return requestBatteryOptimizationsDisabled() // return requestBatteryOptimizationsDisabled()
} // }
val intent = if (!uiState.settings.isKernelEnabled) { // val intent = if (!uiState.settings.isKernelEnabled) {
VpnService.prepare(context) // VpnService.prepare(context)
} else { // } else {
null // null
} // }
if (intent != null) return vpnActivityResultState.launch(intent) // if (intent != null) return vpnActivityResultState.launch(intent)
viewModel.onToggleAutoTunnel(context) // viewModel.onToggleAutoTunnel(context)
} // }
// fun checkFineLocationGranted() { // fun checkFineLocationGranted() {
// isBackgroundLocationGranted = // isBackgroundLocationGranted =
@ -188,18 +185,6 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
// if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { // if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
// checkFineLocationGranted() // checkFineLocationGranted()
// } // }
if (!uiState.generalState.isLocationDisclosureShown) {
BackgroundLocationDisclosure(
onDismiss = { viewModel.setLocationDisclosureShown() },
onAttest = {
context.launchAppSettings()
viewModel.setLocationDisclosureShown()
},
scrollState,
focusRequester,
)
return
}
BackgroundLocationDialog( BackgroundLocationDialog(
showLocationDialog, showLocationDialog,
@ -207,11 +192,11 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
onAttest = { showLocationDialog = false }, onAttest = { showLocationDialog = false },
) )
LocationServicesDialog( // LocationServicesDialog(
showLocationServicesAlertDialog, // showLocationServicesAlertDialog,
onDismiss = { showVpnPermissionDialog = false }, // onDismiss = { showVpnPermissionDialog = false },
onAttest = { handleAutoTunnelToggle() }, // onAttest = { handleAutoTunnelToggle() },
) // )
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false }) VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
@ -243,121 +228,139 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
Modifier Modifier
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.fillMaxSize() .fillMaxSize()
.padding(top = 24.dp.scaledHeight()) .padding(top = topPadding)
.padding(horizontal = 24.dp.scaledWidth()).clickable( .padding(bottom = 40.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth())
.then(if(!isRunningOnTv) Modifier.clickable(
indication = null, indication = null,
interactionSource = interactionSource, interactionSource = interactionSource,
) { ) {
focusManager.clearFocus() focusManager.clearFocus()
}.windowInsetsPadding(WindowInsets.systemBars), } else Modifier)
) { ) {
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
listOf( listOf(
SelectionItem( SelectionItem(
Icons.Outlined.Bolt, 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 = { description = {
Text( Text(
"Configure on demand tunnel rules", stringResource(R.string.on_demand_rules),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline), style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
) )
}, },
onClick = { onClick = {
if(!uiState.generalState.isLocationDisclosureShown) return@SelectionItem navController.navigate(Route.LocationDisclosure)
navController.navigate(Route.AutoTunnel) navController.navigate(Route.AutoTunnel)
}, },
trailing = { trailing = {
val icon = Icons.AutoMirrored.Outlined.ArrowForward ForwardButton(Modifier.focusable().focusRequester(rootFocusRequester)) { navController.navigate(Route.AutoTunnel) }
Icon(icon, icon.name) },
} )
), )
), )
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( SurfaceSelectionGroupButton(
listOf( listOf(SelectionItem(
SelectionItem( Icons.AutoMirrored.Outlined.ViewQuilt,
Icons.Filled.AppShortcut, title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
{ onClick = {
ScaledSwitch( navController.navigate(Route.Appearance)
uiState.settings.isShortcutsEnabled, },
onClick = { viewModel.onToggleShortcutsEnabled() }, trailing = {
) ForwardButton { navController.navigate(Route.Appearance) }
}, },
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)) },
),
), ),
)
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( SelectionItem(
Icons.Outlined.Notifications, 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 = { onClick = {
context.launchNotificationSettings() context.launchNotificationSettings()
}, },
trailing = { trailing = {
val icon = Icons.AutoMirrored.Outlined.ArrowForward ForwardButton { context.launchNotificationSettings() }
Icon(icon, icon.name) },
}
), ),
SelectionItem( SelectionItem(
Icons.Outlined.Pin, 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 = { trailing = {
ScaledSwitch( ScaledSwitch(
uiState.generalState.isPinLockEnabled, 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 = { onClick = { if (uiState.generalState.isPinLockEnabled) {
ScaledSwitch( appViewModel.onPinLockDisabled()
uiState.settings.isKernelEnabled, } else {
onClick = { viewModel.onToggleKernelMode() }, PinManager.initialize(context)
enabled = !( navController.navigate(Route.Lock)
uiState.settings.isAutoTunnelEnabled || } }
uiState.settings.isAlwaysOnVpnEnabled || )
(uiState.vpnState.status == TunnelState.UP) || ))
!settingsUiState.isKernelAvailable
),
)
},
),
),
)
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( listOf(
SelectionItem( SelectionItem(
Icons.Outlined.FolderZip, 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 = { onClick = {
if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required) if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required)
showAuthPrompt = true showAuthPrompt = true
}, },
trailing = {},
), ),
), )
) )
// Surface( // Surface(
// tonalElevation = 2.dp, // tonalElevation = 2.dp,
// shadowElevation = 2.dp, // shadowElevation = 2.dp,

View File

@ -60,24 +60,6 @@ constructor(
appDataRepository.appState.setBatteryOptimizationDisableShown(true) 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 { fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(settings.value) { with(settings.value) {
appDataRepository.settings.save( 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding 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.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Contrast import androidx.compose.material.icons.outlined.Contrast
import androidx.compose.material.icons.outlined.Translate import androidx.compose.material.icons.outlined.Translate
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController 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.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import com.zaneschepke.wireguardautotunnel.R
@Composable @Composable
fun AppearanceScreen() { fun AppearanceScreen() {
val navController = LocalNavController.current val navController = LocalNavController.current
Column( Scaffold(
horizontalAlignment = Alignment.Start, topBar = {
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), TopNavBar(stringResource(R.string.appearance))
modifier = }
Modifier ){
.fillMaxSize() Column(
.windowInsetsPadding(WindowInsets.systemBars) horizontalAlignment = Alignment.Start,
.padding(top = 24.dp.scaledHeight()) verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
.padding(horizontal = 24.dp.scaledWidth()), modifier =
) { Modifier
SurfaceSelectionGroupButton( .fillMaxSize().padding(it)
listOf( .padding(top = 24.dp.scaledHeight())
SelectionItem( .padding(horizontal = 24.dp.scaledWidth()),
Icons.Outlined.Translate, ) {
title = { Text(stringResource(R.string.language), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, SurfaceSelectionGroupButton(
onClick = { navController.navigate(Route.Language) }, listOf(
trailing = { SelectionItem(
val icon = Icons.AutoMirrored.Outlined.ArrowForward Icons.Outlined.Translate,
Icon(icon, icon.name) 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(
SurfaceSelectionGroupButton( listOf(
listOf( SelectionItem(
SelectionItem( Icons.Outlined.Contrast,
Icons.Outlined.Contrast, title = { Text(stringResource(R.string.display_theme), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
title = { Text(stringResource(R.string.display_theme), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, onClick = { navController.navigate(Route.Display) },
onClick = { navController.navigate(Route.Display) }, trailing = {
trailing = { ForwardButton { navController.navigate(Route.Display) }
val icon = Icons.AutoMirrored.Outlined.ArrowForward }
Icon(icon, icon.name) ),
}
), ),
), )
) }
} }
} }

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -16,6 +17,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton 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.ui.theme.Theme
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@ -23,37 +25,43 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable @Composable
fun DisplayScreen(appUiState: AppUiState, viewModel: DisplayViewModel = hiltViewModel()) { fun DisplayScreen(appUiState: AppUiState, viewModel: DisplayViewModel = hiltViewModel()) {
Column( Scaffold(
horizontalAlignment = Alignment.Start, topBar = {
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), TopNavBar(stringResource(R.string.display_theme))
modifier = }
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) { ) {
IconSurfaceButton( Column(
title = stringResource(R.string.automatic), horizontalAlignment = Alignment.Start,
onClick = { verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
viewModel.onThemeChange(Theme.AUTOMATIC) modifier =
}, Modifier
selected = appUiState.generalState.theme == Theme.AUTOMATIC, .fillMaxSize()
) .padding(it)
IconSurfaceButton( .padding(top = 24.dp.scaledHeight())
title = stringResource(R.string.light), .padding(horizontal = 24.dp.scaledWidth()),
onClick = { viewModel.onThemeChange(Theme.LIGHT) }, ) {
selected = appUiState.generalState.theme == Theme.LIGHT, IconSurfaceButton(
) title = stringResource(R.string.automatic),
IconSurfaceButton( onClick = {
title = stringResource(R.string.dark), viewModel.onThemeChange(Theme.AUTOMATIC)
onClick = { viewModel.onThemeChange(Theme.DARK) }, },
selected = appUiState.generalState.theme == Theme.DARK, selected = appUiState.generalState.theme == Theme.AUTOMATIC,
) )
IconSurfaceButton( IconSurfaceButton(
title = stringResource(R.string.dynamic), title = stringResource(R.string.light),
onClick = { viewModel.onThemeChange(Theme.DYNAMIC) }, onClick = { viewModel.onThemeChange(Theme.LIGHT) },
selected = appUiState.generalState.theme == Theme.DYNAMIC, 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 package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars 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.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf 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.SelectedLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController 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.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.extensions.navigateAndForget import com.zaneschepke.wireguardautotunnel.util.extensions.navigateAndForget
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@ -63,44 +66,50 @@ fun LanguageScreen(localeStorage: LocaleStorage) {
navController.navigateAndForget(Route.Main) navController.navigateAndForget(Route.Main)
} }
LazyColumn( Scaffold(
horizontalAlignment = Alignment.CenterHorizontally, topBar = {
verticalArrangement = Arrangement.Top, TopNavBar(stringResource(R.string.language))
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,
)
} }
items(sortedLocales, key = { it }) { locale -> ) {
SelectionItemButton( LazyColumn(
buttonText = locale.getDisplayLanguage(locale).capitalize(locale) + horizontalAlignment = Alignment.CenterHorizontally,
if (locale.toLanguageTag().contains("-")) " (${locale.getDisplayCountry(locale).capitalize(locale)})" else "", verticalArrangement = Arrangement.Top,
onClick = { modifier =
onChangeLocale(locale.toLanguageTag()) Modifier
}, .fillMaxSize().padding(it)
trailing = { .padding(horizontal = 24.dp.scaledWidth()).windowInsetsPadding(WindowInsets.navigationBars),
if (locale.toLanguageTag() == currentLocale.value) { ) {
SelectedLabel() item {
} Box(modifier = Modifier.padding(top = 24.dp.scaledHeight())) {
}, SelectionItemButton(
ripple = false, 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth 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.padding
import androidx.compose.foundation.layout.size 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.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.outlined.Filter1
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.NetworkPing import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.Security import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsEthernet import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.SignalCellular4Bar import androidx.compose.material.icons.outlined.SignalCellular4Bar
import androidx.compose.material.icons.outlined.Wifi import androidx.compose.material.icons.outlined.Wifi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -45,8 +32,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle 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.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ -54,16 +39,15 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState 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.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar 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.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.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.isLocationServicesEnabled 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.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@ -96,137 +80,190 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
} }
Scaffold( Scaffold(
contentWindowInsets = WindowInsets(0.dp),
topBar = { topBar = {
TopNavBar(stringResource(R.string.auto_tunneling)) TopNavBar(stringResource(R.string.auto_tunneling))
}, }
) { ) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier = modifier =
Modifier Modifier
.verticalScroll(rememberScrollState())
.fillMaxSize() .fillMaxSize()
.padding(top = 24.dp.scaledHeight()).padding(it) .padding(it)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()), .padding(horizontal = 24.dp.scaledWidth()),
) { ) {
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
mutableListOf( buildList {
SelectionItem( add(
Icons.Outlined.Wifi, SelectionItem(
title = { Text(stringResource(R.string.tunnel_on_wifi), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, Icons.Outlined.Wifi,
description = { title = {
}, Text(
trailing = { stringResource(R.string.tunnel_on_wifi),
ScaledSwitch( style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)
enabled = !uiState.settings.isAlwaysOnVpnEnabled, )
checked = uiState.settings.isTunnelOnWifiEnabled, },
onClick = { checked -> description = {
if (!checked || uiState.isRooted) viewModel.onToggleTunnelOnWifi().also { return@ScaledSwitch } },
onAutoTunnelWifiChecked() trailing = {
}, ScaledSwitch(
) enabled = !uiState.settings.isAlwaysOnVpnEnabled,
}, checked = uiState.settings.isTunnelOnWifiEnabled,
), onClick = {
SelectionItem( if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi()
Icons.Outlined.SignalCellular4Bar, .also { return@ScaledSwitch }
title = { onAutoTunnelWifiChecked()
Text( },
stringResource(R.string.tunnel_mobile_data), )
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), },
) onClick = {
}, if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi()
trailing = { .also { return@SelectionItem }
ScaledSwitch( onAutoTunnelWifiChecked()
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 {
if (uiState.settings.isTunnelOnWifiEnabled) { if (uiState.settings.isTunnelOnWifiEnabled) {
add(1, addAll(
SelectionItem( listOf(
title = { SelectionItem(
Row( Icons.Outlined.Filter1,
verticalAlignment = Alignment.CenterVertically, title = {
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()), 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( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
.weight(4f, false)
.fillMaxWidth(),
) { ) {
val icon = Icons.Outlined.Security Row(
Icon( verticalAlignment = Alignment.CenterVertically,
icon,
icon.name,
modifier = Modifier.size(iconSize.scaledWidth()),
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .weight(4f, false)
.padding(start = 16.dp.scaledWidth()) .fillMaxWidth(),
.padding(vertical = 6.dp.scaledHeight()),
) { ) {
Text( val icon = Icons.Outlined.Security
"Trusted wifi names", Icon(
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), 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 = { description = {
TrustedNetworkTextBox( TrustedNetworkTextBox(
uiState.settings.trustedNetworkSSIDs, onDelete = viewModel::onDeleteTrustedSSID, uiState.settings.trustedNetworkSSIDs, onDelete = viewModel::onDeleteTrustedSSID,
currentText = currentText, currentText = currentText,
onSave = viewModel::onSaveTrustedSSID, onSave = viewModel::onSaveTrustedSSID,
onValueChange = { currentText = it } 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 @Inject
constructor( constructor(
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
) : ViewModel() { ) : ViewModel() {
private val settings = appDataRepository.settings.getSettingsFlow() 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 { fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch {
with(settings.value) { with(settings.value) {
appDataRepository.settings.save( appDataRepository.settings.save(

View File

@ -16,6 +16,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.R
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField 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.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @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 val context = LocalContext.current
Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())){ Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())){
FlowRow( FlowRow(
modifier = modifier =
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp), horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally),
) { ) {
trustedNetworks.forEach { ssid -> trustedNetworks.forEach { ssid ->
ClickableIconButton( ClickableIconButton(
onClick = { onClick = {
if (context.isRunningOnTv()) { if (context.isRunningOnTv()) {
//focusRequester.requestFocus()
onDelete(ssid) onDelete(ssid)
} }
}, },
onIconClick = { onIconClick = {
//if (context.isRunningOnTv()) focusRequester.requestFocus()
onDelete(ssid) onDelete(ssid)
}, },
text = ssid, text = ssid,
@ -59,17 +56,18 @@ fun TrustedNetworkTextBox(trustedNetworks: List<String>, onDelete: (ssid: String
} }
} }
CustomTextField( CustomTextField(
textStyle = MaterialTheme.typography.bodySmall,
value = currentText, value = currentText,
onValueChange = onValueChange, onValueChange = onValueChange,
label = { Text(stringResource(R.string.add_trusted_ssid)) }, label = { Text(stringResource(R.string.add_wifi_name)) },
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
supportingText = supporting,
modifier = modifier =
Modifier Modifier
.padding( .padding(
top = 5.dp, top = 5.dp,
bottom = 10.dp, bottom = 10.dp,
).fillMaxWidth().padding(end = 16.dp.scaledWidth()), ).fillMaxWidth().padding(end = 16.dp.scaledWidth()),
supportingText = { WildcardSupportingLabel { context.openWebUrl(it)} },
singleLine = true, singleLine = true,
keyboardOptions = keyboardOptions =
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 import com.zaneschepke.wireguardautotunnel.R
@Composable @Composable
fun WildcardSupportingLabel(onClick: (url: String) -> Unit) { fun LearnMoreLinkLabel(onClick: (url: String) -> Unit, url : String) {
// TODO update link when docs are fully updated // TODO update link when docs are fully updated
val gettingStarted = val gettingStarted =
buildAnnotatedString { buildAnnotatedString {
pushStringAnnotation( pushStringAnnotation(
tag = "details", tag = "details",
annotation = stringResource(id = R.string.docs_wildcards), annotation = url,
) )
withStyle( withStyle(
style = SpanStyle(color = MaterialTheme.colorScheme.primary), style = SpanStyle(color = MaterialTheme.colorScheme.primary),
) { ) {
append(stringResource(id = R.string.wildcard_supported)) append(stringResource(id = R.string.learn_more))
} }
pop() 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 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.Arrangement
import androidx.compose.foundation.layout.Column 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowForward import androidx.compose.material.icons.filled.Book
import androidx.compose.material.icons.rounded.Book import androidx.compose.material.icons.filled.LineStyle
import androidx.compose.material.icons.rounded.FormatListNumbered import androidx.compose.material.icons.filled.Mail
import androidx.compose.material.icons.rounded.Mail import androidx.compose.material.icons.filled.Policy
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource 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.dp
import androidx.compose.ui.unit.sp
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.Route 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.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.launchSupportEmail
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable @Composable
fun SupportScreen(focusRequester: FocusRequester, appUiState: AppUiState) { fun SupportScreen() {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val fillMaxWidth = .85f
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.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,
modifier = modifier =
( Modifier
if (context.isRunningOnTv()) { .fillMaxSize()
Modifier .padding(top = topPadding)
.height(IntrinsicSize.Min) .padding(horizontal = 24.dp.scaledWidth()),
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
}
)
.padding(bottom = 25.dp),
) { ) {
Column(modifier = Modifier.padding(20.dp)) { GroupLabel(stringResource(R.string.thank_you))
val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward SurfaceSelectionGroupButton(
Text( listOf(
stringResource(R.string.thank_you), SelectionItem(
textAlign = TextAlign.Start, Icons.Filled.Book,
fontWeight = FontWeight.Bold, title = { Text(stringResource(R.string.docs_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
modifier = Modifier.padding(bottom = 20.dp), trailing = {
fontSize = 16.sp, ForwardButton { context.openWebUrl(context.getString(R.string.docs_url)) }
) },
Text( onClick = {
stringResource(id = R.string.support_help_text), context.openWebUrl(context.getString(R.string.docs_url))
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,
)
} }
Icon( ),
forwardIcon, SelectionItem(
forwardIcon.name, Icons.Filled.LineStyle,
) title = { Text(stringResource(R.string.read_logs),
} style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
} trailing = {
HorizontalDivider( ForwardButton {
thickness = 0.5.dp, navController.navigate(Route.Logs)
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),
)
} }
Icon( },
Icons.AutoMirrored.Rounded.ArrowForward, onClick = {
stringResource(id = R.string.go), navController.navigate(Route.Logs)
)
} }
} ),
} SelectionItem(
} Icons.Filled.Policy,
} title = { Text(stringResource(R.string.privacy_policy), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
Spacer(modifier = Modifier.weight(1f)) trailing = {
Text( ForwardButton { context.openWebUrl(context.getString(R.string.privacy_policy_url)) }
stringResource(id = R.string.privacy_policy), },
style = TextStyle(textDecoration = TextDecoration.Underline), onClick = {
fontSize = 16.sp, context.openWebUrl(context.getString(R.string.privacy_policy_url))
modifier = }
Modifier.clickable { ),
context.openWebUrl(
context.resources.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()
}
),
) )
}, )
) VersionLabel()
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)
} }
} }
}

View File

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

View File

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

View File

@ -49,12 +49,18 @@ fun WireguardAutoTunnelTheme(
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val isDark = isSystemInDarkTheme() var isDark = isSystemInDarkTheme()
val autoTheme = if(isDark) DarkColorScheme else LightColorScheme val autoTheme = if(isDark) DarkColorScheme else LightColorScheme
val colorScheme = when(theme) { val colorScheme = when(theme) {
Theme.AUTOMATIC -> autoTheme Theme.AUTOMATIC -> autoTheme
Theme.DARK -> DarkColorScheme Theme.DARK -> {
Theme.LIGHT -> LightColorScheme isDark = true
DarkColorScheme
}
Theme.LIGHT -> {
isDark = false
LightColorScheme
}
Theme.DYNAMIC -> { Theme.DYNAMIC -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (isDark) { if (isDark) {
@ -72,7 +78,7 @@ fun WireguardAutoTunnelTheme(
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = Color.Transparent.toArgb() window.statusBarColor = Color.Transparent.toArgb()
window.navigationBarColor = 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 = val Typography =
Typography( Typography(
bodyLarge = TextStyle( bodyLarge = TextStyle(
fontFamily = inter,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp.scaled(), fontSize = 16.sp.scaled(),
lineHeight = 24.sp.scaled(), lineHeight = 24.sp.scaled(),
@ -26,12 +27,13 @@ val Typography =
bodySmall = TextStyle( bodySmall = TextStyle(
fontFamily = inter, fontFamily = inter,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 13.sp.scaled(), fontSize = 12.sp.scaled(),
lineHeight = 20.sp.scaled(), lineHeight = 20.sp.scaled(),
letterSpacing = 1.sp, letterSpacing = 1.sp,
color = LightGrey, color = LightGrey,
), ),
bodyMedium = TextStyle( bodyMedium = TextStyle(
fontFamily = inter,
fontSize = 14.sp.scaled(), fontSize = 14.sp.scaled(),
lineHeight = 20.sp.scaled(), lineHeight = 20.sp.scaled(),
fontWeight = FontWeight(400), fontWeight = FontWeight(400),
@ -54,7 +56,7 @@ val Typography =
titleMedium = TextStyle( titleMedium = TextStyle(
fontFamily = inter, fontFamily = inter,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 17.sp.scaled(), fontSize = 16.sp.scaled(),
lineHeight = 21.sp.scaled(), lineHeight = 21.sp.scaled(),
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),

View File

@ -66,6 +66,7 @@ fun Context.resizeWidth(dp: Dp): Dp {
} }
fun Context.launchNotificationSettings() { fun Context.launchNotificationSettings() {
if(isRunningOnTv()) return launchAppSettings()
val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) .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.wireguard.config.Peer
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics 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.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
@ -58,7 +58,7 @@ fun TunnelStatistics?.asColor(): Color {
?.let { statuses -> ?.let { statuses ->
when { when {
statuses.all { it == HandshakeStatus.HEALTHY } -> SilverTree 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 statuses.all { it == HandshakeStatus.NOT_STARTED } -> Color.Gray
else -> 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.Dp
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
fun NavController.navigateAndForget(route: Route) { fun NavController.navigateAndForget(route: Route) {
navigate(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 { fun Dp.scaledHeight(): Dp {
return WireGuardAutoTunnel.instance.resizeHeight(this) return WireGuardAutoTunnel.instance.resizeHeight(this)
} }

View File

@ -23,7 +23,7 @@
<string name="enable_auto_tunnel">Start auto-tunneling</string> <string name="enable_auto_tunnel">Start auto-tunneling</string>
<string name="disable_auto_tunnel">Stop auto-tunneling</string> <string name="disable_auto_tunnel">Stop auto-tunneling</string>
<string name="tunnel_mobile_data">Tunnel on mobile data</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="okay">Okay</string>
<string name="tunnel_on_ethernet">Tunnel on ethernet</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> <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="set_custom_ping_internal">Ping interval (sec)</string>
<string name="optional_default">"optional, default: "</string> <string name="optional_default">"optional, default: "</string>
<string name="set_custom_ping_cooldown">Ping restart cooldown (sec)</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="details">details</string>
<string name="show_amnezia_properties">Show Amnezia properties</string> <string name="show_amnezia_properties">Show Amnezia properties</string>
<string name="never">never</string> <string name="never">never</string>
@ -211,4 +210,14 @@
<string name="language">Language</string> <string name="language">Language</string>
<string name="display_theme">Display theme</string> <string name="display_theme">Display theme</string>
<string name="selected">Selected</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> </resources>