refactor: adjust action ordering, make config view selectable

Other minor UI improvements.

closes #1242
This commit is contained in:
zaneschepke
2026-05-26 02:36:13 -04:00
parent 5f03b190dd
commit c18b3b7ba0
8 changed files with 185 additions and 112 deletions
@@ -48,7 +48,9 @@ fun <T> 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,
) {
@@ -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,12 +485,18 @@ fun currentRouteAsNavbarState(
NavbarState(
topLeading = { TvBackButton { navController.pop() } },
topTrailing = {
Row {
var showOverflowMenu by remember { mutableStateOf(false) }
if (!route.live) {
IconButton(onClick = { navController.push(ConfigEdit(route.id)) }) {
Icon(
Icons.Outlined.Edit,
contentDescription = stringResource(R.string.edit_tunnel),
)
}
}
IconButton(
onClick = {
sharedViewModel.postSideEffect(
LocalSideEffect.ShowSensitive
)
sharedViewModel.postSideEffect(LocalSideEffect.ShowSensitive)
}
) {
Icon(
@@ -469,25 +504,46 @@ fun currentRouteAsNavbarState(
stringResource(R.string.toggle_sensitive_data_visibility),
)
}
if (!route.live) {
IconButton(
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)) }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.copy)) },
leadingIcon = {
Icon(
Icons.Rounded.Edit,
stringResource(R.string.edit_tunnel),
Icons.Outlined.ContentCopy,
contentDescription = null,
)
},
onClick = {
showOverflowMenu = false
sharedViewModel.postSideEffect(
LocalSideEffect.CopyToClipboard
)
},
)
}
}
}
},
@@ -523,7 +579,9 @@ fun currentRouteAsNavbarState(
showBottomItems = true,
)
}
null -> NavbarState()
null -> {
NavbarState()
}
}
}
}
@@ -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,18 +94,19 @@ fun ConfigScreen(
) {
val displayText by
remember(rawConfig, showKeys) { derivedStateOf { maskSensitive(rawConfig, showKeys) } }
val annotated by
remember(displayText) { derivedStateOf { buildConfigAnnotatedString(displayText) } }
SelectionContainer {
Text(
text = annotated,
style = MaterialTheme.typography.bodySmall,
style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
}
}
fun buildConfigAnnotatedString(text: String): AnnotatedString {
val headerRegex = "\\[(Interface|Peer)]".toRegex()
@@ -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(
@@ -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 = {
@@ -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,6 +38,7 @@ fun SelectTunnelModal(
InfoDialog(
title = stringResource(R.string.copy_from),
body = {
Box(modifier = Modifier.fillMaxWidth().heightIn(max = 480.dp)) {
LazyColumn(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
@@ -59,6 +64,7 @@ fun SelectTunnelModal(
)
}
}
}
},
onAttest = { onAttest(selectedTunnelId) },
onDismiss = onDismiss,
@@ -16,6 +16,8 @@ sealed class LocalSideEffect {
data object ShowSensitive : LocalSideEffect()
data object CopyToClipboard : LocalSideEffect()
sealed class Sheet : LocalSideEffect() {
data object ImportTunnels : Sheet()
+1
View File
@@ -566,4 +566,5 @@
<string name="balance_saver">Battery Saver (10s)</string>
<string name="tunnel_scripting">Pre/Post script support</string>
<string name="name_error_empty">Tunnel name cannot be empty</string>
<string name="more_options">More options</string>
</resources>