From c18b3b7ba01e36e3ea89c5e243f537b5f3496aca Mon Sep 17 00:00:00 2001 From: zaneschepke Date: Tue, 26 May 2026 02:36:13 -0400 Subject: [PATCH] refactor: adjust action ordering, make config view selectable Other minor UI improvements. closes #1242 --- .../ui/common/dropdown/DropdownSelector.kt | 4 +- .../currentBackStackEntryAsNavbarState.kt | 204 +++++++++++------- .../tunnels/settings/config/ConfigScreen.kt | 20 +- .../edit/components/InterfaceDropdown.kt | 7 +- .../config/edit/components/PeersSection.kt | 7 +- .../components/SelectTunnelModal.kt | 52 +++-- .../ui/sideeffect/LocalSideEffect.kt | 2 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 185 insertions(+), 112 deletions(-) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/DropdownSelector.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/DropdownSelector.kt index 172805fe..d84897a1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/DropdownSelector.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/DropdownSelector.kt @@ -48,7 +48,9 @@ fun DropdownSelector( DropdownMenu( modifier = modifier.heightIn(max = 250.dp), scrollState = rememberScrollState(), - containerColor = MaterialTheme.colorScheme.surface, + containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), + tonalElevation = 4.dp, + shadowElevation = 4.dp, expanded = isExpanded, onDismissRequest = onDismiss, ) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt index 6f57b9ac..2d9ef93f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt @@ -4,8 +4,12 @@ import android.os.Build import androidx.compose.foundation.layout.Row import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.Sort +import androidx.compose.material.icons.filled.QrCode +import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.ContentPasteGo import androidx.compose.material.icons.outlined.CopyAll +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.RemoveRedEye import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Delete @@ -13,19 +17,26 @@ import androidx.compose.material.icons.rounded.Download import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Menu import androidx.compose.material.icons.rounded.NetworkCheck -import androidx.compose.material.icons.rounded.QrCode2 import androidx.compose.material.icons.rounded.Save import androidx.compose.material.icons.rounded.SelectAll import androidx.compose.material.icons.rounded.SortByAlpha +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.ui.navigation.NavController import com.zaneschepke.wireguardautotunnel.ui.navigation.Route @@ -79,13 +90,14 @@ fun currentRouteAsNavbarState( return remember(route, globalState) { derivedStateOf { when (route) { - Appearance -> + Appearance -> { NavbarState( topLeading = { TvBackButton { navController.pop() } }, showBottomItems = true, topTitle = context.getString(R.string.appearance), ) - AutoTunnel -> + } + AutoTunnel -> { NavbarState( showBottomItems = true, topTitle = @@ -94,13 +106,15 @@ fun currentRouteAsNavbarState( context.getString(R.string.auto_tunnel) }, ) - Display -> + } + Display -> { NavbarState( topLeading = { TvBackButton { navController.pop() } }, showBottomItems = true, topTitle = context.getString(R.string.display_theme), ) - Dns -> + } + Dns -> { NavbarState( topLeading = { TvBackButton { navController.pop() } }, showBottomItems = true, @@ -116,13 +130,15 @@ fun currentRouteAsNavbarState( } }, ) - Language -> + } + Language -> { NavbarState( topLeading = { TvBackButton { navController.pop() } }, showBottomItems = true, topTitle = context.getString(R.string.language), ) - LockdownSettings -> + } + LockdownSettings -> { NavbarState( topLeading = { TvBackButton { navController.pop() } }, showBottomItems = true, @@ -138,15 +154,21 @@ fun currentRouteAsNavbarState( } }, ) - License -> + } + License -> { NavbarState( topLeading = { TvBackButton { navController.pop() } }, showBottomItems = true, topTitle = context.getString(R.string.licenses), ) - LocationDisclosure -> NavbarState(showBottomItems = true) - Lock -> NavbarState(showBottomItems = false) - Logs -> + } + LocationDisclosure -> { + NavbarState(showBottomItems = true) + } + Lock -> { + NavbarState(showBottomItems = false) + } + Logs -> { NavbarState( topLeading = { TvBackButton { navController.pop() } }, showBottomItems = false, @@ -163,7 +185,8 @@ fun currentRouteAsNavbarState( } }, ) - ProxySettings -> + } + ProxySettings -> { NavbarState( topLeading = { TvBackButton { navController.pop() } }, showBottomItems = true, @@ -179,18 +202,27 @@ fun currentRouteAsNavbarState( } }, ) - Settings -> + } + Settings -> { NavbarState( showBottomItems = true, topTitle = context.getString(R.string.settings), ) - Sort -> + } + Sort -> { NavbarState( topLeading = { TvBackButton { navController.pop() } }, showBottomItems = true, topTitle = context.getString(R.string.sort), topTrailing = { Row { + IconButton( + onClick = { + sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges) + } + ) { + Icon(Icons.Rounded.Save, stringResource(R.string.save)) + } IconButton( onClick = { sharedViewModel.postSideEffect( @@ -210,16 +242,10 @@ fun currentRouteAsNavbarState( ) { Icon(Icons.Rounded.SortByAlpha, stringResource(R.string.sort)) } - IconButton( - onClick = { - sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges) - } - ) { - Icon(Icons.Rounded.Save, stringResource(R.string.save)) - } } }, ) + } is ConfigEdit, is ConfigGlobal -> { val global = route !is ConfigEdit @@ -231,6 +257,14 @@ fun currentRouteAsNavbarState( showBottomItems = true, topTitle = tunnelName ?: context.getString(R.string.new_tunnel), topTrailing = { + IconButton( + onClick = { + keyboardController?.hide() + sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges) + } + ) { + Icon(Icons.Rounded.Save, stringResource(R.string.save)) + } if (!global) IconButton( onClick = { @@ -257,14 +291,6 @@ fun currentRouteAsNavbarState( stringResource(R.string.copy_from), ) } - IconButton( - onClick = { - keyboardController?.hide() - sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges) - } - ) { - Icon(Icons.Rounded.Save, stringResource(R.string.save)) - } }, ) } @@ -278,6 +304,13 @@ fun currentRouteAsNavbarState( topTitle = tunnelName ?: "", topTrailing = { Row { + IconButton( + onClick = { + sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges) + } + ) { + Icon(Icons.Rounded.Save, stringResource(R.string.save)) + } IconButton( onClick = { sharedViewModel.postSideEffect( @@ -290,29 +323,24 @@ fun currentRouteAsNavbarState( stringResource(R.string.copy_from), ) } - IconButton( - onClick = { - sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges) - } - ) { - Icon(Icons.Rounded.Save, stringResource(R.string.save)) - } } }, showBottomItems = true, ) } - Support -> + Support -> { NavbarState( topTitle = context.getString(R.string.support), showBottomItems = true, ) - AndroidIntegrations -> + } + AndroidIntegrations -> { NavbarState( topLeading = { TvBackButton { navController.pop() } }, topTitle = context.getString(R.string.android_integrations), showBottomItems = true, ) + } is TunnelSettings -> { val tunnelName = globalState.tunnelNames[route.id] NavbarState( @@ -328,12 +356,6 @@ fun currentRouteAsNavbarState( when (globalState.selectedTunnelCount) { 0 -> Row { - IconButton(onClick = { navController.push(Sort) }) { - Icon( - Icons.AutoMirrored.Rounded.Sort, - stringResource(R.string.sort), - ) - } IconButton( onClick = { sharedViewModel.postSideEffect( @@ -346,6 +368,12 @@ fun currentRouteAsNavbarState( stringResource(R.string.add_tunnel), ) } + IconButton(onClick = { navController.push(Sort) }) { + Icon( + Icons.AutoMirrored.Rounded.Sort, + stringResource(R.string.sort), + ) + } } else -> Row { @@ -411,12 +439,13 @@ fun currentRouteAsNavbarState( showBottomItems = true, ) } - WifiDetectionMethod -> + WifiDetectionMethod -> { NavbarState( topLeading = { TvBackButton { navController.pop() } }, topTitle = context.getString(R.string.wifi_detection_method), showBottomItems = true, ) + } Donate -> { NavbarState( topLeading = { TvBackButton { navController.pop() } }, @@ -456,38 +485,65 @@ fun currentRouteAsNavbarState( NavbarState( topLeading = { TvBackButton { navController.pop() } }, topTrailing = { - Row { - IconButton( - onClick = { - sharedViewModel.postSideEffect( - LocalSideEffect.ShowSensitive - ) - } - ) { + var showOverflowMenu by remember { mutableStateOf(false) } + if (!route.live) { + IconButton(onClick = { navController.push(ConfigEdit(route.id)) }) { Icon( - Icons.Outlined.RemoveRedEye, - stringResource(R.string.toggle_sensitive_data_visibility), + Icons.Outlined.Edit, + contentDescription = stringResource(R.string.edit_tunnel), ) } - if (!route.live) { - IconButton( + } + IconButton( + onClick = { + sharedViewModel.postSideEffect(LocalSideEffect.ShowSensitive) + } + ) { + Icon( + Icons.Outlined.RemoveRedEye, + stringResource(R.string.toggle_sensitive_data_visibility), + ) + } + + if (!route.live) { + IconButton(onClick = { showOverflowMenu = true }) { + Icon( + Icons.Outlined.MoreVert, + contentDescription = stringResource(R.string.more_options), + ) + } + + DropdownMenu( + expanded = showOverflowMenu, + onDismissRequest = { showOverflowMenu = false }, + containerColor = + MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), + tonalElevation = 4.dp, + shadowElevation = 4.dp, + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.show_qr)) }, + leadingIcon = { Icon(Icons.Default.QrCode, null) }, onClick = { + showOverflowMenu = false sharedViewModel.postSideEffect(LocalSideEffect.Modal.QR) - } - ) { - Icon( - Icons.Rounded.QrCode2, - stringResource(R.string.show_qr), - ) - } - IconButton( - onClick = { navController.push(ConfigEdit(route.id)) } - ) { - Icon( - Icons.Rounded.Edit, - stringResource(R.string.edit_tunnel), - ) - } + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.copy)) }, + leadingIcon = { + Icon( + Icons.Outlined.ContentCopy, + contentDescription = null, + ) + }, + onClick = { + showOverflowMenu = false + sharedViewModel.postSideEffect( + LocalSideEffect.CopyToClipboard + ) + }, + ) } } }, @@ -523,7 +579,9 @@ fun currentRouteAsNavbarState( showBottomItems = true, ) } - null -> NavbarState() + null -> { + NavbarState() + } } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/config/ConfigScreen.kt index ba410474..b8513191 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/config/ConfigScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -21,10 +22,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.components.QrCodeDialog import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigHeaderColor @@ -45,6 +48,7 @@ fun ConfigScreen( ) { val context = LocalContext.current + val clipboard = rememberClipboardHelper() val uiState by viewModel.collectAsState() var showKeys by rememberSaveable { mutableStateOf(false) } @@ -74,6 +78,7 @@ fun ConfigScreen( } } is LocalSideEffect.ShowSensitive -> showKeys = !showKeys + is LocalSideEffect.CopyToClipboard -> clipboard.copy(rawConfig) else -> Unit } } @@ -89,16 +94,17 @@ fun ConfigScreen( ) { val displayText by remember(rawConfig, showKeys) { derivedStateOf { maskSensitive(rawConfig, showKeys) } } - val annotated by remember(displayText) { derivedStateOf { buildConfigAnnotatedString(displayText) } } - Text( - text = annotated, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(horizontal = 16.dp), - ) + SelectionContainer { + Text( + text = annotated, + style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/config/edit/components/InterfaceDropdown.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/config/edit/components/InterfaceDropdown.kt index bfc886ce..08138afa 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/config/edit/components/InterfaceDropdown.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/config/edit/components/InterfaceDropdown.kt @@ -1,6 +1,5 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.edit.components -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.MoreVert @@ -11,8 +10,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.R @@ -38,7 +35,9 @@ fun InterfaceDropdown( DropdownMenu( expanded = uiState.ui.isInterfaceDropdownExpanded, onDismissRequest = { onToggleDropdown(false) }, - modifier = Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface), + containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), + tonalElevation = 4.dp, + shadowElevation = 4.dp, ) { if (!uiState.isGlobalConfig) DropdownMenuItem( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/config/edit/components/PeersSection.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/config/edit/components/PeersSection.kt index 36171670..6a366de7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/config/edit/components/PeersSection.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/config/edit/components/PeersSection.kt @@ -1,6 +1,5 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.edit.components -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -18,7 +17,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.R @@ -66,8 +64,9 @@ fun PeersSection( onDismissRequest = { onPeerDropdownExpanded(!uiState.ui.isPeerDropdownExpanded) }, - modifier = - Modifier.shadow(12.dp).background(MaterialTheme.colorScheme.surface), + containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), + tonalElevation = 4.dp, + shadowElevation = 4.dp, ) { DropdownMenuItem( text = { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/splittunnel/components/SelectTunnelModal.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/splittunnel/components/SelectTunnelModal.kt index c84c497c..b3694d8c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/splittunnel/components/SelectTunnelModal.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/splittunnel/components/SelectTunnelModal.kt @@ -2,6 +2,9 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.compo import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -15,6 +18,7 @@ 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.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog @@ -34,29 +38,31 @@ fun SelectTunnelModal( InfoDialog( title = stringResource(R.string.copy_from), body = { - LazyColumn( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.overscroll(rememberOverscrollEffect()), - state = rememberLazyListState(), - userScrollEnabled = true, - flingBehavior = ScrollableDefaults.flingBehavior(), - ) { - items(tunnels, key = { it.id }) { tunnel -> - SurfaceRow( - title = tunnel.name, - trailing = - if (selectedTunnelId == tunnel.id) { - { - Icon( - Icons.Outlined.Check, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) - } - } else null, - onClick = { onSelect(tunnel.id) }, - ) + Box(modifier = Modifier.fillMaxWidth().heightIn(max = 480.dp)) { + LazyColumn( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.overscroll(rememberOverscrollEffect()), + state = rememberLazyListState(), + userScrollEnabled = true, + flingBehavior = ScrollableDefaults.flingBehavior(), + ) { + items(tunnels, key = { it.id }) { tunnel -> + SurfaceRow( + title = tunnel.name, + trailing = + if (selectedTunnelId == tunnel.id) { + { + Icon( + Icons.Outlined.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } + } else null, + onClick = { onSelect(tunnel.id) }, + ) + } } } }, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/sideeffect/LocalSideEffect.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/sideeffect/LocalSideEffect.kt index 83b29cc1..31c2bc76 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/sideeffect/LocalSideEffect.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/sideeffect/LocalSideEffect.kt @@ -16,6 +16,8 @@ sealed class LocalSideEffect { data object ShowSensitive : LocalSideEffect() + data object CopyToClipboard : LocalSideEffect() + sealed class Sheet : LocalSideEffect() { data object ImportTunnels : Sheet() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e6ed717..023af939 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -566,4 +566,5 @@ Battery Saver (10s) Pre/Post script support Tunnel name cannot be empty + More options