diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 202299a..c52f965 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -151,6 +151,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.appcompat) + implementation(libs.material) // test testImplementation(libs.junit) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1ae1216..180c821 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,12 @@ + + + diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt index 5ed880d..54c80c3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt @@ -26,6 +26,7 @@ class DataStoreManager( val currentSSID = stringPreferencesKey("CURRENT_SSID") val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED") val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED") + val wildcardsEnabled = booleanPreferencesKey("WILDCARDS_ENABLED") val theme = stringPreferencesKey("THEME") } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt index f53ca1f..db7be88 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt @@ -7,6 +7,7 @@ data class GeneralState( val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT, val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED, + val isWildcardsEnabled: Boolean = IS_WILDCARDS_ENABLED, val theme: Theme = Theme.AUTOMATIC ) { companion object { @@ -14,5 +15,6 @@ data class GeneralState( const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false const val PIN_LOCK_ENABLED_DEFAULT = false const val IS_TUNNEL_STATS_EXPANDED = false + const val IS_WILDCARDS_ENABLED = false } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt index 82e2cfd..142465c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt @@ -13,6 +13,10 @@ interface AppStateRepository { suspend fun setPinLockEnabled(enabled: Boolean) + suspend fun isWildcardsEnabled(): Boolean + + suspend fun setWildcardsEnabled(enabled: Boolean) + suspend fun isBatteryOptimizationDisableShown(): Boolean suspend fun setBatteryOptimizationDisableShown(shown: Boolean) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt index 06ac943..58260a6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt @@ -29,6 +29,14 @@ class DataStoreAppStateRepository( dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled) } + override suspend fun isWildcardsEnabled(): Boolean { + return dataStoreManager.getFromStore(DataStoreManager.wildcardsEnabled) ?: GeneralState.IS_WILDCARDS_ENABLED + } + + override suspend fun setWildcardsEnabled(enabled: Boolean) { + dataStoreManager.saveToDataStore(DataStoreManager.wildcardsEnabled, enabled) + } + override suspend fun isBatteryOptimizationDisableShown(): Boolean { return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown) ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT @@ -84,6 +92,7 @@ class DataStoreAppStateRepository( pref[DataStoreManager.pinLockEnabled] ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT, isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED, + isWildcardsEnabled = pref[DataStoreManager.wildcardsEnabled] ?: GeneralState.IS_WILDCARDS_ENABLED, theme = getTheme() ) } catch (e: IllegalArgumentException) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt index cad3e47..468303a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt @@ -37,34 +37,17 @@ class AppViewModel constructor( private val appDataRepository: AppDataRepository, private val tunnelService: Provider, - private val rootShell: Provider, @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { - private val _appUiState = MutableStateFlow(AppUiState()) - - val appUiState = _appUiState.onStart { - _appUiState.update { - it.copy( - isRooted = isRooted(), - isKernelAvailable = isKernelSupported(), - ) - } - }.stateIn( - viewModelScope + ioDispatcher, - SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), - AppUiState(), - ) - val uiState = combine( appDataRepository.settings.getSettingsFlow(), appDataRepository.tunnels.getTunnelConfigsFlow(), tunnelService.get().vpnState, appDataRepository.appState.generalStateFlow, - appUiState, - ) { settings, tunnels, tunnelState, generalState, appUiState -> - appUiState.copy( + ) { settings, tunnels, tunnelState, generalState -> + AppUiState( settings, tunnels, tunnelState, @@ -73,14 +56,13 @@ constructor( }.stateIn( viewModelScope + ioDispatcher, SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), - _appUiState.value, + AppUiState(), ) private val _isAppReady = MutableStateFlow(false) val isAppReady = _isAppReady.asStateFlow() init { - viewModelScope.launch { initPin() initAutoTunnel() @@ -116,14 +98,6 @@ constructor( if (settings.isAutoTunnelEnabled) ServiceManager.startWatcherService(WireGuardAutoTunnel.instance) } - fun setTunnels(tunnels: TunnelConfigs) = viewModelScope.launch(ioDispatcher) { - _appUiState.emit( - _appUiState.value.copy( - tunnels = tunnels, - ), - ) - } - fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) { PinManager.clearPin() appDataRepository.appState.setPinLockEnabled(false) @@ -133,20 +107,7 @@ constructor( appDataRepository.appState.setPinLockEnabled(true) } - private suspend fun isKernelSupported(): Boolean { - return withContext(ioDispatcher) { - WgQuickBackend.hasKernelSupport() - } - } - - private suspend fun isRooted(): Boolean { - return try { - withContext(ioDispatcher) { - rootShell.get().start() - } - true - } catch (_: Exception) { - false - } + fun setLocationDisclosureShown() = viewModelScope.launch { + appDataRepository.appState.setLocationDisclosureShown(true) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index be51b7b..b8d1583 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -8,14 +8,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Home import androidx.compose.material.icons.rounded.QuestionMark @@ -31,14 +27,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import com.zaneschepke.wireguardautotunnel.R @@ -50,9 +44,8 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem +import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusRequester import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController -import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar -import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen @@ -65,6 +58,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme @@ -104,7 +98,7 @@ class MainActivity : AppCompatActivity() { setContent { val appUiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycle = this.lifecycle) val navController = rememberNavController() - val navBackStackEntry by navController.currentBackStackEntryAsState() + val rootItemFocusRequester = remember { FocusRequester() } LaunchedEffect(appUiState.vpnState.status) { val context = this@MainActivity @@ -121,122 +115,109 @@ class MainActivity : AppCompatActivity() { } } - CompositionLocalProvider(LocalNavController provides navController) { - SnackbarControllerProvider { host -> - WireguardAutoTunnelTheme(theme = appUiState.generalState.theme){ - val focusRequester = remember { FocusRequester() } - Scaffold( - contentWindowInsets = WindowInsets(0.dp), - snackbarHost = { - SnackbarHost(host) { snackbarData: SnackbarData -> - CustomSnackBar( - snackbarData.visuals.message, - isRtl = false, - containerColor = - MaterialTheme.colorScheme.surfaceColorAtElevation( - 2.dp, - ), - ) - } - }, - modifier = - Modifier - .focusable() - .focusProperties { - if (navBackStackEntry?.isCurrentRoute(Route.Lock) == true) { - Unit - } else { - up = focusRequester + CompositionLocalProvider(LocalFocusRequester provides rootItemFocusRequester) { + CompositionLocalProvider(LocalNavController provides navController) { + SnackbarControllerProvider { host -> + WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) { + Scaffold( + contentWindowInsets = WindowInsets(0.dp), + snackbarHost = { + SnackbarHost(host) { snackbarData: SnackbarData -> + CustomSnackBar( + snackbarData.visuals.message, + isRtl = false, + containerColor = + MaterialTheme.colorScheme.surfaceColorAtElevation( + 2.dp, + ), + ) } }, - bottomBar = { - BottomNavBar( - navController, - listOf( - BottomNavItem( - name = stringResource(R.string.tunnels), - route = Route.Main, - icon = Icons.Rounded.Home, + bottomBar = { + BottomNavBar( + navController, + listOf( + BottomNavItem( + name = stringResource(R.string.tunnels), + route = Route.Main, + icon = Icons.Rounded.Home, + ), + BottomNavItem( + name = stringResource(R.string.settings), + route = Route.Settings, + icon = Icons.Rounded.Settings, + ), + BottomNavItem( + name = stringResource(R.string.support), + route = Route.Support, + icon = Icons.Rounded.QuestionMark, + ), ), - BottomNavItem( - name = stringResource(R.string.settings), - route = Route.Settings, - icon = Icons.Rounded.Settings, - ), - BottomNavItem( - name = stringResource(R.string.support), - route = Route.Support, - icon = Icons.Rounded.QuestionMark, - ), - ), - ) - }, - ) { - Box(modifier = Modifier.fillMaxSize().padding(it)) { - NavHost( - navController, - enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) }, - exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) }, - startDestination = (if (appUiState.generalState.isPinLockEnabled == true) Route.Lock else Route.Main), - ) { - composable { - MainScreen( - focusRequester = focusRequester, - uiState = appUiState, - ) - } - composable { - SettingsScreen( - appViewModel = viewModel, - uiState = appUiState, - focusRequester = focusRequester, - ) - } - composable { - AutoTunnelScreen( - appUiState, - ) - } - composable { - AppearanceScreen() - } - composable { - LanguageScreen(localeStorage) - } - composable { - DisplayScreen(appUiState) - } - composable { - SupportScreen( - focusRequester = focusRequester, - appUiState = appUiState, - ) - } - composable { - LogsScreen() - } - composable { - val args = it.toRoute() - ConfigScreen( - focusRequester = focusRequester, - tunnelId = args.id, - ) - } - composable { - val args = it.toRoute() - OptionsScreen( - tunnelId = args.id, - focusRequester = focusRequester, - appUiState = appUiState, - ) - } - composable { - PinLockScreen( - appViewModel = viewModel, - ) - } - composable { - ScannerScreen() + ) + }, + ) { + Box(modifier = Modifier.fillMaxSize().padding(it)) { + NavHost( + navController, + enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) }, + exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) }, + startDestination = (if (appUiState.generalState.isPinLockEnabled == true) Route.Lock else Route.Main), + ) { + composable { + MainScreen( + uiState = appUiState, + ) + } + composable { + SettingsScreen( + appViewModel = viewModel, + uiState = appUiState, + ) + } + composable { + LocationDisclosureScreen(viewModel, appUiState) + } + composable { + AutoTunnelScreen( + appUiState, + ) + } + composable { + AppearanceScreen() + } + composable { + LanguageScreen(localeStorage) + } + composable { + DisplayScreen(appUiState) + } + composable { + SupportScreen() + } + composable { + LogsScreen() + } + composable { + val args = it.toRoute() + ConfigScreen( + tunnelId = args.id, + ) + } + composable { + val args = it.toRoute() + OptionsScreen( + tunnelId = args.id, + appUiState = appUiState, + ) + } + composable { + PinLockScreen( + appViewModel = viewModel, + ) + } + composable { + ScannerScreen() + } } } } @@ -267,3 +248,4 @@ class MainActivity : AppCompatActivity() { tunnelService.cancelStatsJob() } } + diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt index 0d0e7d3..4a7d714 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt @@ -12,6 +12,9 @@ sealed class Route { @Serializable data object AutoTunnel : Route() + @Serializable + data object LocationDisclosure : Route() + @Serializable data object Appearance : Route() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt index cd1ed79..5cf74b2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -17,7 +18,7 @@ fun ClickableIconButton(onClick: () -> Unit, onIconClick: () -> Unit, text: Stri onClick = onClick, enabled = enabled, ) { - Text(text, Modifier.weight(1f, false)) + Text(text, Modifier.weight(1f, false), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary) Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) Icon( imageVector = icon, @@ -30,7 +31,7 @@ fun ClickableIconButton(onClick: () -> Unit, onIconClick: () -> Unit, text: Stri if (enabled) { onIconClick() } - }, + }, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ExpandingRowListItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ExpandingRowListItem.kt index a83a97d..0109326 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ExpandingRowListItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ExpandingRowListItem.kt @@ -31,12 +31,10 @@ fun ExpandingRowListItem( trailing: @Composable () -> Unit, isExpanded: Boolean, expanded: @Composable () -> Unit = {}, - focusRequester: FocusRequester, ) { Box( modifier = Modifier - .focusRequester(focusRequester) .animateContentSize() .clip(RoundedCornerShape(30.dp)) .combinedClickable( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/IconSurfaceButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/IconSurfaceButton.kt index f12fede..6ceb0c4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/IconSurfaceButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/IconSurfaceButton.kt @@ -2,16 +2,24 @@ package com.zaneschepke.wireguardautotunnel.ui.common.button import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp @@ -27,63 +35,61 @@ fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, lea 1.dp, MaterialTheme.colorScheme.primary ) else null - val interactionSource = - androidx.compose.runtime.remember { androidx.compose.foundation.interaction.MutableInteractionSource() } - androidx.compose.material3.Card( + Card( modifier = Modifier .fillMaxWidth() - .height(IntrinsicSize.Min) - .clickable(interactionSource = interactionSource, indication = null) { - onClick() - }, - shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp), + .height(IntrinsicSize.Min), + shape = RoundedCornerShape(8.dp), border = border, - colors = androidx.compose.material3.CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), ) { - Column( - modifier = - Modifier - .padding(horizontal = 8.dp.scaledWidth(), vertical = 10.dp.scaledHeight()) - .padding(end = 16.dp.scaledWidth()).padding(start = 8.dp.scaledWidth()) - .fillMaxSize(), - verticalArrangement = androidx.compose.foundation.layout.Arrangement.Center, - horizontalAlignment = androidx.compose.ui.Alignment.Companion.Start, - ) { - androidx.compose.foundation.layout.Row( - verticalAlignment = androidx.compose.ui.Alignment.Companion.CenterVertically, - horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(16.dp.scaledWidth()), - ) { - androidx.compose.foundation.layout.Row( - horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy( - 16.dp.scaledWidth() - ), - verticalAlignment = androidx.compose.ui.Alignment.Companion.CenterVertically, - modifier = Modifier.padding(vertical = if (description == null) 10.dp.scaledHeight() else 0.dp), - ) { - leadingIcon?.let { - Icon( - leadingIcon, - leadingIcon.name, - Modifier.Companion.size(iconSize.scaledWidth()), - if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, - ) - } - Column { - Text( - title, - style = MaterialTheme.typography.titleMedium - ) - description?.let { - Text( - description, - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, - ) - } - } - } - } - } + Box(modifier = Modifier.clickable { onClick() } + .fillMaxWidth()) { + Column( + modifier = + Modifier + .padding(horizontal = 8.dp.scaledWidth(), vertical = 10.dp.scaledHeight()) + .padding(end = 16.dp.scaledWidth()).padding(start = 8.dp.scaledWidth()) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start, + ) { + Row( + verticalAlignment = Alignment.Companion.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp.scaledWidth()), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy( + 16.dp.scaledWidth() + ), + verticalAlignment = Alignment.Companion.CenterVertically, + modifier = Modifier.padding(vertical = if (description == null) 10.dp.scaledHeight() else 0.dp), + ) { + leadingIcon?.let { + Icon( + leadingIcon, + leadingIcon.name, + Modifier.size(iconSize), + if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, + ) + } + Column { + Text( + title, + style = MaterialTheme.typography.titleMedium + ) + description?.let { + Text( + description, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } + } + } + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ScaledSwitch.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ScaledSwitch.kt index ec4c24e..f34fa3a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ScaledSwitch.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/ScaledSwitch.kt @@ -8,11 +8,11 @@ import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight @Composable -fun ScaledSwitch(checked: Boolean, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true) { +fun ScaledSwitch(checked: Boolean, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true, modifier: Modifier = Modifier) { Switch( checked, { onClick(it) }, - Modifier.scale((52.dp.scaledHeight() / 52.dp)), - enabled = enabled + modifier.scale((52.dp.scaledHeight() / 52.dp)), + enabled = enabled, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItem.kt index f92eec4..0260594 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SelectionItem.kt @@ -1,6 +1,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.button.surface import androidx.compose.runtime.Composable +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.vector.ImageVector data class SelectionItem( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SurfaceSelectionGroupButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SurfaceSelectionGroupButton.kt index 9f3fa47..6dcedbb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SurfaceSelectionGroupButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/button/surface/SurfaceSelectionGroupButton.kt @@ -1,7 +1,6 @@ package com.zaneschepke.wireguardautotunnel.ui.common.button.surface import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -10,8 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowRight import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider @@ -27,67 +24,65 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth @Composable fun SurfaceSelectionGroupButton(items: List) { - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - ) { - items.mapIndexed { index, it -> - Box( - contentAlignment = Alignment.Center, - modifier = (it.onClick?.let { - Modifier - .clickable { - it() - } - } ?: Modifier).fillMaxWidth() - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()), + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + ) { + items.mapIndexed { index, item -> + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .then(item.onClick?.let { Modifier.clickable { it() }} ?: Modifier) + .fillMaxWidth() ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(start = 16.dp.scaledWidth()) - .weight(4f, false) - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()), ) { - it.leadingIcon?.let { icon -> - Icon( - icon, - icon.name, - modifier = Modifier.size(iconSize.scaledWidth()), - ) - } - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), + Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .fillMaxWidth() - .padding(start = if (it.leadingIcon != null) 16.dp.scaledWidth() else 0.dp) - .padding(vertical = if (it.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()), + .padding(start = 16.dp.scaledWidth()) + .weight(4f, false) + .fillMaxWidth(), ) { - it.title() - it.description?.let { + item.leadingIcon?.let { icon -> + Icon( + icon, + icon.name, + modifier = Modifier.size(iconSize), + ) + } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), + modifier = Modifier + .fillMaxWidth() + .padding(start = if (item.leadingIcon != null) 16.dp.scaledWidth() else 0.dp) + .padding(vertical = if (item.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()), + ) { + item.title() + item.description?.let { + it() + } + } + } + item.trailing?.let { + Box( + contentAlignment = Alignment.CenterEnd, + modifier = Modifier + .padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth()) + .weight(1f), + ) { it() } } } - it.trailing?.let { - Box( - contentAlignment = Alignment.CenterEnd, - modifier = Modifier - .padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth()) - .weight(1f), - ) { - it() - } - } } + if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) } - if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) } } -} + diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt index ea94ce8..bb78fe2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt @@ -32,7 +32,6 @@ fun SubmitConfigurationTextBox( value: String?, label: String, hint: String, - focusRequester: FocusRequester, isErrorValue: (value: String?) -> Boolean, onSubmit: (value: String) -> Unit, keyboardOptions: KeyboardOptions = KeyboardOptions( @@ -50,8 +49,7 @@ fun SubmitConfigurationTextBox( OutlinedTextField( isError = isErrorValue(stateValue), modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), + .fillMaxWidth(), value = stateValue, singleLine = true, interactionSource = interactionSource, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/GroupLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/GroupLabel.kt new file mode 100644 index 0000000..fd2eea4 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/GroupLabel.kt @@ -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, + ) + } +} + diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/VersionLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/VersionLabel.kt new file mode 100644 index 0000000..16051a7 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/label/VersionLabel.kt @@ -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)) + } + ) + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt index d8bbfaa..4155b95 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt @@ -8,11 +8,21 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.compose.currentBackStackEntryAsState +import timber.log.Timber @Composable fun BottomNavBar(navController: NavController, bottomNavItems: List) { @@ -20,15 +30,16 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List - val selected = navBackStackEntry.isCurrentRoute(item.route) + bottomNavItems.forEachIndexed { index, item -> + val selected = navBackStackEntry.isCurrentRoute(item.route::class) NavigationBarItem( selected = selected, onClick = { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/Extensions.kt index ac518d5..23cd41e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/Extensions.kt @@ -5,10 +5,11 @@ import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavDestination.Companion.hierarchy import com.zaneschepke.wireguardautotunnel.ui.Route +import kotlin.reflect.KClass @SuppressLint("RestrictedApi") -fun NavBackStackEntry?.isCurrentRoute(route: Route): Boolean { +fun NavBackStackEntry?.isCurrentRoute(cls: KClass): Boolean { return this?.destination?.hierarchy?.any { - it.hasRoute(route = route::class) + it.hasRoute(route = cls) } == true } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/LocalNavController.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/LocalNavController.kt index be5c806..5e896b8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/LocalNavController.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/LocalNavController.kt @@ -1,8 +1,13 @@ package com.zaneschepke.wireguardautotunnel.ui.common.navigation import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.focus.FocusRequester import androidx.navigation.NavHostController val LocalNavController = compositionLocalOf { error("NavController was not provided") } + +val LocalFocusRequester = compositionLocalOf { error("FocusRequester is not provided") } + diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/TopNavBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/TopNavBar.kt index 05f479e..9feb69b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/TopNavBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/TopNavBar.kt @@ -11,14 +11,14 @@ import androidx.compose.runtime.Composable @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}) { +fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}, showBack: Boolean = true) { val navController = LocalNavController.current CenterAlignedTopAppBar( title = { Text(title) }, navigationIcon = { - IconButton(onClick = { navController.popBackStack() }) { + if(showBack) IconButton(onClick = { navController.popBackStack() }) { val icon = Icons.AutoMirrored.Outlined.ArrowBack Icon( imageVector = icon, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index 637038e..a2a859f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -72,7 +72,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import kotlinx.coroutines.delay @Composable -fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { +fun ConfigScreen(tunnelId: Int) { val viewModel = hiltViewModel { factory -> factory.create(tunnelId) } @@ -102,18 +102,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { } } - LaunchedEffect(Unit) { - if (!uiState.loading && context.isRunningOnTv()) { - delay(Constants.FOCUS_REQUEST_DELAY) - kotlin.runCatching { - focusRequester.requestFocus() - }.onFailure { - delay(Constants.FOCUS_REQUEST_DELAY) - focusRequester.requestFocus() - } - } - } - LaunchedEffect(Unit) { delay(2_000L) viewModel.cleanUpUninstalledApps() @@ -194,7 +182,7 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { } }, ) { - Column(Modifier.padding(top = 24.dp.scaledHeight()).padding(it)) { + Column(Modifier.padding(it)) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, @@ -219,7 +207,7 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { Modifier.fillMaxWidth(fillMaxWidth) } ) - .padding(bottom = 10.dp), + .padding(bottom = 10.dp.scaledHeight()).padding(top = 24.dp.scaledHeight()), ) { Column( horizontalAlignment = Alignment.Start, @@ -237,7 +225,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { stringResource(id = R.string.show_amnezia_properties), checked = derivedConfigType.value == ConfigType.AMNEZIA, onCheckChanged = { configType = if (it) ConfigType.AMNEZIA else ConfigType.WIREGUARD }, - modifier = Modifier.focusRequester(focusRequester), ) ConfigurationTextBox( value = uiState.tunnelName, @@ -248,7 +235,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { modifier = Modifier .fillMaxWidth() - .focusRequester(focusRequester), ) OutlinedTextField( modifier = @@ -362,7 +348,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { modifier = Modifier .fillMaxWidth() - .focusRequester(focusRequester), ) ConfigurationTextBox( value = uiState.interfaceProxy.junkPacketMinSize, @@ -376,7 +361,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { modifier = Modifier .fillMaxWidth() - .focusRequester(focusRequester), ) ConfigurationTextBox( value = uiState.interfaceProxy.junkPacketMaxSize, @@ -390,7 +374,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { modifier = Modifier .fillMaxWidth() - .focusRequester(focusRequester), ) ConfigurationTextBox( value = uiState.interfaceProxy.initPacketJunkSize, @@ -401,7 +384,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { modifier = Modifier .fillMaxWidth() - .focusRequester(focusRequester), ) ConfigurationTextBox( value = uiState.interfaceProxy.responsePacketJunkSize, @@ -415,7 +397,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { modifier = Modifier .fillMaxWidth() - .focusRequester(focusRequester), ) ConfigurationTextBox( value = uiState.interfaceProxy.initPacketMagicHeader, @@ -429,7 +410,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { modifier = Modifier .fillMaxWidth() - .focusRequester(focusRequester), ) ConfigurationTextBox( value = uiState.interfaceProxy.responsePacketMagicHeader, @@ -443,7 +423,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { modifier = Modifier .fillMaxWidth() - .focusRequester(focusRequester), ) ConfigurationTextBox( value = uiState.interfaceProxy.underloadPacketMagicHeader, @@ -457,7 +436,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { modifier = Modifier .fillMaxWidth() - .focusRequester(focusRequester), ) ConfigurationTextBox( value = uiState.interfaceProxy.transportPacketMagicHeader, @@ -471,7 +449,6 @@ fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) { modifier = Modifier .fillMaxWidth() - .focusRequester(focusRequester), ) } Row( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index 1282852..db40bfd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -1,6 +1,5 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main -import android.annotation.SuppressLint import android.net.VpnService import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -9,23 +8,22 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.overscroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.outlined.Add import androidx.compose.material3.FabPosition import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -33,7 +31,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext @@ -47,6 +44,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.NestedScrollListener import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController +import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.AutoTunnelRowItem import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel @@ -58,12 +56,10 @@ import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground -import kotlinx.coroutines.delay -@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalFoundationApi::class) @Composable -fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, focusRequester: FocusRequester) { +fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) { val context = LocalContext.current val navController = LocalNavController.current val snackbar = SnackbarController.current @@ -73,6 +69,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, var isFabVisible by rememberSaveable { mutableStateOf(true) } var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } var selectedTunnel by remember { mutableStateOf(null) } + val isRunningOnTv = remember { context.isRunningOnTv() } val nestedScrollConnection = remember { NestedScrollListener({ isFabVisible = false }, { isFabVisible = true }) @@ -86,18 +83,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, }, ) - LaunchedEffect(Unit) { - if (context.isRunningOnTv()) { - delay(Constants.FOCUS_REQUEST_DELAY) - runCatching { - focusRequester.requestFocus() - }.onFailure { - delay(Constants.FOCUS_REQUEST_DELAY) - focusRequester.requestFocus() - } - } - } - val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = { snackbar.showMessage( context.getString(R.string.error_no_file_explorer), @@ -149,20 +134,37 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, selectedTunnel = null }, ) - }.windowInsetsPadding(WindowInsets.systemBars), + }, floatingActionButtonPosition = FabPosition.End, floatingActionButton = { - ScrollDismissFab({ + if(!isRunningOnTv) ScrollDismissFab({ val icon = Icons.Filled.Add Icon( imageVector = icon, contentDescription = icon.name, tint = MaterialTheme.colorScheme.onPrimary, ) - }, focusRequester, isVisible = isFabVisible, onClick = { + }, isVisible = isFabVisible, onClick = { showBottomSheet = true }) }, + topBar = { + if(isRunningOnTv) TopNavBar( + showBack = false, + title = stringResource(R.string.app_name), + trailing = { + IconButton(onClick = { + showBottomSheet = true + }) { + val icon = Icons.Outlined.Add + Icon( + imageVector = icon, + contentDescription = icon.name, + ) + } + } + ) + } ) { TunnelImportSheet( showBottomSheet, @@ -180,7 +182,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, verticalArrangement = Arrangement.Top, modifier = Modifier - .fillMaxSize() + .fillMaxSize().padding(it) .overscroll(ScrollableDefaults.overscrollEffect()) .nestedScroll(nestedScrollConnection), state = rememberLazyListState(0, uiState.tunnels.count()), @@ -192,10 +194,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, item { GettingStartedLabel(onClick = { context.openWebUrl(it) }) } - } - if (uiState.settings.isAutoTunnelEnabled) { + } else { item { - AutoTunnelRowItem(uiState.settings, { viewModel.onToggleAutoTunnelingPause() }, focusRequester) + AutoTunnelRowItem(uiState.settings, { viewModel.onToggleAutoTunnel(context) }) } } items( @@ -218,7 +219,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, onDelete = { showDeleteTunnelAlertDialog = true }, onCopy = { viewModel.onCopyTunnel(tunnel) }, onSwitchClick = { onTunnelToggle(it, tunnel) }, - focusRequester = focusRequester, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index 4733e78..5454e47 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -160,6 +160,20 @@ constructor( } } + fun onToggleAutoTunnel(context: Context) = viewModelScope.launch { + val settings = appDataRepository.settings.getSettings() + if (settings.isAutoTunnelEnabled) { + ServiceManager.stopWatcherService(context) + } else { + ServiceManager.startWatcherService(context) + } + appDataRepository.settings.save( + settings.copy( + isAutoTunnelEnabled = !settings.isAutoTunnelEnabled, + ), + ) + } + private suspend fun saveTunnelsFromZipUri(uri: Uri, context: Context) { ZipInputStream(getInputStreamFromUri(uri, context)).use { zip -> generateSequence { zip.nextEntry } @@ -186,13 +200,6 @@ constructor( saveTunnelConfigFromStream(stream, name) } - fun onToggleAutoTunnelingPause() = viewModelScope.launch { - val settings = appDataRepository.settings.getSettings() - appDataRepository.settings.save( - settings.copy(isAutoTunnelPaused = !settings.isAutoTunnelPaused), - ) - } - private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch { appDataRepository.tunnels.save(tunnelConfig) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/AutoTunnelRowItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/AutoTunnelRowItem.kt index 54f6170..bf2fe1e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/AutoTunnelRowItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/AutoTunnelRowItem.kt @@ -16,31 +16,20 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem +import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight @Composable -fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit, focusRequester: FocusRequester) { +fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) { val context = LocalContext.current val itemFocusRequester = remember { FocusRequester() } - val autoTunnelingLabel = - buildAnnotatedString { - append(stringResource(id = R.string.auto_tunneling)) - append(": ") - if (settings.isAutoTunnelPaused) { - append( - stringResource(id = R.string.paused), - ) - } else { - append( - stringResource(id = R.string.active), - ) - } - } ExpandingRowListItem( leading = { val icon = Icons.Rounded.Bolt @@ -49,23 +38,23 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit, focusRequester: icon.name, modifier = Modifier - .size(iconSize).scale(1.5f), + .size(16.dp.scaledHeight()).scale(1.5f), tint = - if (settings.isAutoTunnelPaused) { + if (!settings.isAutoTunnelEnabled) { Color.Gray } else { SilverTree }, ) }, - text = autoTunnelingLabel.text, + text = stringResource(R.string.auto_tunneling), trailing = { - TextButton( - modifier = Modifier.focusRequester(itemFocusRequester), - onClick = { onToggle() }, - ) { - Text(stringResource(id = if (settings.isAutoTunnelPaused) R.string.resume else R.string.pause)) - } + ScaledSwitch( + settings.isAutoTunnelEnabled, + onClick = { + onToggle() + } + ) }, onClick = { if (context.isRunningOnTv()) { @@ -73,6 +62,5 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit, focusRequester: } }, isExpanded = false, - focusRequester = focusRequester, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/ScrollDismissMultiFab.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/ScrollDismissMultiFab.kt index fb4706a..dc750fd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/ScrollDismissMultiFab.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/ScrollDismissMultiFab.kt @@ -14,14 +14,13 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.dp @Composable -fun ScrollDismissFab(icon: @Composable () -> Unit, focusRequester: FocusRequester, isVisible: Boolean, onClick: () -> Unit) { +fun ScrollDismissFab(icon: @Composable () -> Unit, isVisible: Boolean, onClick: () -> Unit) { AnimatedVisibility( visible = isVisible, enter = slideInVertically(initialOffsetY = { it * 2 }), exit = slideOutVertically(targetOffsetY = { it * 2 }), modifier = Modifier - .focusRequester(focusRequester) .focusGroup(), ) { FloatingActionButton( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelRowItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelRowItem.kt index 6cd35fe..35011ff 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelRowItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/components/TunnelRowItem.kt @@ -13,7 +13,6 @@ import androidx.compose.material.icons.rounded.Smartphone import androidx.compose.material.icons.rounded.Star import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -23,16 +22,18 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem +import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController -import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.util.extensions.asColor import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight @Composable fun TunnelRowItem( @@ -46,7 +47,6 @@ fun TunnelRowItem( onCopy: () -> Unit, onDelete: () -> Unit, onSwitchClick: (checked: Boolean) -> Unit, - focusRequester: FocusRequester, ) { val leadingIconColor = if (!isActive) Color.Gray else vpnState.statistics.asColor() val context = LocalContext.current @@ -69,7 +69,7 @@ fun TunnelRowItem( icon, icon.name, tint = leadingIconColor, - modifier = Modifier.size(iconSize), + modifier = Modifier.size(16.dp.scaledHeight()), ) }, text = tunnel.name, @@ -89,7 +89,6 @@ fun TunnelRowItem( }, isExpanded = expanded && isActive, expanded = { if (isActive && expanded) TunnelStatisticsRow(vpnState.statistics, tunnel) }, - focusRequester = focusRequester, trailing = { if ( isSelected && @@ -143,7 +142,6 @@ fun TunnelRowItem( ) } IconButton( - modifier = Modifier.focusRequester(focusRequester), onClick = { if (isActive) { onClick() @@ -181,21 +179,17 @@ fun TunnelRowItem( icon.name, ) } - Switch( + ScaledSwitch( modifier = Modifier.focusRequester(itemFocusRequester), checked = isActive, - onCheckedChange = { checked -> - onSwitchClick(checked) - }, + onClick = onSwitchClick ) } } else { - Switch( + ScaledSwitch( modifier = Modifier.focusRequester(itemFocusRequester), checked = isActive, - onCheckedChange = { checked -> - onSwitchClick(checked) - }, + onClick = onSwitchClick ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt index 0a0bd25..d60c2db 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt @@ -1,101 +1,74 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.options -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.NetworkPing +import androidx.compose.material.icons.outlined.PhoneAndroid +import androidx.compose.material.icons.outlined.Security +import androidx.compose.material.icons.outlined.Star import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.Route -import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton -import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle -import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox +import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch +import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem +import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton +import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar -import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle -import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel -import com.zaneschepke.wireguardautotunnel.util.Constants -import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv -import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address -import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel +import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import kotlinx.coroutines.delay @OptIn(ExperimentalLayoutApi::class) @Composable -fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), focusRequester: FocusRequester, appUiState: AppUiState, tunnelId: Int) { - val scrollState = rememberScrollState() - val context = LocalContext.current +fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiState: AppUiState, tunnelId: Int) { val navController = LocalNavController.current val config = appUiState.tunnels.first { it.id == tunnelId } - val interactionSource = remember { MutableInteractionSource() } - val focusManager = LocalFocusManager.current - val screenPadding = 5.dp - val fillMaxWidth = .85f - var currentText by remember { mutableStateOf("") } - LaunchedEffect(Unit) { - if (context.isRunningOnTv()) { - delay(Constants.FOCUS_REQUEST_DELAY) - kotlin.runCatching { - focusRequester.requestFocus() - }.onFailure { - delay(Constants.FOCUS_REQUEST_DELAY) - focusRequester.requestFocus() - } - } + LaunchedEffect(config.tunnelNetworks) { + currentText = "" } - - fun saveTrustedSSID() { - if (currentText.isNotEmpty()) { - optionsViewModel.onSaveRunSSID(currentText, config) - currentText = "" - } - } - Scaffold( topBar = { TopNavBar(config.name, trailing = { @@ -111,219 +84,121 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), focusReq ) } }) - }, + } ) { Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), modifier = Modifier - .fillMaxSize().padding(it) - .verticalScroll(scrollState) - .clickable( - indication = null, - interactionSource = interactionSource, - ) { - focusManager.clearFocus() - }, + .fillMaxSize() + .padding(it) + .padding(top = 24.dp.scaledHeight()) + .padding(horizontal = 24.dp.scaledWidth()), ) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - ( - if (context.isRunningOnTv()) { - Modifier - .height(IntrinsicSize.Min) - .fillMaxWidth(fillMaxWidth) - .padding(top = 10.dp) - } else { - Modifier - .fillMaxWidth(fillMaxWidth) - .padding(top = 20.dp) - } - ) - .padding(bottom = 10.dp), - ) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp), - ) { - SectionTitle( - title = stringResource(id = R.string.general), - padding = screenPadding, - ) - ConfigurationToggle( - stringResource(R.string.set_primary_tunnel), - enabled = true, - checked = config.isPrimaryTunnel, - modifier = - Modifier - .focusRequester(focusRequester), - onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel(config) }, - ) - } - } - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - ( - if (context.isRunningOnTv()) { - Modifier - .height(IntrinsicSize.Min) - .fillMaxWidth(fillMaxWidth) - .padding(top = 10.dp) - } else { - Modifier - .fillMaxWidth(fillMaxWidth) - .padding(top = 20.dp) - } - ) - .padding(bottom = 10.dp), - ) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp), - ) { - SectionTitle( - title = stringResource(id = R.string.auto_tunneling), - padding = screenPadding, - ) - ConfigurationToggle( - stringResource(R.string.mobile_data_tunnel), - enabled = true, - checked = config.isMobileDataTunnel, - onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel(config) }, - ) - Column { - FlowRow( - modifier = - Modifier - .padding(screenPadding) - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(5.dp), - ) { - config.tunnelNetworks.forEach { ssid -> - ClickableIconButton( - onClick = { - if (context.isRunningOnTv()) { - focusRequester.requestFocus() - optionsViewModel.onDeleteRunSSID(ssid, config) - } - }, - onIconClick = { - if (context.isRunningOnTv()) focusRequester.requestFocus() - optionsViewModel.onDeleteRunSSID(ssid, config) - }, - text = ssid, - icon = Icons.Filled.Close, - enabled = true, - ) - } - if (config.tunnelNetworks.isEmpty()) { - Text( - stringResource(R.string.no_wifi_names_configured), - fontStyle = FontStyle.Italic, - color = MaterialTheme.colorScheme.onSurface, - ) - } - } - OutlinedTextField( - enabled = true, - value = currentText, - onValueChange = { currentText = it }, - label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) }, - supportingText = { WildcardSupportingLabel { context.openWebUrl(it) } }, - modifier = - Modifier - .padding( - start = screenPadding, - top = 5.dp, - bottom = 10.dp, - ), - maxLines = 1, - keyboardOptions = - KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }), - trailingIcon = { - if (currentText != "") { - IconButton(onClick = { saveTrustedSSID() }) { - Icon( - imageVector = Icons.Outlined.Add, - contentDescription = stringResource(R.string.save_changes), - tint = MaterialTheme.colorScheme.primary, + GroupLabel(stringResource(R.string.auto_tunneling)) + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.Star, + title = { Text(stringResource(R.string.primary_tunnel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + description = { + Text( + stringResource(R.string.set_primary_tunnel), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ScaledSwitch( + config.isPrimaryTunnel, + onClick = { optionsViewModel.onTogglePrimaryTunnel(config) }, + ) + }, + onClick = { optionsViewModel.onTogglePrimaryTunnel(config) } + ), + SelectionItem( + Icons.Outlined.PhoneAndroid, + title = { Text(stringResource(R.string.mobile_tunnel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + description = { + Text( + stringResource(R.string.mobile_data_tunnel), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ScaledSwitch( + config.isMobileDataTunnel, + onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) }, + ) + }, + onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) } + ), + SelectionItem( + Icons.Outlined.NetworkPing, + title = { + Text( + stringResource(R.string.restart_on_ping), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ScaledSwitch( + checked = config.isPingEnabled, + onClick = { optionsViewModel.onToggleRestartOnPing(config) }, + ) + }, + onClick = { optionsViewModel.onToggleRestartOnPing(config) } + ), + SelectionItem( + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .weight(4f, false) + .fillMaxWidth(), + ) { + val icon = Icons.Outlined.Security + Icon( + icon, + icon.name, + modifier = Modifier.size(iconSize), + ) + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp.scaledWidth()) + .padding(vertical = 6.dp.scaledHeight()), + ) { + Text( + stringResource(R.string.use_tunnel_on_wifi_name), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), ) } + } - }, - ) - ConfigurationToggle( - stringResource(R.string.restart_on_ping), - enabled = !appUiState.settings.isPingEnabled, - checked = config.isPingEnabled || appUiState.settings.isPingEnabled, - onCheckChanged = { optionsViewModel.onToggleRestartOnPing(config) }, - ) - if (config.isPingEnabled || appUiState.settings.isPingEnabled) { - SubmitConfigurationTextBox( - config.pingIp, - stringResource(R.string.set_custom_ping_ip), - stringResource(R.string.default_ping_ip), - focusRequester, - isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() }, - onSubmit = { - optionsViewModel.saveTunnelChanges( - config.copy(pingIp = it.ifBlank { null }), - ) - }, - ) - fun isSecondsError(seconds: String?): Boolean { - return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 } ?: false } - SubmitConfigurationTextBox( - config.pingInterval?.let { (it / 1000).toString() }, - stringResource(R.string.set_custom_ping_internal), - "(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})", - focusRequester, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done, - ), - isErrorValue = ::isSecondsError, - onSubmit = { - optionsViewModel.saveTunnelChanges( - config.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000), - ) - }, + + }, + description = { + TrustedNetworkTextBox( + config.tunnelNetworks, onDelete = { optionsViewModel.onDeleteRunSSID(it, config) }, + currentText = currentText, + onSave = { optionsViewModel.onSaveRunSSID(it, config) }, + onValueChange = { currentText = it }, + supporting = { if(appUiState.generalState.isWildcardsEnabled) { + WildcardsLabel() + }} ) - SubmitConfigurationTextBox( - config.pingCooldown?.let { (it / 1000).toString() }, - stringResource(R.string.set_custom_ping_cooldown), - "(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})", - focusRequester, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - ), - isErrorValue = ::isSecondsError, - onSubmit = { - optionsViewModel.saveTunnelChanges( - config.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000), - ) - }, - ) - } - } - } - } + }, + ) + ) + ) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt index eecd3df..d7d3236 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt @@ -32,6 +32,7 @@ constructor( } fun onSaveRunSSID(ssid: String, tunnelConfig: TunnelConfig) = viewModelScope.launch { + if(ssid.isBlank()) return@launch val trimmed = ssid.trim() val tunnelsWithName = appDataRepository.tunnels.findByTunnelNetworksName(trimmed) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index 5373b14..09a4067 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -11,6 +11,7 @@ import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity.RESULT_OK import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -23,15 +24,9 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowForward -import androidx.compose.material.icons.automirrored.outlined.ArrowRight import androidx.compose.material.icons.automirrored.outlined.ViewQuilt import androidx.compose.material.icons.filled.AppShortcut -import androidx.compose.material.icons.filled.Bolt -import androidx.compose.material.icons.filled.Restore -import androidx.compose.material.icons.filled.VpnLock import androidx.compose.material.icons.outlined.AdminPanelSettings -import androidx.compose.material.icons.outlined.AppShortcut import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.FolderZip @@ -39,7 +34,6 @@ import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material.icons.outlined.Pin import androidx.compose.material.icons.outlined.Restore import androidx.compose.material.icons.outlined.VpnLock -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -49,7 +43,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource @@ -58,7 +52,6 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.zaneschepke.wireguardautotunnel.R -import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.AppViewModel @@ -66,13 +59,15 @@ import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton +import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusRequester import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog -import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDisclosure +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog +import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings @@ -87,13 +82,14 @@ import xyz.teamgravity.pin_lock_compose.PinManager ExperimentalLayoutApi::class, ) @Composable -fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, uiState: AppUiState, focusRequester: FocusRequester) { +fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, uiState: AppUiState) { val context = LocalContext.current val navController = LocalNavController.current val focusManager = LocalFocusManager.current val snackbar = SnackbarController.current + val rootFocusRequester = LocalFocusRequester.current + val isRunningOnTv = remember { context.isRunningOnTv() } - val scrollState = rememberScrollState() val interactionSource = remember { MutableInteractionSource() } val settingsUiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -119,9 +115,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: ActivityResultContracts.StartActivityForResult(), onResult = { val accepted = (it.resultCode == RESULT_OK) - if (accepted) { - viewModel.onToggleAutoTunnel(context) - } else { + if (!accepted) { showVpnPermissionDialog = true } }, @@ -141,20 +135,23 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: startForResult.launch(intent) } - fun handleAutoTunnelToggle() { - if (!uiState.generalState.isBatteryOptimizationDisableShown && - !isBatteryOptimizationsDisabled() && !context.isRunningOnTv() - ) { - return requestBatteryOptimizationsDisabled() - } - val intent = if (!uiState.settings.isKernelEnabled) { - VpnService.prepare(context) - } else { - null - } - if (intent != null) return vpnActivityResultState.launch(intent) - viewModel.onToggleAutoTunnel(context) - } +// fun handleAutoTunnelToggle() { +// if (!uiState.generalState.isBatteryOptimizationDisableShown && +// !isBatteryOptimizationsDisabled() && !isRunningOnTv +// ) { +// return requestBatteryOptimizationsDisabled() +// } +// val intent = if (!uiState.settings.isKernelEnabled) { +// VpnService.prepare(context) +// } else { +// null +// } +// if (intent != null) return vpnActivityResultState.launch(intent) +// viewModel.onToggleAutoTunnel(context) +// } + + + // fun checkFineLocationGranted() { // isBackgroundLocationGranted = @@ -188,18 +185,6 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: // if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { // checkFineLocationGranted() // } - if (!uiState.generalState.isLocationDisclosureShown) { - BackgroundLocationDisclosure( - onDismiss = { viewModel.setLocationDisclosureShown() }, - onAttest = { - context.launchAppSettings() - viewModel.setLocationDisclosureShown() - }, - scrollState, - focusRequester, - ) - return - } BackgroundLocationDialog( showLocationDialog, @@ -207,11 +192,11 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: onAttest = { showLocationDialog = false }, ) - LocationServicesDialog( - showLocationServicesAlertDialog, - onDismiss = { showVpnPermissionDialog = false }, - onAttest = { handleAutoTunnelToggle() }, - ) +// LocationServicesDialog( +// showLocationServicesAlertDialog, +// onDismiss = { showVpnPermissionDialog = false }, +// onAttest = { handleAutoTunnelToggle() }, +// ) VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false }) @@ -243,121 +228,139 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: Modifier .verticalScroll(rememberScrollState()) .fillMaxSize() - .padding(top = 24.dp.scaledHeight()) - .padding(horizontal = 24.dp.scaledWidth()).clickable( + .padding(top = topPadding) + .padding(bottom = 40.dp.scaledHeight()) + .padding(horizontal = 24.dp.scaledWidth()) + .then(if(!isRunningOnTv) Modifier.clickable( indication = null, interactionSource = interactionSource, ) { focusManager.clearFocus() - }.windowInsetsPadding(WindowInsets.systemBars), + } else Modifier) ) { SurfaceSelectionGroupButton( listOf( SelectionItem( Icons.Outlined.Bolt, - title = { Text(stringResource(R.string.auto_tunneling), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + title = { Text(stringResource(R.string.auto_tunneling), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, description = { Text( - "Configure on demand tunnel rules", - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline), + stringResource(R.string.on_demand_rules), + style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), ) }, onClick = { + if(!uiState.generalState.isLocationDisclosureShown) return@SelectionItem navController.navigate(Route.LocationDisclosure) navController.navigate(Route.AutoTunnel) }, trailing = { - val icon = Icons.AutoMirrored.Outlined.ArrowForward - Icon(icon, icon.name) - } - ), - ), + ForwardButton(Modifier.focusable().focusRequester(rootFocusRequester)) { navController.navigate(Route.AutoTunnel) } + }, + ) + ) + ) + SurfaceSelectionGroupButton( + buildList { + if (!isRunningOnTv) addAll( + listOf( + SelectionItem( + Icons.Filled.AppShortcut, + { + ScaledSwitch( + uiState.settings.isShortcutsEnabled, + onClick = { viewModel.onToggleShortcutsEnabled() }, + ) + }, + title = { + Text( + stringResource(R.string.enabled_app_shortcuts), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) + }, + onClick = { viewModel.onToggleShortcutsEnabled() } + ), + SelectionItem( + Icons.Outlined.VpnLock, + { + ScaledSwitch( + enabled = !( + ( + uiState.settings.isTunnelOnWifiEnabled || + uiState.settings.isTunnelOnEthernetEnabled || + uiState.settings.isTunnelOnMobileDataEnabled + ) && + uiState.settings.isAutoTunnelEnabled + ), + onClick = { viewModel.onToggleAlwaysOnVPN() }, + checked = uiState.settings.isAlwaysOnVpnEnabled, + ) + }, + title = { + Text( + stringResource(R.string.always_on_vpn_support), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) + }, + onClick = { viewModel.onToggleAlwaysOnVPN() } + ), + SelectionItem( + Icons.Outlined.AdminPanelSettings, + title = { + Text( + stringResource(R.string.kill_switch), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) + }, + onClick = { + context.launchVpnSettings() + }, + trailing = { + ForwardButton { context.launchVpnSettings() } + }, + ) + ) + ) + add( + SelectionItem( + Icons.Outlined.Restore, + { + ScaledSwitch( + uiState.settings.isRestoreOnBootEnabled, + onClick = { viewModel.onToggleRestartAtBoot() }, + ) + }, + title = { + Text( + stringResource(R.string.restart_at_boot), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) + }, + onClick = { viewModel.onToggleRestartAtBoot() } + ) + ) + } ) SurfaceSelectionGroupButton( - listOf( - SelectionItem( - Icons.Filled.AppShortcut, - { - ScaledSwitch( - uiState.settings.isShortcutsEnabled, - onClick = { viewModel.onToggleShortcutsEnabled() }, - ) - }, - title = { - Text(stringResource(R.string.enabled_app_shortcuts), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) - }, - ), - SelectionItem( - Icons.Outlined.VpnLock, - { - ScaledSwitch( - enabled = !( - ( - uiState.settings.isTunnelOnWifiEnabled || - uiState.settings.isTunnelOnEthernetEnabled || - uiState.settings.isTunnelOnMobileDataEnabled - ) && - uiState.settings.isAutoTunnelEnabled - ), - onClick = { viewModel.onToggleAlwaysOnVPN() }, - checked = uiState.settings.isAlwaysOnVpnEnabled, - ) - }, - title = { - Text(stringResource(R.string.always_on_vpn_support), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) - }, - ), - SelectionItem( - Icons.Outlined.AdminPanelSettings, - title = { Text(stringResource(R.string.kill_switch), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, - onClick = { - context.launchVpnSettings() - }, - trailing = { - val icon = Icons.AutoMirrored.Outlined.ArrowForward - Icon(icon, icon.name) - } - ), - SelectionItem( - Icons.Outlined.Restore, - { - ScaledSwitch( - uiState.settings.isRestoreOnBootEnabled, - onClick = { viewModel.onToggleRestartAtBoot() }, - ) - }, - title = { Text(stringResource(R.string.restart_at_boot), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, - ), + listOf(SelectionItem( + Icons.AutoMirrored.Outlined.ViewQuilt, + title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + onClick = { + navController.navigate(Route.Appearance) + }, + trailing = { + ForwardButton { navController.navigate(Route.Appearance) } + }, ), - ) - - SurfaceSelectionGroupButton( - mutableListOf( - SelectionItem( - Icons.AutoMirrored.Outlined.ViewQuilt, - title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, - onClick = { - navController.navigate(Route.Appearance) - }, - trailing = { - val icon = Icons.AutoMirrored.Outlined.ArrowForward - Icon(icon, icon.name) - } - ), SelectionItem( Icons.Outlined.Notifications, - title = { Text(stringResource(R.string.notifications), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + title = { Text(stringResource(R.string.notifications), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, onClick = { context.launchNotificationSettings() }, trailing = { - val icon = Icons.AutoMirrored.Outlined.ArrowForward - Icon(icon, icon.name) - } + ForwardButton { context.launchNotificationSettings() } + }, ), SelectionItem( Icons.Outlined.Pin, - title = { Text(stringResource(R.string.enable_app_lock), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + title = { Text(stringResource(R.string.enable_app_lock), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, trailing = { ScaledSwitch( uiState.generalState.isPinLockEnabled, @@ -370,52 +373,61 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: } }, ) - } - ), - ), - ) - - SurfaceSelectionGroupButton( - listOf( - SelectionItem( - Icons.Outlined.Code, - title = { Text(stringResource(R.string.kernel), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, - description = { - Text( - "Use kernel backend (root only)", - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline), - ) }, - trailing = { - ScaledSwitch( - uiState.settings.isKernelEnabled, - onClick = { viewModel.onToggleKernelMode() }, - enabled = !( - uiState.settings.isAutoTunnelEnabled || - uiState.settings.isAlwaysOnVpnEnabled || - (uiState.vpnState.status == TunnelState.UP) || - !settingsUiState.isKernelAvailable - ), - ) - }, - ), - ), - ) + onClick = { if (uiState.generalState.isPinLockEnabled) { + appViewModel.onPinLockDisabled() + } else { + PinManager.initialize(context) + navController.navigate(Route.Lock) + } } + ) + )) - SurfaceSelectionGroupButton( + if(!isRunningOnTv) SurfaceSelectionGroupButton(listOf( + SelectionItem( + Icons.Outlined.Code, + title = { Text(stringResource(R.string.kernel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + description = { + Text( + stringResource(R.string.use_kernel), + style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), + ) + }, + trailing = { + ScaledSwitch( + uiState.settings.isKernelEnabled, + onClick = { viewModel.onToggleKernelMode() }, + enabled = !( + uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled || + (uiState.vpnState.status == TunnelState.UP) || + !settingsUiState.isKernelAvailable + ), + ) + }, + onClick = { + viewModel.onToggleKernelMode() + } + ), + )) + + if(!isRunningOnTv) SurfaceSelectionGroupButton( listOf( SelectionItem( Icons.Outlined.FolderZip, - title = { Text(stringResource(R.string.export_configs), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + title = { Text(stringResource(R.string.export_configs), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, onClick = { if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required) showAuthPrompt = true }, - trailing = {}, ), - ), + ) ) + + + + // Surface( // tonalElevation = 2.dp, // shadowElevation = 2.dp, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt index 992a86a..a5f25b6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt @@ -60,24 +60,6 @@ constructor( appDataRepository.appState.setBatteryOptimizationDisableShown(true) } - fun onToggleAutoTunnel(context: Context) = viewModelScope.launch { - with(settings.value) { - var isAutoTunnelPaused = this.isAutoTunnelPaused - if (isAutoTunnelEnabled) { - ServiceManager.stopWatcherService(context) - } else { - ServiceManager.startWatcherService(context) - isAutoTunnelPaused = false - } - appDataRepository.settings.save( - copy( - isAutoTunnelEnabled = !isAutoTunnelEnabled, - isAutoTunnelPaused = isAutoTunnelPaused, - ), - ) - } - } - fun onToggleAlwaysOnVPN() = viewModelScope.launch { with(settings.value) { appDataRepository.settings.save( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/AppearanceScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/AppearanceScreen.kt index 7a751e4..e25db8f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/AppearanceScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/AppearanceScreen.kt @@ -2,70 +2,71 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowForward import androidx.compose.material.icons.outlined.Contrast import androidx.compose.material.icons.outlined.Translate -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController +import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth -import com.zaneschepke.wireguardautotunnel.R @Composable fun AppearanceScreen() { val navController = LocalNavController.current - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), - modifier = - Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.systemBars) - .padding(top = 24.dp.scaledHeight()) - .padding(horizontal = 24.dp.scaledWidth()), - ) { - SurfaceSelectionGroupButton( - listOf( - SelectionItem( - Icons.Outlined.Translate, - title = { Text(stringResource(R.string.language), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, - onClick = { navController.navigate(Route.Language) }, - trailing = { - val icon = Icons.AutoMirrored.Outlined.ArrowForward - Icon(icon, icon.name) - } + Scaffold( + topBar = { + TopNavBar(stringResource(R.string.appearance)) + } + ){ + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), + modifier = + Modifier + .fillMaxSize().padding(it) + .padding(top = 24.dp.scaledHeight()) + .padding(horizontal = 24.dp.scaledWidth()), + ) { + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.Translate, + title = { Text(stringResource(R.string.language), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + onClick = { navController.navigate(Route.Language) }, + trailing = { + ForwardButton { navController.navigate(Route.Language) } + } + ), ), - ), - ) - SurfaceSelectionGroupButton( - listOf( - SelectionItem( - Icons.Outlined.Contrast, - title = { Text(stringResource(R.string.display_theme), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, - onClick = { navController.navigate(Route.Display) }, - trailing = { - val icon = Icons.AutoMirrored.Outlined.ArrowForward - Icon(icon, icon.name) - } + ) + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.Contrast, + title = { Text(stringResource(R.string.display_theme), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + onClick = { navController.navigate(Route.Display) }, + trailing = { + ForwardButton { navController.navigate(Route.Display) } + } + ), ), - ), - ) + ) + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayScreen.kt index be76f2c..d6f9c8a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/display/DisplayScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -16,6 +17,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton +import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar import com.zaneschepke.wireguardautotunnel.ui.theme.Theme import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth @@ -23,37 +25,43 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth @Composable fun DisplayScreen(appUiState: AppUiState, viewModel: DisplayViewModel = hiltViewModel()) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), - modifier = - Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.systemBars) - .padding(top = 24.dp.scaledHeight()) - .padding(horizontal = 24.dp.scaledWidth()), + Scaffold( + topBar = { + TopNavBar(stringResource(R.string.display_theme)) + } ) { - IconSurfaceButton( - title = stringResource(R.string.automatic), - onClick = { - viewModel.onThemeChange(Theme.AUTOMATIC) - }, - selected = appUiState.generalState.theme == Theme.AUTOMATIC, - ) - IconSurfaceButton( - title = stringResource(R.string.light), - onClick = { viewModel.onThemeChange(Theme.LIGHT) }, - selected = appUiState.generalState.theme == Theme.LIGHT, - ) - IconSurfaceButton( - title = stringResource(R.string.dark), - onClick = { viewModel.onThemeChange(Theme.DARK) }, - selected = appUiState.generalState.theme == Theme.DARK, - ) - IconSurfaceButton( - title = stringResource(R.string.dynamic), - onClick = { viewModel.onThemeChange(Theme.DYNAMIC) }, - selected = appUiState.generalState.theme == Theme.DYNAMIC, - ) + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), + modifier = + Modifier + .fillMaxSize() + .padding(it) + .padding(top = 24.dp.scaledHeight()) + .padding(horizontal = 24.dp.scaledWidth()), + ) { + IconSurfaceButton( + title = stringResource(R.string.automatic), + onClick = { + viewModel.onThemeChange(Theme.AUTOMATIC) + }, + selected = appUiState.generalState.theme == Theme.AUTOMATIC, + ) + IconSurfaceButton( + title = stringResource(R.string.light), + onClick = { viewModel.onThemeChange(Theme.LIGHT) }, + selected = appUiState.generalState.theme == Theme.LIGHT, + ) + IconSurfaceButton( + title = stringResource(R.string.dark), + onClick = { viewModel.onThemeChange(Theme.DARK) }, + selected = appUiState.generalState.theme == Theme.DARK, + ) + IconSurfaceButton( + title = stringResource(R.string.dynamic), + onClick = { viewModel.onThemeChange(Theme.DYNAMIC) }, + selected = appUiState.generalState.theme == Theme.DYNAMIC, + ) + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/LanguageScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/LanguageScreen.kt index 8b97f15..7015783 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/LanguageScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/appearance/language/LanguageScreen.kt @@ -1,6 +1,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBars @@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf @@ -24,6 +26,7 @@ import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.common.SelectedLabel import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController +import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.extensions.navigateAndForget import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight @@ -63,44 +66,50 @@ fun LanguageScreen(localeStorage: LocaleStorage) { navController.navigateAndForget(Route.Main) } - LazyColumn( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = - Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.systemBars) - .padding(top = 24.dp.scaledHeight()) - .padding(horizontal = 24.dp.scaledWidth()).windowInsetsPadding(WindowInsets.navigationBars), - ) { - item { - SelectionItemButton( - buttonText = stringResource(R.string.automatic), - onClick = { - onChangeLocale(LocaleUtil.OPTION_PHONE_LANGUAGE) - }, - trailing = { - if (currentLocale.value == LocaleUtil.OPTION_PHONE_LANGUAGE) { - SelectedLabel() - } - }, - ripple = false, - ) + Scaffold( + topBar = { + TopNavBar(stringResource(R.string.language)) } - items(sortedLocales, key = { it }) { locale -> - SelectionItemButton( - buttonText = locale.getDisplayLanguage(locale).capitalize(locale) + - if (locale.toLanguageTag().contains("-")) " (${locale.getDisplayCountry(locale).capitalize(locale)})" else "", - onClick = { - onChangeLocale(locale.toLanguageTag()) - }, - trailing = { - if (locale.toLanguageTag() == currentLocale.value) { - SelectedLabel() - } - }, - ripple = false, - ) + ) { + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .fillMaxSize().padding(it) + .padding(horizontal = 24.dp.scaledWidth()).windowInsetsPadding(WindowInsets.navigationBars), + ) { + item { + Box(modifier = Modifier.padding(top = 24.dp.scaledHeight())) { + SelectionItemButton( + buttonText = stringResource(R.string.automatic), + onClick = { + onChangeLocale(LocaleUtil.OPTION_PHONE_LANGUAGE) + }, + trailing = { + if (currentLocale.value == LocaleUtil.OPTION_PHONE_LANGUAGE) { + SelectedLabel() + } + }, + ripple = false, + ) + } + } + items(sortedLocales, key = { it }) { locale -> + SelectionItemButton( + buttonText = locale.getDisplayLanguage(locale).capitalize(locale) + + if (locale.toLanguageTag().contains("-")) " (${locale.getDisplayCountry(locale).capitalize(locale)})" else "", + onClick = { + onChangeLocale(locale.toLanguageTag()) + }, + trailing = { + if (locale.toLanguageTag() == currentLocale.value) { + SelectedLabel() + } + }, + ripple = false, + ) + } } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelScreen.kt index cbe9b99..ad360c3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelScreen.kt @@ -4,34 +4,21 @@ import android.Manifest import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.isImeVisible -import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Filter1 import androidx.compose.material.icons.outlined.NetworkPing import androidx.compose.material.icons.outlined.Security import androidx.compose.material.icons.outlined.SettingsEthernet import androidx.compose.material.icons.outlined.SignalCellular4Bar import androidx.compose.material.icons.outlined.Wifi import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -45,8 +32,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.permissions.ExperimentalPermissionsApi @@ -54,16 +39,15 @@ import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.ui.AppUiState -import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox -import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.util.extensions.isLocationServicesEnabled -import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth @@ -96,137 +80,190 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV } Scaffold( + contentWindowInsets = WindowInsets(0.dp), topBar = { TopNavBar(stringResource(R.string.auto_tunneling)) - }, + } ) { Column( horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), + verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), modifier = Modifier - .verticalScroll(rememberScrollState()) .fillMaxSize() - .padding(top = 24.dp.scaledHeight()).padding(it) + .padding(it) + .padding(top = 24.dp.scaledHeight()) .padding(horizontal = 24.dp.scaledWidth()), ) { SurfaceSelectionGroupButton( - mutableListOf( - SelectionItem( - Icons.Outlined.Wifi, - title = { Text(stringResource(R.string.tunnel_on_wifi), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, - description = { - }, - trailing = { - ScaledSwitch( - enabled = !uiState.settings.isAlwaysOnVpnEnabled, - checked = uiState.settings.isTunnelOnWifiEnabled, - onClick = { checked -> - if (!checked || uiState.isRooted) viewModel.onToggleTunnelOnWifi().also { return@ScaledSwitch } - onAutoTunnelWifiChecked() - }, - ) - }, - ), - SelectionItem( - Icons.Outlined.SignalCellular4Bar, - title = { - Text( - stringResource(R.string.tunnel_mobile_data), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ScaledSwitch( - enabled = !uiState.settings.isAlwaysOnVpnEnabled, - checked = uiState.settings.isTunnelOnMobileDataEnabled, - onClick = { viewModel.onToggleTunnelOnMobileData() }, - ) - }, - ), - SelectionItem( - Icons.Outlined.SettingsEthernet, - title = { - Text( - stringResource(R.string.tunnel_on_ethernet), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ScaledSwitch( - enabled = !uiState.settings.isAlwaysOnVpnEnabled, - checked = uiState.settings.isTunnelOnEthernetEnabled, - onClick = { viewModel.onToggleTunnelOnEthernet() }, - ) - }, - ), - SelectionItem( - Icons.Outlined.NetworkPing, - title = { - Text( - stringResource(R.string.restart_on_ping), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), - ) - }, - trailing = { - ScaledSwitch( - checked = uiState.settings.isPingEnabled, - onClick = { viewModel.onToggleRestartOnPing() }, - ) - }, - ), - ).apply { + buildList { + add( + SelectionItem( + Icons.Outlined.Wifi, + title = { + Text( + stringResource(R.string.tunnel_on_wifi), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface) + ) + }, + description = { + }, + trailing = { + ScaledSwitch( + enabled = !uiState.settings.isAlwaysOnVpnEnabled, + checked = uiState.settings.isTunnelOnWifiEnabled, + onClick = { + if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi() + .also { return@ScaledSwitch } + onAutoTunnelWifiChecked() + }, + ) + }, + onClick = { + if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi() + .also { return@SelectionItem } + onAutoTunnelWifiChecked() + } + ) + ) if (uiState.settings.isTunnelOnWifiEnabled) { - add(1, - SelectionItem( - title = { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()), - ) { + addAll( + listOf( + SelectionItem( + Icons.Outlined.Filter1, + title = { + Text( + stringResource(R.string.use_wildcards), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface) + ) + }, + description = { + LearnMoreLinkLabel({context.openWebUrl(it)}, stringResource(id = R.string.docs_wildcards)) + }, + trailing = { + ScaledSwitch( + checked = uiState.generalState.isWildcardsEnabled, + onClick = { + viewModel.onToggleWildcards() + }, + ) + }, + onClick = { + viewModel.onToggleWildcards() + } + ), + SelectionItem( + title = { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .weight(4f, false) - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()), ) { - val icon = Icons.Outlined.Security - Icon( - icon, - icon.name, - modifier = Modifier.size(iconSize.scaledWidth()), - ) - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), + Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp.scaledWidth()) - .padding(vertical = 6.dp.scaledHeight()), + .weight(4f, false) + .fillMaxWidth(), ) { - Text( - "Trusted wifi names", - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface), + val icon = Icons.Outlined.Security + Icon( + icon, + icon.name, + modifier = Modifier.size(iconSize), ) + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp.scaledWidth()) + .padding(vertical = 6.dp.scaledHeight()), + ) { + Text( + stringResource(R.string.trusted_wifi_names), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + } + } - } - } - }, - description = { - TrustedNetworkTextBox( - uiState.settings.trustedNetworkSSIDs, onDelete = viewModel::onDeleteTrustedSSID, - currentText = currentText, - onSave = viewModel::onSaveTrustedSSID, - onValueChange = { currentText = it } + }, + description = { + TrustedNetworkTextBox( + uiState.settings.trustedNetworkSSIDs, onDelete = viewModel::onDeleteTrustedSSID, + currentText = currentText, + onSave = viewModel::onSaveTrustedSSID, + onValueChange = { currentText = it }, + supporting = { if(uiState.generalState.isWildcardsEnabled) { + WildcardsLabel() + }} + ) + }, + ) + )) + } + } + ) + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.SignalCellular4Bar, + title = { + Text( + stringResource(R.string.tunnel_mobile_data), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), ) }, + trailing = { + ScaledSwitch( + enabled = !uiState.settings.isAlwaysOnVpnEnabled, + checked = uiState.settings.isTunnelOnMobileDataEnabled, + onClick = { viewModel.onToggleTunnelOnMobileData() }, + ) + }, + onClick = { + viewModel.onToggleTunnelOnMobileData() + } + ), + SelectionItem( + Icons.Outlined.SettingsEthernet, + title = { + Text( + stringResource(R.string.tunnel_on_ethernet), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ScaledSwitch( + enabled = !uiState.settings.isAlwaysOnVpnEnabled, + checked = uiState.settings.isTunnelOnEthernetEnabled, + onClick = { viewModel.onToggleTunnelOnEthernet() }, + ) + }, + onClick = { + viewModel.onToggleTunnelOnEthernet() + } + ), + SelectionItem( + Icons.Outlined.NetworkPing, + title = { + Text( + stringResource(R.string.restart_on_ping), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), + ) + }, + trailing = { + ScaledSwitch( + checked = uiState.settings.isPingEnabled, + onClick = { viewModel.onToggleRestartOnPing() }, + ) + }, + onClick = { + viewModel.onToggleRestartOnPing() + } ) ) - } - }, - ) + ) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt index 565fa20..1449c41 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt @@ -18,7 +18,6 @@ class AutoTunnelViewModel @Inject constructor( private val appDataRepository: AppDataRepository, - ) : ViewModel() { private val settings = appDataRepository.settings.getSettingsFlow() @@ -44,6 +43,13 @@ constructor( } } + fun onToggleWildcards() = viewModelScope.launch { + val wildcards = appDataRepository.appState.isWildcardsEnabled() + appDataRepository.appState.setWildcardsEnabled( + !wildcards + ) + } + fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch { with(settings.value) { appDataRepository.settings.save( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/TrustNetworksTextBox.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/TrustNetworksTextBox.kt index 1ac22fb..635be17 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/TrustNetworksTextBox.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/TrustNetworksTextBox.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -25,32 +26,28 @@ import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField -import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv -import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth @OptIn(ExperimentalLayoutApi::class) @Composable -fun TrustedNetworkTextBox(trustedNetworks: List, onDelete: (ssid: String) -> Unit, currentText: String, onSave : (ssid: String) -> Unit, onValueChange: (network: String) -> Unit) { +fun TrustedNetworkTextBox(trustedNetworks: List, onDelete: (ssid: String) -> Unit, currentText: String, onSave : (ssid: String) -> Unit, onValueChange: (network: String) -> Unit, supporting: @Composable () -> Unit) { val context = LocalContext.current Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())){ FlowRow( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(5.dp), + horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally), ) { trustedNetworks.forEach { ssid -> ClickableIconButton( onClick = { if (context.isRunningOnTv()) { - //focusRequester.requestFocus() onDelete(ssid) } }, onIconClick = { - //if (context.isRunningOnTv()) focusRequester.requestFocus() onDelete(ssid) }, text = ssid, @@ -59,17 +56,18 @@ fun TrustedNetworkTextBox(trustedNetworks: List, onDelete: (ssid: String } } CustomTextField( + textStyle = MaterialTheme.typography.bodySmall, value = currentText, onValueChange = onValueChange, - label = { Text(stringResource(R.string.add_trusted_ssid)) }, + label = { Text(stringResource(R.string.add_wifi_name)) }, containerColor = MaterialTheme.colorScheme.surface, + supportingText = supporting, modifier = Modifier .padding( top = 5.dp, bottom = 10.dp, ).fillMaxWidth().padding(end = 16.dp.scaledWidth()), - supportingText = { WildcardSupportingLabel { context.openWebUrl(it)} }, singleLine = true, keyboardOptions = KeyboardOptions( @@ -94,7 +92,7 @@ fun TrustedNetworkTextBox(trustedNetworks: List, onDelete: (ssid: String } } }, - ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/WildcardsLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/WildcardsLabel.kt new file mode 100644 index 0000000..56039be --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/components/WildcardsLabel.kt @@ -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), + ) +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDisclosure.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDisclosure.kt deleted file mode 100644 index 107c56c..0000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/BackgroundLocationDisclosure.kt +++ /dev/null @@ -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)) - } - } - } -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ForwardButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ForwardButton.kt new file mode 100644 index 0000000..675b5d3 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/ForwardButton.kt @@ -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)) + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/WildcardSupportingLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LearnMoreLinkLabel.kt similarity index 86% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/WildcardSupportingLabel.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LearnMoreLinkLabel.kt index 9c78a4b..453d368 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/WildcardSupportingLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/LearnMoreLinkLabel.kt @@ -12,18 +12,18 @@ import androidx.compose.ui.text.withStyle import com.zaneschepke.wireguardautotunnel.R @Composable -fun WildcardSupportingLabel(onClick: (url: String) -> Unit) { +fun LearnMoreLinkLabel(onClick: (url: String) -> Unit, url : String) { // TODO update link when docs are fully updated val gettingStarted = buildAnnotatedString { pushStringAnnotation( tag = "details", - annotation = stringResource(id = R.string.docs_wildcards), + annotation = url, ) withStyle( style = SpanStyle(color = MaterialTheme.colorScheme.primary), ) { - append(stringResource(id = R.string.wildcard_supported)) + append(stringResource(id = R.string.learn_more)) } pop() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/LocationDisclosureScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/LocationDisclosureScreen.kt new file mode 100644 index 0000000..63fd324 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/disclosure/LocationDisclosureScreen.kt @@ -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() } + } + ), + ), + ) + } + } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt index ceb885a..561d637 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt @@ -1,314 +1,131 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support -import androidx.compose.foundation.clickable -import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.ArrowForward -import androidx.compose.material.icons.rounded.Book -import androidx.compose.material.icons.rounded.FormatListNumbered -import androidx.compose.material.icons.rounded.Mail -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon +import androidx.compose.material.icons.filled.Book +import androidx.compose.material.icons.filled.LineStyle +import androidx.compose.material.icons.filled.Mail +import androidx.compose.material.icons.filled.Policy import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.zaneschepke.wireguardautotunnel.BuildConfig import com.zaneschepke.wireguardautotunnel.R -import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.Route +import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem +import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton +import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel +import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController -import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton +import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth @Composable -fun SupportScreen(focusRequester: FocusRequester, appUiState: AppUiState) { +fun SupportScreen() { val context = LocalContext.current val navController = LocalNavController.current - val fillMaxWidth = .85f - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = - Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.systemBars) - .verticalScroll(rememberScrollState()) - .focusable(), - ) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), modifier = - ( - if (context.isRunningOnTv()) { - Modifier - .height(IntrinsicSize.Min) - .fillMaxWidth(fillMaxWidth) - .padding(top = 10.dp) - } else { - Modifier - .fillMaxWidth(fillMaxWidth) - .padding(top = 20.dp) - } - ) - .padding(bottom = 25.dp), + Modifier + .fillMaxSize() + .padding(top = topPadding) + .padding(horizontal = 24.dp.scaledWidth()), ) { - Column(modifier = Modifier.padding(20.dp)) { - val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward - Text( - stringResource(R.string.thank_you), - textAlign = TextAlign.Start, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(bottom = 20.dp), - fontSize = 16.sp, - ) - Text( - stringResource(id = R.string.support_help_text), - textAlign = TextAlign.Start, - fontSize = 16.sp, - modifier = Modifier.padding(bottom = 20.dp), - ) - TextButton( - onClick = { - context.openWebUrl( - context.resources.getString(R.string.docs_url), - ) - }, - modifier = - Modifier - .padding(vertical = 5.dp) - .focusRequester(focusRequester), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Row { - val icon = Icons.Rounded.Book - Icon(icon, icon.name) - Text( - stringResource(id = R.string.docs_description), - textAlign = TextAlign.Justify, - modifier = - Modifier - .padding(start = 10.dp) - .weight( - weight = 1.0f, - fill = false, - ), - softWrap = true, - ) + GroupLabel(stringResource(R.string.thank_you)) + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Filled.Book, + title = { Text(stringResource(R.string.docs_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + trailing = { + ForwardButton { context.openWebUrl(context.getString(R.string.docs_url)) } + }, + onClick = { + context.openWebUrl(context.getString(R.string.docs_url)) } - Icon( - forwardIcon, - forwardIcon.name, - ) - } - } - HorizontalDivider( - thickness = 0.5.dp, - color = MaterialTheme.colorScheme.onBackground, - ) - TextButton( - onClick = { - context.openWebUrl( - context.resources.getString(R.string.telegram_url), - ) - }, - modifier = Modifier.padding(vertical = 5.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Row { - val icon = ImageVector.vectorResource(R.drawable.telegram) - Icon( - icon, - icon.name, - Modifier.size(25.dp), - ) - Text( - stringResource(id = R.string.chat_description), - textAlign = TextAlign.Justify, - modifier = Modifier.padding(start = 10.dp), - ) - } - Icon( - forwardIcon, - forwardIcon.name, - ) - } - } - HorizontalDivider( - thickness = 0.5.dp, - color = MaterialTheme.colorScheme.onBackground, - ) - TextButton( - onClick = { - context.openWebUrl( - context.resources.getString(R.string.github_url), - ) - }, - modifier = Modifier.padding(vertical = 5.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Row { - val icon = ImageVector.vectorResource(R.drawable.github) - Icon( - imageVector = icon, - icon.name, - Modifier.size(25.dp), - ) - Text( - stringResource(id = R.string.open_issue), - textAlign = TextAlign.Justify, - modifier = Modifier.padding(start = 10.dp), - ) - } - Icon( - forwardIcon, - forwardIcon.name, - ) - } - } - HorizontalDivider( - thickness = 0.5.dp, - color = MaterialTheme.colorScheme.onBackground, - ) - TextButton( - onClick = { context.launchSupportEmail() }, - modifier = Modifier.padding(vertical = 5.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Row { - val icon = Icons.Rounded.Mail - Icon(icon, icon.name) - Text( - stringResource(id = R.string.email_description), - textAlign = TextAlign.Justify, - modifier = Modifier.padding(start = 10.dp), - ) - } - Icon( - forwardIcon, - forwardIcon.name, - ) - } - } - if (!context.isRunningOnTv()) { - HorizontalDivider( - thickness = 0.5.dp, - color = MaterialTheme.colorScheme.onBackground, - ) - TextButton( - onClick = { navController.navigate(Route.Logs) }, - modifier = Modifier.padding(vertical = 5.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Row { - val icon = Icons.Rounded.FormatListNumbered - Icon(icon, icon.name) - Text( - stringResource(id = R.string.read_logs), - textAlign = TextAlign.Justify, - modifier = Modifier.padding(start = 10.dp), - ) + ), + SelectionItem( + Icons.Filled.LineStyle, + title = { Text(stringResource(R.string.read_logs), + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + trailing = { + ForwardButton { + navController.navigate(Route.Logs) } - Icon( - Icons.AutoMirrored.Rounded.ArrowForward, - stringResource(id = R.string.go), - ) + }, + onClick = { + navController.navigate(Route.Logs) } - } - } - } - } - Spacer(modifier = Modifier.weight(1f)) - Text( - stringResource(id = R.string.privacy_policy), - style = TextStyle(textDecoration = TextDecoration.Underline), - fontSize = 16.sp, - modifier = - Modifier.clickable { - context.openWebUrl( - context.resources.getString(R.string.privacy_policy_url), + ), + SelectionItem( + Icons.Filled.Policy, + title = { Text(stringResource(R.string.privacy_policy), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + trailing = { + ForwardButton { context.openWebUrl(context.getString(R.string.privacy_policy_url)) } + }, + onClick = { + context.openWebUrl(context.getString(R.string.privacy_policy_url)) + } + ), + + ) + ) + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + ImageVector.vectorResource(R.drawable.telegram), + title = { Text(stringResource(R.string.chat_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + trailing = { + ForwardButton { + context.openWebUrl(context.getString(R.string.telegram_url)) + } + }, + onClick = { + context.openWebUrl(context.getString(R.string.telegram_url)) + } + ), + SelectionItem( + ImageVector.vectorResource(R.drawable.github), + title = { Text(stringResource(R.string.open_issue), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + trailing = { + ForwardButton { + context.openWebUrl(context.getString(R.string.github_url)) + } + }, + onClick = { + context.openWebUrl(context.getString(R.string.github_url)) + } + ), + SelectionItem( + Icons.Filled.Mail, + title = { Text(stringResource(R.string.email_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, + trailing = { + ForwardButton { + context.launchSupportEmail() + } + }, + onClick = { + context.launchSupportEmail() + } + ), ) - }, - ) - Row( - horizontalArrangement = Arrangement.spacedBy(25.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(25.dp), - ) { - val version = - buildAnnotatedString { - append(stringResource(id = R.string.version)) - append(": ") - append(BuildConfig.VERSION_NAME) - } - val mode = - buildAnnotatedString { - append(stringResource(R.string.mode)) - append(": ") - when (appUiState.settings.isKernelEnabled) { - true -> append(stringResource(id = R.string.kernel)) - false -> append(stringResource(id = R.string.userspace)) - } - } - Text(version.text, modifier = Modifier.focusable()) - Text(mode.text) + ) + VersionLabel() } } -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt index 63ac3b4..1caf5e2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt @@ -3,14 +3,16 @@ package com.zaneschepke.wireguardautotunnel.ui.theme import androidx.compose.ui.graphics.Color val OffWhite = Color(0xFFE5E1E5) -val LightGrey = Color(0xFF8D9D9F) +val LightGrey = Color(0xFFCAC4D0) val Aqua = Color(0xFF76BEBD) val SilverTree = Color(0xFF6DB58B) val Plantation = Color(0xFF264A49) val Shark = Color(0xFF21272A) val BalticSea = Color(0xFF1C1B1F) val Brick = Color(0xFFCE4257) -val Corn = Color(0xFFFBEC5D) +val Straw = Color(0xFFD4C483) + + sealed class ThemeColors( val background: Color, @@ -19,7 +21,7 @@ sealed class ThemeColors( val secondary: Color, val onSurface: Color, ) { - // TODO fix light theme colors + data object Light : ThemeColors( background = LightGrey, surface = OffWhite, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Size.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Size.kt index 21276f5..4900c22 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Size.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Size.kt @@ -4,3 +4,4 @@ import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight val iconSize = 24.dp.scaledHeight() +val topPadding = 80.dp.scaledHeight() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt index 8c4eb3c..bb4eeef 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt @@ -49,12 +49,18 @@ fun WireguardAutoTunnelTheme( content: @Composable () -> Unit, ) { val context = LocalContext.current - val isDark = isSystemInDarkTheme() + var isDark = isSystemInDarkTheme() val autoTheme = if(isDark) DarkColorScheme else LightColorScheme val colorScheme = when(theme) { Theme.AUTOMATIC -> autoTheme - Theme.DARK -> DarkColorScheme - Theme.LIGHT -> LightColorScheme + Theme.DARK -> { + isDark = true + DarkColorScheme + } + Theme.LIGHT -> { + isDark = false + LightColorScheme + } Theme.DYNAMIC -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (isDark) { @@ -72,7 +78,7 @@ fun WireguardAutoTunnelTheme( WindowCompat.setDecorFitsSystemWindows(window, false) window.statusBarColor = Color.Transparent.toArgb() window.navigationBarColor = Color.Transparent.toArgb() - WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = isDark + WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = !isDark } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt index 5bc690d..ad880ad 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt @@ -18,6 +18,7 @@ val inter = FontFamily( val Typography = Typography( bodyLarge = TextStyle( + fontFamily = inter, fontWeight = FontWeight.Normal, fontSize = 16.sp.scaled(), lineHeight = 24.sp.scaled(), @@ -26,12 +27,13 @@ val Typography = bodySmall = TextStyle( fontFamily = inter, fontWeight = FontWeight.Normal, - fontSize = 13.sp.scaled(), + fontSize = 12.sp.scaled(), lineHeight = 20.sp.scaled(), letterSpacing = 1.sp, color = LightGrey, ), bodyMedium = TextStyle( + fontFamily = inter, fontSize = 14.sp.scaled(), lineHeight = 20.sp.scaled(), fontWeight = FontWeight(400), @@ -54,7 +56,7 @@ val Typography = titleMedium = TextStyle( fontFamily = inter, fontWeight = FontWeight.Bold, - fontSize = 17.sp.scaled(), + fontSize = 16.sp.scaled(), lineHeight = 21.sp.scaled(), letterSpacing = 0.sp, ), diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt index 056213c..18a3d2f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt @@ -66,6 +66,7 @@ fun Context.resizeWidth(dp: Dp): Dp { } fun Context.launchNotificationSettings() { + if(isRunningOnTv()) return launchAppSettings() val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(Settings.EXTRA_APP_PACKAGE, packageName) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt index 8764bc4..c39f15c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt @@ -5,7 +5,7 @@ import com.wireguard.android.util.RootShell import com.wireguard.config.Peer import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics -import com.zaneschepke.wireguardautotunnel.ui.theme.Corn +import com.zaneschepke.wireguardautotunnel.ui.theme.Straw import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.NumberUtils @@ -58,7 +58,7 @@ fun TunnelStatistics?.asColor(): Color { ?.let { statuses -> when { statuses.all { it == HandshakeStatus.HEALTHY } -> SilverTree - statuses.any { it == HandshakeStatus.STALE } -> Corn + statuses.any { it == HandshakeStatus.STALE } -> Straw statuses.all { it == HandshakeStatus.NOT_STARTED } -> Color.Gray else -> Color.Gray } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt index afba1f3..6abc1dc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/UiExtensions.kt @@ -3,8 +3,10 @@ package com.zaneschepke.wireguardautotunnel.util.extensions import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.ui.Route +import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute fun NavController.navigateAndForget(route: Route) { navigate(route) { @@ -12,6 +14,22 @@ fun NavController.navigateAndForget(route: Route) { } } +fun NavController.goFromRoot(route: Route) { + if (currentBackStackEntry?.isCurrentRoute(route::class) == true) return + this.navigate(route) { + // Pop up to the start destination of the graph to + // avoid building up a large stack of destinations + // on the back stack as users select items + popUpTo(graph.findStartDestination().id) { + saveState = true + } + // Avoid multiple copies of the same destination when + // reselecting the same item + launchSingleTop = true + restoreState = true + } +} + fun Dp.scaledHeight(): Dp { return WireGuardAutoTunnel.instance.resizeHeight(this) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8cc001b..48ebb9d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,7 +23,7 @@ Start auto-tunneling Stop auto-tunneling Tunnel on mobile data - View Privacy Policy + View privacy policy Okay Tunnel on ethernet 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. @@ -193,7 +193,6 @@ Ping interval (sec) "optional, default: " Ping restart cooldown (sec) - Learn about supported wildcards. details Show Amnezia properties never @@ -211,4 +210,14 @@ Language Display theme Selected + Trusted wifi names + Add wifi name + On demand tunnel rules + Primary tunnel + Mobile data tunnel + Skip + Launch app settings + Use name wildcards + Learn more + Wildcards active