fix: more ui fixes, move ping targets

This commit is contained in:
Zane Schepke
2025-10-14 02:56:48 -04:00
parent 5519f1da8b
commit b0da2982d7
24 changed files with 236 additions and 169 deletions
+1 -1
View File
@@ -73,7 +73,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustNothing"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.WireguardAutoTunnel"
android:configChanges="orientation|screenSize|keyboardHidden"
>
@@ -71,6 +71,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsSc
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.TunnelGlobalsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.ping.PingTargetScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.AndroidIntegrationsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.DonateScreen
@@ -449,6 +450,7 @@ class MainActivity : AppCompatActivity() {
entry<Route.PreferredTunnel> { key ->
PreferredTunnelScreen(key.tunnelNetwork)
}
entry<Route.PingTarget> { PingTargetScreen() }
},
)
}
@@ -102,7 +102,6 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
return ret
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -180,7 +180,6 @@ class TunnelControlTile : TileService(), LifecycleOwner {
return ret
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun SectionDivider() {
HorizontalDivider(
color = MaterialTheme.colorScheme.outline.copy(0.30f),
modifier = Modifier.padding(horizontal = 12.dp),
)
}
@@ -76,8 +76,7 @@ fun SurfaceRow(
) {
Row(
modifier =
Modifier.weight(1f)
.focusRequester(mainFocusRequester)
Modifier.focusRequester(mainFocusRequester)
.focusProperties { right = trailingFocusRequester }
.run {
if (onClick != null) {
@@ -108,7 +107,7 @@ fun SurfaceRow(
}
}
Column(
modifier = Modifier.padding(end = 16.dp),
modifier = Modifier.padding(end = 16.dp).weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
@@ -122,7 +121,6 @@ fun SurfaceRow(
description()
}
}
Spacer(Modifier.weight(1f))
if (trailing != null) {
trailing(
Modifier.focusRequester(trailingFocusRequester).focusProperties {
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.common
package com.zaneschepke.wireguardautotunnel.ui.common.security
import android.view.WindowManager
import androidx.compose.runtime.Composable
@@ -3,15 +3,12 @@ package com.zaneschepke.wireguardautotunnel.ui.common.textbox
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@@ -46,8 +43,7 @@ fun ConfigurationTextBox(
isError = isError,
textStyle =
MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface),
modifier =
modifier.fillMaxWidth().height(48.dp).focusRequester(focusRequester).imePadding(),
modifier = modifier.fillMaxWidth().height(48.dp).focusRequester(focusRequester),
value = value,
visualTransformation = visualTransformation,
singleLine = singleLine,
@@ -10,7 +10,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
@@ -41,6 +46,11 @@ fun CustomTextField(
interactionSource: MutableInteractionSource = MutableInteractionSource(),
) {
val space = " "
var isFocused by remember { mutableStateOf(false) }
val cursorBrush =
if (isFocused) SolidColor(MaterialTheme.colorScheme.primary)
else SolidColor(Color.Transparent)
BasicTextField(
value = value,
textStyle = textStyle,
@@ -48,8 +58,8 @@ fun CustomTextField(
keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions,
readOnly = readOnly,
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface),
modifier = modifier,
cursorBrush = cursorBrush,
modifier = modifier.onFocusChanged { focusState -> isFocused = focusState.isFocused },
interactionSource = interactionSource,
enabled = enabled,
singleLine = singleLine,
@@ -79,7 +89,7 @@ fun CustomTextField(
focusedContainerColor = containerColor,
unfocusedContainerColor = containerColor,
focusedTextColor = MaterialTheme.colorScheme.onSurface,
cursorColor = MaterialTheme.colorScheme.onSurface,
cursorColor = MaterialTheme.colorScheme.primary,
),
enabled = enabled,
label = label,
@@ -97,12 +107,13 @@ fun CustomTextField(
errorContainerColor = containerColor,
disabledLabelColor = MaterialTheme.colorScheme.onSurface,
disabledContainerColor = containerColor,
focusedIndicatorColor = MaterialTheme.colorScheme.onSurface,
focusedIndicatorColor = MaterialTheme.colorScheme.primary,
unfocusedIndicatorColor = MaterialTheme.colorScheme.outline,
focusedLabelColor = MaterialTheme.colorScheme.onSurface,
focusedContainerColor = containerColor,
unfocusedContainerColor = containerColor,
focusedTextColor = MaterialTheme.colorScheme.onSurface,
cursorColor = MaterialTheme.colorScheme.onSurface,
cursorColor = MaterialTheme.colorScheme.primary,
),
shape = RoundedCornerShape(8.dp),
focusedBorderThickness = 0.5.dp,
@@ -73,6 +73,8 @@ sealed class Route : NavKey {
@Keep @Serializable data object Addresses : Route()
@Keep @Serializable data class PreferredTunnel(val tunnelNetwork: TunnelNetwork) : Route()
@Keep @Serializable data object PingTarget : Route()
}
@Serializable
@@ -126,6 +128,7 @@ enum class Tab(
Route.Appearance,
Route.Language,
Route.Display,
Route.PingTarget,
Route.Logs -> SETTINGS
is Route.Support,
Route.License,
@@ -542,6 +542,19 @@ fun currentRouteAsNavbarState(
showBottomItems = true,
)
}
PingTarget ->
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.ping_target),
showBottomItems = true,
)
null -> NavbarState()
}
}
@@ -45,7 +45,7 @@ fun TrustedNetworkTextBox(
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = modifier.animateContentSize().imePadding(),
modifier = modifier.animateContentSize(),
) {
FlowRow(
modifier = Modifier.fillMaxWidth().animateContentSize(),
@@ -2,18 +2,11 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferred
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
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.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@@ -25,12 +18,7 @@ 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.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@@ -182,17 +170,10 @@ fun PreferredTunnelScreen(
}
if (selectedTunnel?.id == tunnel.id) {
val focusRequester = remember { FocusRequester() }
val bringIntoViewRequester = remember {
BringIntoViewRequester()
}
val keyboardController = LocalSoftwareKeyboardController.current
var currentText by remember { mutableStateOf("") }
LaunchedEffect(tunnel.tunnelNetworks.size) {
bringIntoViewRequester.bringIntoView()
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
keyboardController?.show()
@@ -219,10 +200,7 @@ fun PreferredTunnelScreen(
{ WildcardsLabel() }
} else null,
modifier =
Modifier.fillMaxWidth()
.focusRequester(focusRequester)
.bringIntoViewRequester(bringIntoViewRequester)
.imePadding(),
Modifier.fillMaxWidth().focusRequester(focusRequester),
singleLine = true,
keyboardOptions =
KeyboardOptions(
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Adjust
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.QueryStats
import androidx.compose.material.icons.outlined.Replay
@@ -110,6 +111,11 @@ fun TunnelMonitoringScreen(viewModel: SettingsViewModel = hiltViewModel()) {
},
onClick = { viewModel.setDetailedPingStats(!settingsState.showDetailedPingStats) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Adjust, contentDescription = null) },
title = stringResource(R.string.set_custom_ping_target),
onClick = { navController.push(Route.PingTarget) },
)
}
Column {
GroupLabel(
@@ -0,0 +1,163 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.ping
import android.util.Patterns
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxWidth
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.foundation.rememberOverscrollEffect
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Save
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.LaunchedEffect
import androidx.compose.runtime.getValue
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.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
@Composable
fun PingTargetScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val settingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (settingsState.isLoading) return
var selectedTunnel by remember { mutableStateOf<TunnelConf?>(null) }
var isError by remember { mutableStateOf<Boolean>(false) }
LazyColumn(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.overscroll(rememberOverscrollEffect()),
state = rememberLazyListState(),
userScrollEnabled = true,
reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(),
) {
item {
DescriptionText(
stringResource(R.string.ping_target_description),
modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 16.dp),
)
GroupLabel(
stringResource(R.string.tunnels),
modifier = Modifier.padding(horizontal = 16.dp),
)
}
items(settingsState.tunnels, key = { it.id }) { tunnel ->
SurfaceRow(
title = tunnel.tunName,
onClick = { selectedTunnel = tunnel },
description = {
DescriptionText(
stringResource(
R.string.ping_target_template,
tunnel.pingTarget ?: stringResource(R.string._default),
)
)
},
expandedContent = {
if (selectedTunnel?.id == tunnel.id) {
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
var currentText by remember { mutableStateOf(tunnel.pingTarget ?: "") }
var isError by remember { mutableStateOf(false) }
LaunchedEffect(currentText) { isError = false }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
keyboardController?.show()
}
fun onClick() {
val isValid =
currentText.isBlank() ||
currentText.isValidIpv4orIpv6Address() ||
Patterns.DOMAIN_NAME.matcher(currentText).matches()
if (!isValid) {
isError = true
return
}
viewModel.setPingTarget(tunnel, currentText)
selectedTunnel = null
}
CustomTextField(
isError = isError,
textStyle =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onSurface
),
value = currentText,
onValueChange = { currentText = it },
interactionSource = remember { MutableInteractionSource() },
label = {
Text(
stringResource(R.string.set_ping_target),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelMedium,
)
},
containerColor = MaterialTheme.colorScheme.surface,
modifier =
Modifier.fillMaxWidth()
.focusRequester(focusRequester)
.padding(top = 8.dp),
singleLine = true,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { onClick() }),
trailing = {
if (currentText != "") {
IconButton(onClick = { onClick() }) {
Icon(
imageVector = Icons.Outlined.Save,
contentDescription = stringResource(R.string.save),
tint = MaterialTheme.colorScheme.primary,
)
}
}
},
)
}
},
)
}
}
}
@@ -24,10 +24,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.viewmodel.ProxySettingsViewModel
@@ -29,11 +29,11 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
@@ -14,7 +14,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.AddPeerButton
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.InterfaceSection
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.PeersSection
@@ -1,23 +1,16 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions
import android.util.Patterns
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.automirrored.outlined.CallSplit
import androidx.compose.material.icons.outlined.Dns
import androidx.compose.material.icons.outlined.Save
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.Text
import androidx.compose.runtime.*
@@ -25,12 +18,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
@@ -40,11 +29,9 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components.*
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
import org.orbitmvi.orbit.compose.collectSideEffect
@@ -108,7 +95,7 @@ fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
}
Column {
GroupLabel(
stringResource(R.string.reliability),
stringResource(R.string.other),
modifier = Modifier.padding(horizontal = 16.dp),
)
SurfaceRow(
@@ -138,83 +125,6 @@ fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
},
onClick = { viewModel.toggleIpv4Preferred() },
)
SurfaceRow(
title = "",
description = {
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
var stateValue by remember { mutableStateOf(tunnel.pingTarget ?: "") }
val isError by
remember(stateValue) {
derivedStateOf {
stateValue.isNotBlank() &&
!stateValue.isValidIpv4orIpv6Address() &&
!Patterns.DOMAIN_NAME.matcher(stateValue).matches()
}
}
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
CustomTextField(
isError = isError,
textStyle =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onSurface
),
value = stateValue,
onValueChange = { stateValue = it },
interactionSource = remember { MutableInteractionSource() },
label = {
Text(
stringResource(R.string.set_custom_ping_target),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelMedium,
)
},
placeholder = {
Text(
stringResource(R.string.ip_or_hostname),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
},
containerColor = MaterialTheme.colorScheme.surface,
supportingText = {
Text(stringResource(R.string.ping_target_description))
},
modifier =
Modifier.padding(top = 5.dp, bottom = 10.dp)
.fillMaxWidth()
.padding(end = 16.dp),
singleLine = true,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions =
KeyboardActions(onDone = { viewModel.setTunnelPingIp(stateValue) }),
trailing = {
if (!isError) {
IconButton(
onClick = {
viewModel.setTunnelPingIp(stateValue)
keyboardController?.hide()
focusManager.clearFocus()
}
) {
val icon = Icons.Outlined.Save
Icon(
imageVector = icon,
contentDescription = icon.name,
tint = MaterialTheme.colorScheme.primary,
)
}
}
},
)
}
},
)
}
}
}
@@ -12,4 +12,5 @@ data class SettingUiState(
val showDetailedPingStats: Boolean = false,
val isLoading: Boolean = true,
val globalTunnelConf: TunnelConf? = null,
val tunnels: List<TunnelConf> = emptyList(),
)
@@ -10,10 +10,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
private val DarkColorScheme =
@@ -94,16 +94,13 @@ fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composabl
val view = LocalView.current
if (!view.isInEditMode) {
@Suppress("DEPRECATION")
SideEffect {
val window = (view.context as Activity).window
WindowCompat.setDecorFitsSystemWindows(window, false)
window.navigationBarColor = Color.Transparent.toArgb()
window.statusBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = !isDark
isAppearanceLightNavigationBars = !isDark
}
// For API 33+, use WindowInsetsControllerCompat for appearance control
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
insetsController.isAppearanceLightStatusBars = !isDark
insetsController.isAppearanceLightNavigationBars = !isDark
}
}
@@ -39,8 +39,9 @@ constructor(
settingsRepository.flow,
appStateRepository.flow,
tunnelsRepository.globalTunnelFlow,
) { settings, appState, tunnel ->
SettingUiState(
tunnelsRepository.userTunnelsFlow,
) { settings, appState, tunnel, tunnels ->
state.copy(
settings = settings,
isLocalLoggingEnabled = appState.isLocalLogsEnabled,
remoteKey = appState.remoteKey,
@@ -49,6 +50,7 @@ constructor(
showDetailedPingStats = appState.showDetailedPingStats,
isLoading = false,
globalTunnelConf = tunnel,
tunnels = tunnels,
)
}
.collect { reduce { it } }
@@ -113,6 +115,10 @@ constructor(
appStateRepository.setIsRemoteControlEnabled(to)
}
fun setPingTarget(tunnel: TunnelConf, target: String?) = intent {
tunnelsRepository.save(tunnel.copy(pingTarget = target?.ifBlank { null }))
}
fun setDnsProvider(dnsProvider: DnsProvider) = intent {
settingsRepository.save(
state.settings.copy(dnsEndpoint = dnsProvider.asAddress(state.settings.dnsProtocol))
+4 -5
View File
@@ -272,11 +272,9 @@
<string name="pinger_bounce_recovery">Pinger restart failed. Tunnel recovered with old addresses.</string>
<string name="bounce_failed_template">Pinger failed to recovery tunnel with error: %1$s</string>
<string name="tunnel_monitoring">Tunnel monitoring</string>
<string name="set_custom_ping_target">Custom ping target (optional)</string>
<string name="set_custom_ping_target">Custom ping targets</string>
<string name="ip_or_hostname">IP or hostname</string>
<string name="ping_target_description">Used if no internal IP is available (e.g., in full-tunnel configs). Leaving
blank defaults to Cloudflare.
</string>
<string name="ping_target_description">Defaults to an internal tunnel IP for split configurations or Cloudflare for full tunnel configurations.</string>
<string name="tunnel_ping_interval">Tunnel ping interval</string>
<string name="attempts_per_interval">Ping attempts per interval</string>
<string name="ping_timeout">Tunnel ping timeout</string>
@@ -411,5 +409,6 @@
<string name="ping">Ping</string>
<string name="ping_monitor">Ping monitor</string>
<string name="backup_and_restore">Backup and restore</string>
<string name="reliability">Reliability</string>
<string name="ping_target">Ping target</string>
<string name="set_ping_target">Set ping target</string>
</resources>
+2
View File
@@ -8,6 +8,8 @@
<item name="android:windowAllowEnterTransitionOverlap">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>
<style name="Theme.App.Start" parent="@style/Theme.SplashScreen">