refactor: ui improvements

This commit is contained in:
Zane Schepke
2025-10-11 20:43:43 -04:00
parent 3c1e6b5862
commit 71461f4d0b
120 changed files with 2623 additions and 3467 deletions
@@ -61,6 +61,8 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScree
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AutoTunnelAdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.detection.WifiDetectionMethodScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferences.PreferredTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferences.wifi.WifiSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen
@@ -70,13 +72,12 @@ 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.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.SystemFeaturesScreen
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
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.AddressesScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.TunnelAutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
@@ -149,13 +150,15 @@ class MainActivity : AppCompatActivity() {
val startingStack = buildList {
add(Route.Tunnels)
if (intent?.action == Intent.ACTION_APPLICATION_PREFERENCES) add(Route.Settings)
if (appState.pinLockEnabled) add(Route.Lock)
}
val backStack = rememberNavBackStack(*startingStack.toTypedArray())
var previousRoute by remember { mutableStateOf<Route?>(null) }
val navController =
rememberNavController<NavKey>(backStack) { previousKey ->
rememberNavController<NavKey>(backStack, appState.isLocationDisclosureShown) {
previousKey ->
previousRoute = previousKey as? Route
}
@@ -376,18 +379,6 @@ class MainActivity : AppCompatActivity() {
)
SplitTunnelScreen(viewModel)
}
entry<Route.TunnelAutoTunnel> { key ->
val viewModel =
hiltViewModel<
TunnelViewModel,
TunnelViewModel.Factory,
>(
creationCallback = { factory ->
factory.create(key.id)
}
)
TunnelAutoTunnelScreen(viewModel)
}
entry<Route.Config> { key ->
val viewModel =
hiltViewModel<
@@ -404,6 +395,9 @@ class MainActivity : AppCompatActivity() {
LocationDisclosureScreen()
}
entry<Route.AutoTunnel> { AutoTunnelScreen() }
entry<Route.WifiPreferences> {
WifiSettingsScreen()
}
entry<Route.AdvancedAutoTunnel> {
AutoTunnelAdvancedScreen()
}
@@ -414,8 +408,8 @@ class MainActivity : AppCompatActivity() {
entry<Route.TunnelMonitoring> {
TunnelMonitoringScreen()
}
entry<Route.SystemFeatures> {
SystemFeaturesScreen()
entry<Route.AndroidIntegrations> {
AndroidIntegrationsScreen()
}
entry<Route.Dns> { DnsSettingsScreen() }
entry<Route.TunnelGlobals> { key ->
@@ -454,6 +448,9 @@ class MainActivity : AppCompatActivity() {
entry<Route.License> { LicenseScreen() }
entry<Route.Donate> { DonateScreen() }
entry<Route.Addresses> { AddressesScreen() }
entry<Route.PreferredTunnel> { key ->
PreferredTunnelScreen(key.tunnelNetwork)
}
},
)
}
@@ -1,21 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@Composable
fun ActionIconButton(icon: ImageVector, labelRes: Int, onClick: () -> Unit) {
IconButton(onClick = onClick) {
Icon(
icon,
contentDescription = stringResource(labelRes),
modifier = Modifier.size(iconSize),
)
}
}
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.*
@@ -11,7 +10,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
@Composable
fun ClickableIconButton(
onClick: () -> Unit,
onIconClick: () -> Unit,
text: String,
icon: ImageVector,
enabled: Boolean = true,
@@ -27,12 +25,7 @@ fun ClickableIconButton(
Icon(
imageVector = icon,
contentDescription = icon.name,
modifier =
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
if (enabled) {
onIconClick()
}
},
modifier = Modifier.size(ButtonDefaults.IconSize).weight(1f, false),
)
}
}
@@ -1,19 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
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))
}
}
@@ -12,8 +12,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
@Composable
fun IconSurfaceButton(
@@ -59,7 +57,11 @@ fun IconSurfaceButton(
) {
leading?.invoke()
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
SelectionItemLabel(title, SelectionLabelType.TITLE)
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
description?.let {
Text(
description,
@@ -1,19 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Launch
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 LaunchButton(modifier: Modifier = Modifier.focusable(), onClick: () -> Unit) {
IconButton(modifier = modifier, onClick = onClick) {
val icon = Icons.AutoMirrored.Outlined.Launch
Icon(icon, icon.name, Modifier.size(iconSize))
}
}
@@ -1,19 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
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 LinkIconButton(modifier: Modifier = Modifier.focusable(), onClick: () -> Unit) {
IconButton(modifier = modifier, onClick = onClick) {
val icon = Icons.AutoMirrored.Outlined.OpenInNew
Icon(icon, icon.name, Modifier.size(iconSize))
}
}
@@ -5,8 +5,6 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.unit.dp
@Composable
fun ScaledSwitch(
@@ -18,7 +16,7 @@ fun ScaledSwitch(
Switch(
checked,
{ onClick(it) },
modifier.scale((52.dp / 52.dp)),
modifier = modifier,
enabled = enabled,
colors =
SwitchDefaults.colors()
@@ -1,73 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
@Composable
fun SelectionItemButton(
buttonText: String,
description: String? = null,
onClick: () -> Unit,
modifier: Modifier = Modifier,
leading: (@Composable () -> Unit)? = null,
trailing: (@Composable () -> Unit)? = null,
ripple: Boolean = true,
) {
Card(
modifier =
modifier
.clip(RoundedCornerShape(8.dp))
.clickable(
indication = if (ripple) ripple() else null,
interactionSource = remember { MutableInteractionSource() },
onClick = { onClick() },
)
.height(IntrinsicSize.Min),
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
leading?.let { it() }
Column(
horizontalAlignment = Alignment.Start,
modifier = Modifier.weight(1f).padding(end = 12.dp),
verticalArrangement = Arrangement.Center,
) {
SelectionItemLabel(
buttonText,
SelectionLabelType.TITLE,
modifier = Modifier.weight(1f).padding(end = 24.dp),
)
description?.let {
SelectionItemLabel(
it,
SelectionLabelType.DESCRIPTION,
modifier = Modifier.weight(1f).padding(end = 24.dp),
)
}
}
}
trailing?.let { it() }
}
}
}
@@ -0,0 +1,167 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import android.R.attr.description
import android.R.attr.enabled
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.focusable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
@Composable
fun SurfaceRow(
title: AnnotatedString,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
description: @Composable (() -> Unit)? = null,
expandedContent: @Composable (() -> Unit)? = null,
onLongClick: () -> Unit = {},
enabled: Boolean = true,
selected: Boolean = false,
leading: @Composable (() -> Unit)? = null,
trailing: @Composable ((Modifier) -> Unit)? = null,
) {
val density = LocalDensity.current
var leadingPadding by remember { mutableStateOf(0.dp) }
val interactionSource = remember { MutableInteractionSource() }
val mainFocusRequester = remember { FocusRequester() }
val trailingFocusRequester = remember { FocusRequester() }
Column(
modifier =
modifier
.fillMaxWidth()
.focusGroup()
.indication(interactionSource, ripple())
.background(
if (!selected) MaterialTheme.colorScheme.surface
else MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
.padding(horizontal = 16.dp, vertical = 8.dp)
.defaultMinSize(minHeight = 42.dp)
.animateContentSize(),
verticalArrangement = Arrangement.Center,
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
Row(
modifier =
Modifier.weight(1f)
.focusRequester(mainFocusRequester)
.focusProperties { right = trailingFocusRequester }
.run {
if (onClick != null) {
combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
enabled = enabled,
interactionSource = interactionSource,
indication = null,
)
} else {
this
}
}
.focusable(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
if (leading != null) {
Row(
modifier =
Modifier.onSizeChanged {
leadingPadding = with(density) { it.width.toDp() }
}
) {
leading()
Spacer(modifier = Modifier.width(16.dp))
}
}
Column(
modifier = Modifier.padding(end = 16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
color =
if (enabled) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
)
if (description != null) {
description()
}
}
}
if (trailing != null) {
trailing(
Modifier.focusRequester(trailingFocusRequester).focusProperties {
left = mainFocusRequester
}
)
}
}
if (expandedContent != null) {
Row(modifier = Modifier.fillMaxWidth().padding(start = leadingPadding, top = 4.dp)) {
expandedContent()
}
}
}
}
@Composable
fun SurfaceRow(
title: String,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
description: @Composable (() -> Unit)? = null,
expandedContent: @Composable (() -> Unit)? = null,
onLongClick: () -> Unit = {},
enabled: Boolean = true,
selected: Boolean = false,
leading: @Composable (() -> Unit)? = null,
trailing: @Composable ((Modifier) -> Unit)? = null,
) {
SurfaceRow(
title = AnnotatedString(title),
onClick = onClick,
description = description,
expandedContent = expandedContent,
onLongClick = onLongClick,
enabled = enabled,
selected = selected,
leading = leading,
trailing = trailing,
modifier = modifier,
)
}
@@ -0,0 +1,44 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
@Composable
fun SwitchWithDivider(
checked: Boolean,
onClick: (Boolean) -> Unit,
enabled: Boolean = true,
modifier: Modifier = Modifier,
) {
Row(
modifier = Modifier.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
VerticalDivider(
modifier = Modifier.fillMaxHeight().padding(horizontal = 8.dp, vertical = 8.dp),
color = MaterialTheme.colorScheme.outline,
)
Box(modifier = Modifier.pointerInput(Unit) { detectTapGestures {} }) {
ScaledSwitch(
checked = checked,
onClick = onClick,
enabled = enabled,
modifier = modifier,
)
}
}
}
@@ -1,20 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
data class SelectionItem(
val leading: (@Composable () -> Unit)? = null,
val trailing: (@Composable () -> Unit)? = null,
val title: (@Composable () -> Unit),
val description: (@Composable () -> Unit)? = null,
val onClick: (() -> Unit)? = null,
val isEnabled: Boolean = true,
val disabledReason: String? = null,
val modifier: Modifier = Modifier.height(48.dp),
val padding: Dp = if (description == null && disabledReason == null) 16.dp else 6.dp,
val onLongPress: (() -> Unit)? = null,
)
@@ -1,28 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun SelectionItemLabel(text: String, labelType: SelectionLabelType, modifier: Modifier = Modifier) {
val style =
when (labelType) {
SelectionLabelType.DESCRIPTION ->
MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline)
SelectionLabelType.TITLE ->
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface
)
}
Text(text = text, style = style, modifier = modifier)
}
enum class SelectionLabelType {
DESCRIPTION,
TITLE,
}
@@ -1,117 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
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.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SurfaceSelectionGroupButton(items: List<SelectionItem>, modifier: Modifier = Modifier) {
if (items.isEmpty()) return
val context = LocalContext.current
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.fillMaxWidth(),
) {
items.forEach { item ->
Box(
contentAlignment = Alignment.Center,
modifier =
Modifier.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.alpha(if (item.isEnabled) 1f else 0.6f)
.semantics {
if (!item.isEnabled) {
stateDescription =
item.disabledReason ?: context.getString(R.string.disabled)
}
}
.combinedClickable(
onClick = { item.onClick?.invoke() },
onLongClick = { item.onLongPress?.invoke() },
enabled = true,
),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.weight(4f, false)
.fillMaxWidth()
.padding(vertical = item.padding),
) {
item.leading?.let {
Box(modifier = Modifier.alpha(if (item.isEnabled) 1f else 0.6f)) {
it()
}
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement =
Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier =
Modifier.fillMaxWidth()
.padding(start = if (item.leading != null) 16.dp else 0.dp)
.weight(1f),
) {
Box(modifier = Modifier.alpha(if (item.isEnabled) 1f else 0.6f)) {
item.title()
}
item.description?.invoke()
if (!item.isEnabled && item.disabledReason != null) {
Text(
text = item.disabledReason,
style = MaterialTheme.typography.bodySmall,
color =
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
)
}
}
}
item.trailing?.let {
Box(
contentAlignment = Alignment.CenterEnd,
modifier =
Modifier.padding(start = 16.dp)
.alpha(if (item.isEnabled) 1f else 0.6f)
.run {
if (!item.isEnabled) {
semantics {
stateDescription =
context.getString(R.string.disabled)
}
} else {
this
}
},
) {
it()
}
}
}
}
}
}
}
}
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.common.dropdown
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.rememberScrollState
@@ -27,30 +28,35 @@ fun <T> DropdownSelector(
it?.toString() ?: stringResource(R.string._default)
},
) {
Row(
horizontalArrangement = Arrangement.spacedBy(5.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (label != null) label()
Text(text = optionToString(currentValue), style = MaterialTheme.typography.bodyMedium)
Icon(Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.dropdown))
}
DropdownMenu(
modifier = modifier.heightIn(max = 250.dp),
scrollState = rememberScrollState(),
containerColor = MaterialTheme.colorScheme.surface,
expanded = isExpanded,
onDismissRequest = onDismiss,
) {
options.forEach { option ->
DropdownMenuItem(
text = { Text(optionToString(option)) },
onClick = {
onValueSelected(option)
onDismiss()
},
Box(modifier = modifier) {
Row(
horizontalArrangement = Arrangement.spacedBy(5.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (label != null) label()
Text(text = optionToString(currentValue), style = MaterialTheme.typography.bodyMedium)
Icon(
Icons.Default.ArrowDropDown,
contentDescription = stringResource(R.string.dropdown),
)
}
DropdownMenu(
modifier = modifier.heightIn(max = 250.dp),
scrollState = rememberScrollState(),
containerColor = MaterialTheme.colorScheme.surface,
expanded = isExpanded,
onDismissRequest = onDismiss,
) {
options.forEach { option ->
DropdownMenuItem(
text = { Text(optionToString(option)) },
onClick = {
onValueSelected(option)
onDismiss()
},
)
}
}
}
}
@@ -1,12 +1,11 @@
package com.zaneschepke.wireguardautotunnel.ui.common.dropdown
import androidx.compose.runtime.*
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
@Composable
fun <T> LabelledDropdown(
title: @Composable () -> Unit,
title: String,
description: (@Composable () -> Unit)? = null,
leading: @Composable () -> Unit,
onSelected: (T?) -> Unit,
@@ -16,24 +15,20 @@ fun <T> LabelledDropdown(
) {
var isDropDownExpanded by remember { mutableStateOf(false) }
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
leading = leading,
title = title,
description = description,
onClick = { isDropDownExpanded = true },
trailing = {
DropdownSelector(
currentValue = currentValue,
options = options,
onValueSelected = { selected -> onSelected(selected) },
isExpanded = isDropDownExpanded,
onDismiss = { isDropDownExpanded = false },
optionToString = optionToString,
)
},
SurfaceRow(
leading = leading,
title = title,
description = description,
onClick = { isDropDownExpanded = true },
trailing = {
DropdownSelector(
currentValue = currentValue,
options = options,
onValueSelected = { selected -> onSelected(selected) },
isExpanded = isDropDownExpanded,
onDismiss = { isDropDownExpanded = false },
optionToString = optionToString,
)
)
},
)
}
@@ -7,10 +7,15 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
@Composable
fun GroupLabel(title: String, modifier: Modifier = Modifier) {
fun GroupLabel(
title: String,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colorScheme.primary,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
@@ -20,7 +25,7 @@ fun GroupLabel(title: String, modifier: Modifier = Modifier) {
title,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground,
color = color,
)
}
}
@@ -0,0 +1,15 @@
package com.zaneschepke.wireguardautotunnel.ui.common.text
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun DescriptionText(text: String, modifier: Modifier = Modifier) {
Text(
text = text,
style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline),
modifier = modifier,
)
}
@@ -3,12 +3,18 @@ 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
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.VisualTransformation
@@ -34,11 +40,14 @@ fun ConfigurationTextBox(
readOnly: Boolean = false,
singleLine: Boolean = true,
) {
val focusRequester = remember { FocusRequester() }
CustomTextField(
isError = isError,
textStyle =
MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface),
modifier = modifier.fillMaxWidth().height(48.dp),
modifier =
modifier.fillMaxWidth().height(48.dp).focusRequester(focusRequester).imePadding(),
value = value,
visualTransformation = visualTransformation,
singleLine = singleLine,
@@ -5,6 +5,7 @@ import androidx.navigation3.runtime.NavKey
class NavController(
private val backStack: NavBackStack<NavKey>,
private val isDisclosureShown: Boolean,
private val onChange: (previous: NavKey?) -> Unit = {},
) {
fun push(route: NavKey) {
@@ -27,6 +28,9 @@ class NavController(
backStack.removeLast()
}
if (backStack.last() != route) {
val route =
if (route is Route.AutoTunnel && !isDisclosureShown) Route.LocationDisclosure
else route
backStack.add(route)
}
}
@@ -46,15 +46,13 @@ sealed class Route : NavKey {
@Keep @Serializable data class SplitTunnelGlobal(val id: Int) : Route()
@Keep @Serializable data class TunnelAutoTunnel(val id: Int) : Route()
@Keep @Serializable data object Sort : Route()
@Keep @Serializable data object Settings : Route()
@Keep @Serializable data object TunnelMonitoring : Route()
@Keep @Serializable data object SystemFeatures : Route()
@Keep @Serializable data object AndroidIntegrations : Route()
@Keep @Serializable data object Dns : Route()
@@ -66,11 +64,22 @@ sealed class Route : NavKey {
@Keep @Serializable data object WifiDetectionMethod : Route()
@Keep @Serializable data object WifiPreferences : Route()
@Keep @Serializable data object LocationDisclosure : Route()
@Keep @Serializable data object Donate : Route()
@Keep @Serializable data object Addresses : Route()
@Keep @Serializable data class PreferredTunnel(val tunnelNetwork: TunnelNetwork) : Route()
}
@Serializable
enum class TunnelNetwork {
MOBILE_DATA,
ETHERNET,
WIFI,
}
enum class Tab(
@@ -98,15 +107,17 @@ enum class Tab(
Route.Sort,
is Route.TunnelOptions,
is Route.Config,
is Route.SplitTunnel,
is Route.TunnelAutoTunnel -> TUNNELS
is Route.Lock,
is Route.SplitTunnel -> TUNNELS
is Route.AutoTunnel,
Route.AdvancedAutoTunnel,
Route.WifiDetectionMethod,
Route.WifiPreferences,
is Route.PreferredTunnel,
Route.LocationDisclosure -> AUTOTUNNEL
is Route.Settings,
Route.TunnelMonitoring,
Route.SystemFeatures,
Route.AndroidIntegrations,
Route.Dns,
is Route.TunnelGlobals,
is Route.ConfigGlobal,
@@ -6,14 +6,18 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.Sort
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.*
import androidx.compose.runtime.derivedStateOf
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ActionIconButton
import com.zaneschepke.wireguardautotunnel.ui.navigation.NavController
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route.*
import com.zaneschepke.wireguardautotunnel.ui.navigation.TunnelNetwork
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.ui.state.NavbarState
import com.zaneschepke.wireguardautotunnel.ui.state.SharedAppUiState
@@ -32,27 +36,33 @@ fun currentRouteAsNavbarState(
return remember(route, sharedState) {
derivedStateOf {
when (route) {
Route.AdvancedAutoTunnel ->
AdvancedAutoTunnel ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.advanced_settings),
)
Route.Appearance ->
Appearance ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.appearance),
)
Route.AutoTunnel ->
AutoTunnel ->
NavbarState(
showBottomItems = true,
topTitle =
@@ -61,295 +71,392 @@ fun currentRouteAsNavbarState(
context.getString(R.string.auto_tunnel)
},
)
Route.Display ->
Display ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.display_theme),
)
Route.Dns ->
Dns ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.dns_settings),
)
Route.Language ->
Language ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.language),
)
Route.License ->
License ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.licenses),
)
Route.LocationDisclosure -> NavbarState(showBottomItems = true)
Route.Lock -> NavbarState(showBottomItems = false)
Route.Logs ->
LocationDisclosure -> NavbarState(showBottomItems = true)
Lock -> NavbarState(showBottomItems = false)
Logs ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.logs),
topTrailing = {
ActionIconButton(Icons.Rounded.Menu, R.string.quick_actions) {
sharedViewModel.postSideEffect(LocalSideEffect.Sheet.LoggerActions)
IconButton(
onClick = {
sharedViewModel.postSideEffect(
LocalSideEffect.Sheet.LoggerActions
)
}
) {
Icon(Icons.Rounded.Menu, stringResource(R.string.quick_actions))
}
},
)
Route.ProxySettings ->
ProxySettings ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.proxy_settings),
topTrailing = {
ActionIconButton(Icons.Rounded.Save, R.string.save) {
keyboardController?.hide()
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
IconButton(
onClick = {
keyboardController?.hide()
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
)
Route.Settings ->
Settings ->
NavbarState(
showBottomItems = true,
topTitle = context.getString(R.string.settings),
topTrailing = {
ActionIconButton(
Icons.Rounded.SettingsBackupRestore,
R.string.quick_actions,
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.Sheet.BackupApp)
}
) {
sharedViewModel.postSideEffect(LocalSideEffect.Sheet.BackupApp)
Icon(
Icons.Rounded.SettingsBackupRestore,
stringResource(R.string.quick_actions),
)
}
},
)
Route.Sort ->
Sort ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.sort),
topTrailing = {
Row {
ActionIconButton(Icons.Rounded.SortByAlpha, R.string.sort) {
sharedViewModel.postSideEffect(LocalSideEffect.Sort)
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.Sort)
}
) {
Icon(Icons.Rounded.SortByAlpha, stringResource(R.string.sort))
}
ActionIconButton(Icons.Rounded.Save, R.string.save) {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
}
},
)
is Route.Config -> {
is Config -> {
val tunnelName = sharedState.tunnelNames[route.id]
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = tunnelName ?: context.getString(R.string.new_tunnel),
topTrailing = {
ActionIconButton(Icons.Rounded.Save, R.string.save) {
keyboardController?.hide()
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
IconButton(
onClick = {
keyboardController?.hide()
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
)
}
is Route.SplitTunnel -> {
is SplitTunnel -> {
val tunnelName = sharedState.tunnelNames[route.id]
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = tunnelName ?: "",
topTrailing = {
ActionIconButton(Icons.Rounded.Save, R.string.save) {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
showBottomItems = true,
)
}
is Route.SplitTunnelGlobal -> {
is SplitTunnelGlobal -> {
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.splt_tunneling),
topTrailing = {
ActionIconButton(Icons.Rounded.Save, R.string.save) {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
showBottomItems = true,
)
}
is Route.ConfigGlobal -> {
is ConfigGlobal -> {
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.configuration),
topTrailing = {
ActionIconButton(Icons.Rounded.Save, R.string.save) {
keyboardController?.hide()
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
)
}
Route.Support ->
Support ->
NavbarState(
topTitle = context.getString(R.string.support),
showBottomItems = true,
)
Route.SystemFeatures ->
AndroidIntegrations ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.android_integrations),
showBottomItems = true,
)
is Route.TunnelAutoTunnel -> {
val tunnelName = sharedState.tunnelNames[route.id]
TunnelMonitoring ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
}
},
showBottomItems = true,
topTitle = tunnelName ?: "",
)
}
Route.TunnelMonitoring ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.tunnel_monitoring),
showBottomItems = true,
)
is Route.TunnelOptions -> {
is TunnelOptions -> {
val tunnelName = sharedState.tunnelNames[route.id]
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = tunnelName ?: "",
topTrailing = {
Row {
ActionIconButton(Icons.Rounded.QrCode2, R.string.show_qr) {
sharedViewModel.postSideEffect(LocalSideEffect.Modal.QR)
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.Modal.QR)
}
) {
Icon(Icons.Rounded.QrCode2, stringResource(R.string.show_qr))
}
ActionIconButton(Icons.Rounded.Edit, R.string.edit_tunnel) {
navController.push(Route.Config(route.id))
IconButton(onClick = { navController.push(Config(route.id)) }) {
Icon(Icons.Rounded.Edit, stringResource(R.string.edit_tunnel))
}
}
},
)
}
Route.Tunnels -> {
Tunnels -> {
NavbarState(
topTitle = context.getString(R.string.tunnels),
topTrailing = {
when (sharedState.selectedTunnelCount) {
0 ->
Row {
ActionIconButton(
Icons.AutoMirrored.Rounded.Sort,
R.string.sort,
) {
navController.push(Route.Sort)
IconButton(onClick = { navController.push(Sort) }) {
Icon(
Icons.AutoMirrored.Rounded.Sort,
stringResource(R.string.sort),
)
}
ActionIconButton(Icons.Rounded.Add, R.string.add_tunnel) {
sharedViewModel.postSideEffect(
LocalSideEffect.Sheet.ImportTunnels
IconButton(
onClick = {
sharedViewModel.postSideEffect(
LocalSideEffect.Sheet.ImportTunnels
)
}
) {
Icon(
Icons.Rounded.Add,
stringResource(R.string.add_tunnel),
)
}
}
else ->
Row {
ActionIconButton(
Icons.Rounded.SelectAll,
R.string.select_all,
IconButton(
onClick = {
sharedViewModel.postSideEffect(
LocalSideEffect.SelectedTunnels.SelectAll
)
}
) {
sharedViewModel.postSideEffect(
LocalSideEffect.SelectedTunnels.SelectAll
Icon(
Icons.Rounded.SelectAll,
stringResource(R.string.select_all),
)
}
// due to permissions, and SAF issues on TV, not support
// less than Android
// 10
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ActionIconButton(
Icons.Rounded.Download,
R.string.download,
IconButton(
onClick = {
sharedViewModel.postSideEffect(
LocalSideEffect.Sheet.ExportTunnels
)
}
) {
sharedViewModel.postSideEffect(
LocalSideEffect.Sheet.ExportTunnels
Icon(
Icons.Rounded.Download,
stringResource(R.string.download),
)
}
}
if (sharedState.selectedTunnelCount == 1) {
ActionIconButton(Icons.Rounded.CopyAll, R.string.copy) {
sharedViewModel.postSideEffect(
LocalSideEffect.SelectedTunnels.Copy
IconButton(
onClick = {
sharedViewModel.postSideEffect(
LocalSideEffect.SelectedTunnels.Copy
)
}
) {
Icon(
Icons.Rounded.CopyAll,
stringResource(R.string.copy),
)
}
}
ActionIconButton(
Icons.Rounded.Delete,
R.string.delete_tunnel,
IconButton(
onClick = {
sharedViewModel.postSideEffect(
LocalSideEffect.Modal.DeleteTunnels
)
}
) {
sharedViewModel.postSideEffect(
LocalSideEffect.Modal.DeleteTunnels
Icon(
Icons.Rounded.Delete,
stringResource(R.string.delete_tunnel),
)
}
}
@@ -358,46 +465,92 @@ fun currentRouteAsNavbarState(
showBottomItems = true,
)
}
Route.WifiDetectionMethod ->
WifiDetectionMethod ->
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.wifi_detection_method),
showBottomItems = true,
)
Route.Donate -> {
Donate -> {
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.donate_title),
showBottomItems = true,
)
}
Route.Addresses -> {
Addresses -> {
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.addresses),
showBottomItems = true,
)
}
is Route.TunnelGlobals -> {
is TunnelGlobals -> {
NavbarState(
topLeading = {
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
navController.pop()
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.tunnel_global_overrides),
topTitle = context.getString(R.string.global_overrides),
showBottomItems = true,
)
}
is WifiPreferences -> {
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.wifi_settings),
showBottomItems = true,
)
}
is PreferredTunnel -> {
val title =
when (route.tunnelNetwork) {
TunnelNetwork.MOBILE_DATA,
TunnelNetwork.ETHERNET -> context.getString(R.string.preferred_tunnel)
TunnelNetwork.WIFI -> context.getString(R.string.tunnel_mapping)
}
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = title,
showBottomItems = true,
)
}
@@ -9,7 +9,10 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.NavController
@Composable
fun <T : NavKey> rememberNavController(
backStack: NavBackStack<NavKey>,
isDisclosureShown: Boolean,
onChange: (NavKey?) -> Unit = {},
): NavController {
return remember(backStack, onChange) { NavController(backStack, onChange) }
return remember(backStack, onChange, isDisclosureShown) {
NavController(backStack, isDisclosureShown, onChange)
}
}
@@ -7,30 +7,31 @@ 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.CheckCircle
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.banner.WarningBanner
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.networkTunnelingItems
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.wifiTunnelingItems
import com.zaneschepke.wireguardautotunnel.ui.navigation.TunnelNetwork
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.launchLocationServicesSettings
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
@@ -42,15 +43,18 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
val navController = LocalNavController.current
val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
LaunchedEffect(autoTunnelState.stateInitialized) {
if (!autoTunnelState.isLocationDisclosureShown && autoTunnelState.stateInitialized) {
navController.push(Route.LocationDisclosure)
}
}
if (!autoTunnelState.stateInitialized) return
if (autoTunnelState.isLoading) return
var showLocationDialog by remember { mutableStateOf(false) }
val (ethernetTunnel, mobileDataTunnel, mappedTunnels) =
remember(autoTunnelState.tunnels) {
Triple(
autoTunnelState.tunnels.firstOrNull { it.isEthernetTunnel },
autoTunnelState.tunnels.firstOrNull { it.isMobileDataTunnel },
autoTunnelState.tunnels.any { it.tunnelNetworks.isNotEmpty() },
)
}
val showLocationServicesWarning by
remember(
autoTunnelState.connectivityState?.wifiState,
@@ -92,83 +96,194 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier =
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
WarningBanner(
stringResource(R.string.location_services_not_detected),
showLocationServicesWarning,
trailing = {
TextButton({ context.launchLocationServicesSettings() }) {
Text(
stringResource(R.string.fix),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyMedium,
)
}
},
)
WarningBanner(
stringResource(R.string.location_permissions_missing),
showLocationPermissionsWarning,
trailing = {
TextButton({ showLocationDialog = true }) {
Text(
stringResource(R.string.fix),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyMedium,
)
}
},
)
val (title, buttonText, icon) =
remember(autoTunnelState.autoTunnelActive) {
when (autoTunnelState.autoTunnelActive) {
true ->
Triple(
context.getString(R.string.auto_tunnel_running),
context.getString(R.string.stop),
Icons.Outlined.CheckCircle,
Column {
WarningBanner(
stringResource(R.string.location_services_not_detected),
showLocationServicesWarning,
trailing = {
TextButton({ context.launchLocationServicesSettings() }) {
Text(
stringResource(R.string.fix),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyMedium,
)
false ->
Triple(
context.getString(R.string.auto_tunnel_not_running),
context.getString(R.string.start),
Icons.Outlined.Info,
}
},
)
WarningBanner(
stringResource(R.string.location_permissions_missing),
showLocationPermissionsWarning,
trailing = {
TextButton({ showLocationDialog = true }) {
Text(
stringResource(R.string.fix),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyMedium,
)
}
},
)
val (title, buttonText, icon) =
remember(autoTunnelState.autoTunnelActive) {
when (autoTunnelState.autoTunnelActive) {
true ->
Triple(
context.getString(R.string.auto_tunnel_running),
context.getString(R.string.stop),
Icons.Outlined.CheckCircle,
)
false ->
Triple(
context.getString(R.string.auto_tunnel_not_running),
context.getString(R.string.start),
Icons.Outlined.Info,
)
}
}
}
SurfaceSelectionGroupButton(
items =
listOf(
SelectionItem(
leading = { Icon(icon, null) },
title = { Text(title) },
trailing = {
Button({ viewModel.toggleAutoTunnel() }) {
Text(
buttonText,
fontWeight = FontWeight.Bold,
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.surface
),
SurfaceRow(
leading = { Icon(icon, null) },
title = title,
trailing = {
Button({ viewModel.toggleAutoTunnel() }) {
Text(
buttonText,
fontWeight = FontWeight.Bold,
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.surface
),
)
}
},
onClick = { viewModel.toggleAutoTunnel() },
)
}
Column {
GroupLabel(
stringResource(R.string.networks),
modifier = Modifier.padding(horizontal = 16.dp),
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Wifi, contentDescription = null) },
title = stringResource(R.string.tunnel_on_wifi),
trailing = { modifier ->
SwitchWithDivider(
checked = autoTunnelState.settings.isTunnelOnWifiEnabled,
onClick = { viewModel.setAutoTunnelOnWifiEnabled(it) },
modifier = modifier,
)
},
description = {
Text(
buildAnnotatedString {
append(stringResource(R.string.preferred_tunnel_label))
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(
if (mappedTunnels) stringResource(R.string.mapped)
else stringResource(R.string._default)
)
}
},
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
)
)
)
SurfaceSelectionGroupButton(items = wifiTunnelingItems(autoTunnelState, viewModel))
SectionDivider()
SurfaceSelectionGroupButton(items = networkTunnelingItems(autoTunnelState, viewModel))
SectionDivider()
SurfaceSelectionGroupButton(
items =
listOf(
AdvancedSettingsItem(onClick = { navController.push(Route.AdvancedAutoTunnel) })
)
)
},
onClick = { navController.push(Route.WifiPreferences) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.SignalCellular4Bar, contentDescription = null) },
title = stringResource(R.string.tunnel_mobile_data),
trailing = { modifier ->
SwitchWithDivider(
checked = autoTunnelState.settings.isTunnelOnMobileDataEnabled,
onClick = { viewModel.setTunnelOnCellular(it) },
modifier = modifier,
)
},
description = {
Text(
buildAnnotatedString {
append(stringResource(R.string.preferred_tunnel_label))
mobileDataTunnel?.tunName?.let { append(it) }
?: withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(stringResource(R.string._default))
}
},
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
)
},
onClick = { navController.push(Route.PreferredTunnel(TunnelNetwork.MOBILE_DATA)) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.SettingsEthernet, contentDescription = null) },
title = stringResource(R.string.tunnel_on_ethernet),
trailing = { modifier ->
SwitchWithDivider(
checked = autoTunnelState.settings.isTunnelOnEthernetEnabled,
onClick = { viewModel.setTunnelOnEthernet(it) },
modifier = modifier,
)
},
description = {
Text(
buildAnnotatedString {
append(stringResource(R.string.preferred_tunnel_label))
ethernetTunnel?.tunName?.let { append(it) }
?: withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(stringResource(R.string._default))
}
},
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
)
},
onClick = { navController.push(Route.PreferredTunnel(TunnelNetwork.ETHERNET)) },
)
}
Column {
GroupLabel(
stringResource(R.string.other),
modifier = Modifier.padding(horizontal = 16.dp),
)
SurfaceRow(
leading = { Icon(Icons.Outlined.PublicOff, contentDescription = null) },
title = stringResource(R.string.stop_on_no_internet),
description = {
Text(
stringResource(R.string.stop_on_internet_loss),
style =
MaterialTheme.typography.bodySmall.copy(
MaterialTheme.colorScheme.outline
),
)
},
trailing = {
ScaledSwitch(
checked = autoTunnelState.settings.isStopOnNoInternetEnabled,
onClick = { viewModel.setStopOnNoInternetEnabled(it) },
)
},
onClick = {
viewModel.setStopOnNoInternetEnabled(
!autoTunnelState.settings.isStopOnNoInternetEnabled
)
},
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
title = stringResource(R.string.advanced_settings),
onClick = { navController.push(Route.AdvancedAutoTunnel) },
)
}
}
}
@@ -3,14 +3,11 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.PauseCircle
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
@@ -29,19 +26,10 @@ fun AutoTunnelAdvancedScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier =
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 16.dp),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
LabelledDropdown(
title = {
Text(
stringResource(R.string.debounce_delay),
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface
),
)
},
title = stringResource(R.string.debounce_delay),
leading = { Icon(Icons.Outlined.PauseCircle, null) },
onSelected = { selected -> viewModel.setDebounceDelay(selected!!) },
options = (0..10).toList(),
@@ -1,28 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun AdvancedSettingsItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
title = {
Text(
stringResource(R.string.advanced_settings),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = { ForwardButton { onClick() } },
onClick = { onClick() },
)
}
@@ -1,133 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.PublicOff
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.SignalCellular4Bar
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.state.AutoTunnelUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
@Composable
fun networkTunnelingItems(
autoTunnelState: AutoTunnelUiState,
viewModel: AutoTunnelViewModel,
): List<SelectionItem> {
return listOf(
SelectionItem(
leading = { Icon(Icons.Outlined.SignalCellular4Bar, contentDescription = null) },
title = {
Text(
stringResource(R.string.tunnel_mobile_data),
style =
MaterialTheme.typography.bodyMedium.copy(
MaterialTheme.colorScheme.onSurface
),
)
},
trailing = {
ScaledSwitch(
checked = autoTunnelState.settings.isTunnelOnMobileDataEnabled,
onClick = { viewModel.setTunnelOnCellular(it) },
)
},
description = {
val cellularActive =
remember(autoTunnelState.connectivityState) {
autoTunnelState.connectivityState?.cellularConnected ?: false
}
Text(
text =
if (cellularActive) stringResource(R.string.active)
else stringResource(R.string.inactive),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
onClick = {
viewModel.setTunnelOnCellular(!autoTunnelState.settings.isTunnelOnMobileDataEnabled)
},
),
SelectionItem(
leading = { Icon(Icons.Outlined.SettingsEthernet, contentDescription = null) },
title = {
Text(
stringResource(R.string.tunnel_on_ethernet),
style =
MaterialTheme.typography.bodyMedium.copy(
MaterialTheme.colorScheme.onSurface
),
)
},
trailing = {
ScaledSwitch(
checked = autoTunnelState.settings.isTunnelOnEthernetEnabled,
onClick = { viewModel.setTunnelOnEthernet(it) },
)
},
description = {
val ethernetActive =
remember(autoTunnelState.connectivityState) {
autoTunnelState.connectivityState?.ethernetConnected ?: false
}
Text(
text =
if (ethernetActive) stringResource(R.string.active)
else stringResource(R.string.inactive),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
onClick = {
viewModel.setTunnelOnEthernet(!autoTunnelState.settings.isTunnelOnEthernetEnabled)
},
),
SelectionItem(
leading = { Icon(Icons.Outlined.PublicOff, contentDescription = null) },
title = {
Text(
stringResource(R.string.stop_on_no_internet),
style =
MaterialTheme.typography.bodyMedium.copy(
MaterialTheme.colorScheme.onSurface
),
)
},
description = {
Text(
stringResource(R.string.stop_on_internet_loss),
style =
MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
checked = autoTunnelState.settings.isStopOnNoInternetEnabled,
onClick = { viewModel.setStopOnNoInternetEnabled(it) },
)
},
onClick = {
viewModel.setStopOnNoInternetEnabled(
!autoTunnelState.settings.isStopOnNoInternetEnabled
)
},
),
)
}
@@ -1,7 +1,11 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
import android.R.attr.text
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
@@ -12,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.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -20,7 +25,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.common.button.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
@@ -33,21 +37,23 @@ fun TrustedNetworkTextBox(
onSave: (ssid: String) -> Unit,
onValueChange: (network: String) -> Unit,
supporting: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
val isTv = LocalIsAndroidTV.current
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
val bringIntoViewRequester = remember { BringIntoViewRequester() }
LaunchedEffect(trustedNetworks.size) { bringIntoViewRequester.bringIntoView() }
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = modifier.animateContentSize().imePadding(),
) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().animateContentSize(),
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally),
) {
trustedNetworks.forEach { ssid ->
ClickableIconButton(
onClick = {
if (isTv) {
onDelete(ssid)
}
},
onIconClick = { onDelete(ssid) },
onClick = { onDelete(ssid) },
text = ssid,
icon = Icons.Filled.Close,
)
@@ -70,8 +76,7 @@ fun TrustedNetworkTextBox(
},
containerColor = MaterialTheme.colorScheme.surface,
supportingText = supporting,
modifier =
Modifier.padding(top = 5.dp, bottom = 10.dp).fillMaxWidth().padding(end = 16.dp),
modifier = Modifier.fillMaxWidth().bringIntoViewRequester(bringIntoViewRequester),
singleLine = true,
keyboardOptions =
KeyboardOptions(
@@ -1,218 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Filter1
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.Wifi
import androidx.compose.material.icons.outlined.WifiFind
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
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.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
import com.zaneschepke.wireguardautotunnel.ui.state.AutoTunnelUiState
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
@Composable
fun wifiTunnelingItems(
autoTunnelState: AutoTunnelUiState,
viewModel: AutoTunnelViewModel,
): List<SelectionItem> {
val context = LocalContext.current
val navController = LocalNavController.current
val clipboardHelper = rememberClipboardHelper()
var currentText by rememberSaveable { mutableStateOf("") }
LaunchedEffect(autoTunnelState.settings.trustedNetworkSSIDs) { currentText = "" }
val baseItems =
listOf(
SelectionItem(
leading = { Icon(Icons.Outlined.Wifi, contentDescription = null) },
title = {
Text(
stringResource(R.string.tunnel_on_wifi),
style =
MaterialTheme.typography.bodyMedium.copy(
MaterialTheme.colorScheme.onSurface
),
)
},
trailing = {
ScaledSwitch(
checked = autoTunnelState.settings.isTunnelOnWifiEnabled,
onClick = { viewModel.setAutoTunnelOnWifiEnabled(it) },
)
},
description = {
val wifiInfo by
remember(autoTunnelState.connectivityState) {
derivedStateOf {
autoTunnelState.connectivityState
?.wifiState
?.takeIf { it.connected }
.let { Pair(it?.ssid, it?.securityType) }
}
}
val (wifiName, securityType) = wifiInfo
Column {
Text(
text =
wifiName?.let { stringResource(R.string.wifi_name_template, it) }
?: stringResource(R.string.inactive),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier.clickable { wifiName?.let { clipboardHelper.copy(it) } },
)
securityType?.let {
Text(
text = stringResource(R.string.security_template, it.name),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
},
onClick = {
viewModel.setAutoTunnelOnWifiEnabled(
!autoTunnelState.settings.isTunnelOnWifiEnabled
)
},
)
)
return if (autoTunnelState.settings.isTunnelOnWifiEnabled) {
baseItems +
listOf(
SelectionItem(
leading = { Icon(Icons.Outlined.WifiFind, contentDescription = null) },
title = {
Text(
stringResource(R.string.wifi_detection_method),
style =
MaterialTheme.typography.bodyMedium.copy(
MaterialTheme.colorScheme.onSurface
),
)
},
description = {
Text(
stringResource(
R.string.current_template,
autoTunnelState.settings.wifiDetectionMethod.asTitleString(context),
),
style =
MaterialTheme.typography.bodySmall.copy(
MaterialTheme.colorScheme.outline
),
)
},
trailing = { ForwardButton { navController.push(Route.WifiDetectionMethod) } },
onClick = { navController.push(Route.WifiDetectionMethod) },
),
SelectionItem(
leading = { Icon(Icons.Outlined.Filter1, contentDescription = null) },
title = {
Text(
stringResource(R.string.use_wildcards),
style =
MaterialTheme.typography.bodyMedium.copy(
MaterialTheme.colorScheme.onSurface
),
)
},
description = {
LearnMoreLinkLabel(
{ context.openWebUrl(it) },
stringResource(R.string.docs_wildcards),
)
},
trailing = {
ScaledSwitch(
checked = autoTunnelState.settings.isWildcardsEnabled,
onClick = { viewModel.setWildcardsEnabled(it) },
)
},
onClick = {
viewModel.setWildcardsEnabled(!autoTunnelState.settings.isWildcardsEnabled)
},
),
SelectionItem(
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
) {
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)
.padding(vertical = 6.dp),
) {
Text(
stringResource(R.string.trusted_wifi_names),
style =
MaterialTheme.typography.bodyMedium.copy(
MaterialTheme.colorScheme.onSurface
),
)
}
}
}
},
description = {
TrustedNetworkTextBox(
autoTunnelState.settings.trustedNetworkSSIDs,
onDelete = { viewModel.removeTrustedNetworkName(it) },
currentText = currentText,
onSave = { ssid -> viewModel.saveTrustedNetworkName(ssid) },
onValueChange = { currentText = it },
supporting = {
if (autoTunnelState.settings.isWildcardsEnabled) WildcardsLabel()
},
)
},
),
)
} else {
baseItems
}
}
@@ -9,19 +9,24 @@ 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.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.DirectionsWalk
import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
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.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.LocationDisclosureHeader
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.appSettingsItem
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.skipItem
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
@Composable
@@ -43,21 +48,29 @@ fun LocationDisclosureScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(18.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().padding(top = 18.dp).padding(horizontal = 16.dp),
modifier = Modifier.fillMaxSize().padding(top = 18.dp),
) {
LocationDisclosureHeader()
SurfaceSelectionGroupButton(
items =
listOf(
appSettingsItem {
val intent =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
settingsLauncher.launch(intent)
}
)
)
SurfaceSelectionGroupButton(items = listOf(skipItem { goToAutoTunnel() }))
LocationDisclosureHeader(Modifier.padding(horizontal = 16.dp))
Column {
SurfaceRow(
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
title = stringResource(R.string.launch_app_settings),
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
onClick = {
val intent =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
settingsLauncher.launch(intent)
},
)
SurfaceRow(
leading = {
Icon(Icons.AutoMirrored.Outlined.DirectionsWalk, contentDescription = null)
},
title = stringResource(R.string.skip),
onClick = { goToAutoTunnel() },
)
}
}
}
@@ -1,27 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.LaunchButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
@Composable
fun appSettingsItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.LocationOn, contentDescription = null) },
title = {
SelectionItemLabel(
stringResource(R.string.launch_app_settings),
labelType = SelectionLabelType.TITLE,
)
},
trailing = { LaunchButton { onClick() } },
onClick = onClick,
)
}
@@ -1,6 +1,8 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.PermScanWifi
@@ -8,26 +10,37 @@ import androidx.compose.material3.Icon
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.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun LocationDisclosureHeader() {
val icon = Icons.Rounded.PermScanWifi
Icon(
imageVector = icon,
contentDescription = icon.name,
modifier = Modifier.padding(24.dp).size(100.dp),
)
Text(
text = stringResource(R.string.prominent_background_location_title),
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
)
Text(
text = stringResource(R.string.prominent_background_location_message),
style = MaterialTheme.typography.bodyMedium,
)
fun LocationDisclosureHeader(modifier: Modifier = Modifier) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
) {
Icon(
imageVector = Icons.Rounded.PermScanWifi,
contentDescription = null,
modifier = Modifier.size(100.dp),
)
Text(
text = stringResource(R.string.prominent_background_location_title),
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResource(R.string.prominent_background_location_message),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth(),
)
}
}
@@ -1,20 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
@Composable
fun skipItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
title = {
SelectionItemLabel(stringResource(R.string.skip), labelType = SelectionLabelType.TITLE)
},
trailing = { ForwardButton { onClick() } },
onClick = onClick,
)
}
@@ -0,0 +1,271 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferences
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.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
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.Check
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.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.withStyle
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.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
import com.zaneschepke.wireguardautotunnel.ui.navigation.TunnelNetwork
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
@Composable
fun PreferredTunnelScreen(
tunnelNetwork: TunnelNetwork,
viewModel: AutoTunnelViewModel = hiltViewModel(),
) {
val navController = LocalNavController.current
val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (autoTunnelState.isLoading) return
var selectedTunnel by remember { mutableStateOf<TunnelConf?>(null) }
val currentSelection =
remember(autoTunnelState.tunnels) {
when (tunnelNetwork) {
TunnelNetwork.MOBILE_DATA ->
autoTunnelState.tunnels.firstOrNull { it.isMobileDataTunnel }
TunnelNetwork.ETHERNET ->
autoTunnelState.tunnels.firstOrNull { it.isEthernetTunnel }
TunnelNetwork.WIFI -> null
}
}
LazyColumn(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier =
Modifier.pointerInput(Unit) {
if (autoTunnelState.tunnels.isEmpty()) return@pointerInput
}
.overscroll(rememberOverscrollEffect()),
state = rememberLazyListState(),
userScrollEnabled = true,
reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(),
) {
item { GroupLabel(stringResource(R.string.tunnels), Modifier.padding(horizontal = 16.dp)) }
if (tunnelNetwork != TunnelNetwork.WIFI) {
item {
SurfaceRow(
title =
buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(stringResource(R.string._default))
}
},
trailing =
if (currentSelection == null) {
{
Icon(
Icons.Outlined.Check,
stringResource(id = R.string.selected),
tint = MaterialTheme.colorScheme.primary,
)
}
} else null,
onClick = {
when (tunnelNetwork) {
TunnelNetwork.MOBILE_DATA -> {
viewModel.setPreferredMobileDataTunnel(null)
navController.pop()
}
TunnelNetwork.ETHERNET -> {
viewModel.setPreferredEthernetTunnel(null)
navController.pop()
}
TunnelNetwork.WIFI -> Unit
}
},
)
}
}
items(autoTunnelState.tunnels, key = { it.id }) { tunnel ->
SurfaceRow(
title = tunnel.tunName,
trailing =
if (currentSelection?.id == tunnel.id) {
{
Icon(
Icons.Outlined.Check,
stringResource(id = R.string.selected),
tint = MaterialTheme.colorScheme.primary,
)
}
} else null,
onClick = {
when (tunnelNetwork) {
TunnelNetwork.MOBILE_DATA -> {
viewModel.setPreferredMobileDataTunnel(tunnel)
navController.pop()
}
TunnelNetwork.ETHERNET -> {
viewModel.setPreferredEthernetTunnel(tunnel)
navController.pop()
}
TunnelNetwork.WIFI -> {
selectedTunnel = tunnel
}
}
},
expandedContent =
if (tunnelNetwork == TunnelNetwork.WIFI) {
{
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement =
Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally),
) {
tunnel.tunnelNetworks.forEach { ssid ->
ClickableIconButton(
onClick = {
viewModel.removeTunnelNetwork(tunnel, ssid)
},
text = ssid,
icon = Icons.Filled.Close,
)
}
}
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()
}
CustomTextField(
textStyle =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onSurface
),
value = currentText,
onValueChange = { currentText = it },
interactionSource = remember { MutableInteractionSource() },
label = {
Text(
stringResource(R.string.add_wifi_name),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelMedium,
)
},
containerColor = MaterialTheme.colorScheme.surface,
supportingText =
if (autoTunnelState.settings.isWildcardsEnabled) {
{ WildcardsLabel() }
} else null,
modifier =
Modifier.fillMaxWidth()
.focusRequester(focusRequester)
.bringIntoViewRequester(bringIntoViewRequester)
.imePadding(),
singleLine = true,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions =
KeyboardActions(
onDone = {
viewModel.addTunnelNetwork(tunnel, currentText)
currentText = ""
}
),
trailing = {
if (currentText != "") {
IconButton(
onClick = {
viewModel.addTunnelNetwork(
tunnel,
currentText,
)
currentText = ""
}
) {
val icon = Icons.Outlined.Add
Icon(
imageVector = icon,
contentDescription =
stringResource(
R.string
.trusted_ssid_value_description
),
tint = MaterialTheme.colorScheme.primary,
)
}
}
},
)
}
}
}
} else null,
)
}
}
}
@@ -0,0 +1,117 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.preferences.wifi
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Filter1
import androidx.compose.material.icons.outlined.Map
import androidx.compose.material.icons.outlined.WifiFind
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
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.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
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.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.TunnelNetwork
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
@Composable
fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
val context = LocalContext.current
val navController = LocalNavController.current
val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
var currentText by rememberSaveable { mutableStateOf("") }
LaunchedEffect(autoTunnelState.settings.trustedNetworkSSIDs) { currentText = "" }
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
Column {
GroupLabel(stringResource(R.string.general), Modifier.padding(horizontal = 16.dp))
SurfaceRow(
leading = { Icon(Icons.Outlined.WifiFind, contentDescription = null) },
title = stringResource(R.string.wifi_detection_method),
description = {
DescriptionText(
stringResource(
R.string.current_template,
autoTunnelState.settings.wifiDetectionMethod.asTitleString(context),
)
)
},
onClick = { navController.push(Route.WifiDetectionMethod) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Filter1, contentDescription = null) },
title = stringResource(R.string.use_wildcards),
description = {
LearnMoreLinkLabel(
{ context.openWebUrl(it) },
stringResource(R.string.docs_wildcards),
)
},
trailing = {
ScaledSwitch(
checked = autoTunnelState.settings.isWildcardsEnabled,
onClick = { viewModel.setWildcardsEnabled(it) },
)
},
onClick = {
viewModel.setWildcardsEnabled(!autoTunnelState.settings.isWildcardsEnabled)
},
)
}
Column {
GroupLabel(stringResource(R.string.networks), Modifier.padding(horizontal = 16.dp))
SurfaceRow(
title = stringResource(R.string.trusted_wifi_names),
expandedContent = {
TrustedNetworkTextBox(
autoTunnelState.settings.trustedNetworkSSIDs,
onDelete = { viewModel.removeTrustedNetworkName(it) },
currentText = currentText,
onSave = { ssid -> viewModel.saveTrustedNetworkName(ssid) },
onValueChange = { currentText = it },
supporting = {
if (autoTunnelState.settings.isWildcardsEnabled) WildcardsLabel()
},
modifier = Modifier.padding(top = 4.dp),
)
},
)
}
Column {
GroupLabel(stringResource(R.string.tunnels), Modifier.padding(horizontal = 16.dp))
SurfaceRow(
leading = { Icon(Icons.Outlined.Map, contentDescription = null) },
title = stringResource(R.string.tunnel_mapping),
onClick = { navController.push(Route.PreferredTunnel(TunnelNetwork.WIFI)) },
)
}
}
}
@@ -1,51 +1,66 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
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.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
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.automirrored.outlined.ViewQuilt
import androidx.compose.material.icons.outlined.Android
import androidx.compose.material.icons.outlined.Dns
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material.icons.outlined.Lan
import androidx.compose.material.icons.outlined.MonitorHeart
import androidx.compose.material.icons.outlined.Pin
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
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.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
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.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.*
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.AppModeBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
import java.util.Locale
import org.orbitmvi.orbit.compose.collectSideEffect
@Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val isTv = LocalIsAndroidTV.current
val context = LocalContext.current
val navController = LocalNavController.current
val focusManager = LocalFocusManager.current
val sharedViewModel = LocalSharedVm.current
val interactionSource = remember { MutableInteractionSource() }
val locale = remember { Locale.getDefault() }
val settingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (settingsState.isLoading) return
var showBackupSheet by rememberSaveable { mutableStateOf(false) }
var showAppModeSheet by rememberSaveable { mutableStateOf(false) }
@@ -53,8 +68,11 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
rememberSaveable(settingsState.settings.appMode) {
mutableStateOf(settingsState.settings.appMode)
}
if (!settingsState.stateInitialized) return
val monitoringEnabled by
rememberSaveable(appMode) {
mutableStateOf(appMode == AppMode.VPN || appMode == AppMode.KERNEL)
}
val dnsEnabled by rememberSaveable(appMode) { mutableStateOf(appMode != AppMode.KERNEL) }
sharedViewModel.collectSideEffect { sideEffect ->
when (sideEffect) {
@@ -81,101 +99,146 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier =
Modifier.verticalScroll(rememberScrollState())
.fillMaxSize()
.then(
if (!isTv) {
Modifier.clickable(
indication = null,
interactionSource = interactionSource,
onClick = { focusManager.clearFocus() },
)
} else {
Modifier
}
)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.verticalScroll(rememberScrollState()).fillMaxSize(),
) {
SurfaceSelectionGroupButton(
listOf(appModeItem(settingsState.settings.appMode) { showAppModeSheet = true })
)
SectionDivider()
SurfaceSelectionGroupButton(
items =
buildList {
if (appMode == AppMode.LOCK_DOWN) {
add(
lanTrafficItem(settingsState.settings.isLanOnKillSwitchEnabled) {
viewModel.setLanKillSwitchEnabled(it)
}
Column {
GroupLabel(
stringResource(R.string.tunnel).capitalize(locale),
modifier = Modifier.padding(horizontal = 16.dp),
)
SurfaceRow(
leading = {
Icon(ImageVector.vectorResource(R.drawable.sdk), contentDescription = null)
},
trailing = {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
},
title = stringResource(R.string.backend_mode),
description = {
DescriptionText(
stringResource(R.string.current_template, appMode.asTitleString(context))
)
},
onClick = { showAppModeSheet = true },
)
if (appMode == AppMode.LOCK_DOWN) {
SurfaceRow(
leading = { Icon(Icons.Outlined.Lan, contentDescription = null) },
title = stringResource(R.string.allow_lan_traffic),
description = {
Text(
text = stringResource(R.string.bypass_lan_for_kill_switch),
style =
MaterialTheme.typography.bodySmall.copy(
MaterialTheme.colorScheme.outline
),
)
},
trailing = {
ScaledSwitch(
checked = settingsState.settings.isLanOnKillSwitchEnabled,
onClick = { viewModel.setLanKillSwitchEnabled(it) },
)
},
onClick = {
viewModel.setLanKillSwitchEnabled(
!settingsState.settings.isLanOnKillSwitchEnabled
)
},
)
}
SurfaceRow(
leading = { Icon(Icons.Outlined.MonitorHeart, null) },
title = stringResource(R.string.tunnel_monitoring),
enabled = monitoringEnabled,
onClick = { navController.push(Route.TunnelMonitoring) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Dns, null) },
title = stringResource(R.string.dns_settings),
enabled = dnsEnabled,
onClick = {
if (dnsEnabled) navController.push(Route.Dns)
else
sharedViewModel.showSnackMessage(
StringValue.StringResource(
R.string.mode_disabled_template,
appMode.asString(context),
)
)
},
)
SurfaceRow(
leading = {
Icon(ImageVector.vectorResource(R.drawable.globe), contentDescription = null)
},
title = stringResource(R.string.global_overrides),
trailing = { modifier ->
SwitchWithDivider(
checked = settingsState.settings.isTunnelGlobalsEnabled,
onClick = { viewModel.setTunnelGlobals(it) },
modifier = modifier,
)
},
onClick = {
settingsState.globalTunnelConf?.let {
navController.push(Route.TunnelGlobals(it.id))
}
add(
tunnelMonitoringItem(
appMode,
onClick = { navController.push(Route.TunnelMonitoring) },
) {
sharedViewModel.showSnackMessage(
StringValue.StringResource(
R.string.mode_disabled_template,
appMode.asString(context),
)
)
}
)
add(
dnsSettingsItem(appMode, onClick = { navController.push(Route.Dns) }) {
sharedViewModel.showSnackMessage(
StringValue.StringResource(
R.string.mode_disabled_template,
appMode.asString(context),
)
)
}
)
add(
tunnelGlobalsSettingItem(
settingsState.settings.isTunnelGlobalsEnabled,
onClick = viewModel::setTunnelGlobals,
) {
settingsState.globalTunnelConf?.let {
navController.push(Route.TunnelGlobals(it.id))
}
}
)
if (showProxySettings)
add(proxYSettingsItem { navController.push(Route.ProxySettings) })
}
)
SectionDivider()
SurfaceSelectionGroupButton(
listOf(systemFeaturesItem { navController.push(Route.SystemFeatures) })
)
SectionDivider()
SurfaceSelectionGroupButton(
items =
buildList {
add(appearanceItem { navController.push(Route.Appearance) })
add(
localLoggingItem(settingsState.isLocalLoggingEnabled) {
viewModel.setLocalLogging(it)
}
)
if (settingsState.isLocalLoggingEnabled)
add(readLogsItem { navController.push(Route.Logs) })
add(
pinLockItem(settingsState.isPinLockEnabled) { enabled ->
if (enabled) {
},
)
if (showProxySettings) {
SurfaceRow(
leading = { Icon(ImageVector.vectorResource(R.drawable.proxy), null) },
title = stringResource(R.string.proxy_settings),
onClick = { navController.push(Route.ProxySettings) },
)
}
}
Column {
GroupLabel(
stringResource(R.string.general),
modifier = Modifier.padding(horizontal = 16.dp),
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Android, null) },
title = stringResource(R.string.android_integrations),
onClick = { navController.push(Route.AndroidIntegrations) },
)
SurfaceRow(
leading = {
Icon(Icons.AutoMirrored.Outlined.ViewQuilt, contentDescription = null)
},
title = stringResource(R.string.appearance),
onClick = { navController.push(Route.Appearance) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Pin, contentDescription = null) },
title = stringResource(R.string.enable_app_lock),
trailing = {
ScaledSwitch(
checked = settingsState.isPinLockEnabled,
onClick = {
if (it) {
navController.push(Route.Lock)
} else {
sharedViewModel.setPinLockEnabled(false)
}
}
},
)
}
)
Spacer(Modifier.height(80.dp))
},
onClick = {
if (!settingsState.isPinLockEnabled) {
navController.push(Route.Lock)
} else {
sharedViewModel.setPinLockEnabled(false)
}
},
)
}
}
}
@@ -3,28 +3,50 @@ 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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.outlined.Contrast
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.Translate
import androidx.compose.material3.Icon
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
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.components.DisplayThemeItem
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.components.LanguageItem
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.components.NotificationsItem
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings
@Composable
fun AppearanceScreen() {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.fillMaxSize(),
) {
SurfaceSelectionGroupButton(items = listOf(LanguageItem()))
SectionDivider()
SurfaceSelectionGroupButton(items = listOf(NotificationsItem()))
SectionDivider()
SurfaceSelectionGroupButton(items = listOf(DisplayThemeItem()))
val navController = LocalNavController.current
val context = LocalContext.current
Column {
SurfaceRow(
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
title = stringResource(R.string.language),
onClick = { navController.push(Route.Language) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Notifications, contentDescription = null) },
title = stringResource(R.string.notifications),
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
onClick = { context.launchNotificationSettings() },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Contrast, contentDescription = null) },
title = stringResource(R.string.display_theme),
onClick = { navController.push(Route.Display) },
)
}
}
}
@@ -1,31 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Contrast
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
@Composable
fun DisplayThemeItem(): SelectionItem {
val navController = LocalNavController.current
return SelectionItem(
leading = { Icon(Icons.Outlined.Contrast, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.display_theme),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = { ForwardButton { navController.push(Route.Display) } },
onClick = { navController.push(Route.Display) },
)
}
@@ -1,31 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Translate
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
@Composable
fun LanguageItem(): SelectionItem {
val navController = LocalNavController.current
return SelectionItem(
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.language),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = { ForwardButton { navController.push(Route.Language) } },
onClick = { navController.push(Route.Language) },
)
}
@@ -1,31 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.LaunchButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings
@Composable
fun NotificationsItem(): SelectionItem {
val context = LocalContext.current
return SelectionItem(
leading = { Icon(Icons.Outlined.Notifications, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.notifications),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = { LaunchButton { context.launchNotificationSettings() } },
onClick = { context.launchNotificationSettings() },
)
}
@@ -3,17 +3,19 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.displ
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.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
@Composable
@@ -25,8 +27,8 @@ fun DisplayScreen() {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxSize(),
) {
enumValues<Theme>().forEach {
val title =
@@ -38,10 +40,19 @@ fun DisplayScreen() {
Theme.DARKER -> stringResource(R.string.darker)
Theme.AMOLED -> stringResource(R.string.amoled)
}
IconSurfaceButton(
SurfaceRow(
title = title,
trailing =
if (appState.theme == it) {
{
Icon(
Icons.Outlined.Check,
stringResource(id = R.string.selected),
tint = MaterialTheme.colorScheme.primary,
)
}
} else null,
onClick = { sharedViewModel.setTheme(it) },
selected = appState.theme == it,
)
}
}
@@ -1,22 +1,23 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language
import androidx.compose.foundation.layout.Arrangement
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.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import java.text.Collator
import java.util.*
@@ -62,13 +63,21 @@ fun LanguageScreen() {
state = lazyListState,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(horizontal = 16.dp),
) {
item {
IconSurfaceButton(
stringResource(R.string.automatic),
{ sharedViewModel.setLocale(LocaleUtil.OPTION_PHONE_LANGUAGE) },
selected = appState.locale == LocaleUtil.OPTION_PHONE_LANGUAGE,
SurfaceRow(
title = stringResource(R.string.automatic),
trailing =
if (appState.locale == LocaleUtil.OPTION_PHONE_LANGUAGE) {
{
Icon(
Icons.Outlined.Check,
stringResource(id = R.string.selected),
tint = MaterialTheme.colorScheme.primary,
)
}
} else null,
onClick = { sharedViewModel.setLocale(LocaleUtil.OPTION_PHONE_LANGUAGE) },
)
}
items(sortedLocales, key = { it }) { locale ->
@@ -76,10 +85,19 @@ fun LanguageScreen() {
locale.getDisplayName(locale).replaceFirstChar {
if (it.isLowerCase()) it.titlecase(locale) else it.toString()
}
IconSurfaceButton(
buttonText,
SurfaceRow(
title = buttonText,
trailing =
if (appState.locale == locale.toLanguageTag()) {
{
Icon(
Icons.Outlined.Check,
stringResource(id = R.string.selected),
tint = MaterialTheme.colorScheme.primary,
)
}
} else null,
onClick = { sharedViewModel.setLocale(locale.toLanguageTag()) },
selected = appState.locale == locale.toLanguageTag(),
)
}
}
@@ -1,30 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
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.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton
import com.zaneschepke.wireguardautotunnel.ui.common.label.SelectedLabel
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
@Composable
fun automaticLanguageItem(locale: String, onChange: (locale: String) -> Unit) {
val isTv = LocalIsAndroidTV.current
Box(modifier = Modifier.padding(top = 24.dp)) {
SelectionItemButton(
buttonText = stringResource(R.string.automatic),
onClick = { onChange(LocaleUtil.OPTION_PHONE_LANGUAGE) },
trailing = {
if (locale == LocaleUtil.OPTION_PHONE_LANGUAGE) {
SelectedLabel()
}
},
ripple = isTv,
)
}
}
@@ -1,32 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.components
import androidx.compose.runtime.Composable
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton
import com.zaneschepke.wireguardautotunnel.ui.common.label.SelectedLabel
import java.util.*
@Composable
fun languageItem(currentLocale: String, locale: Locale, onChange: (String) -> Unit) {
val isTv = LocalIsAndroidTV.current
SelectionItemButton(
buttonText =
locale.getDisplayLanguage(locale).replaceFirstChar {
if (it.isLowerCase()) it.titlecase(locale) else it.toString()
} +
if (locale.toLanguageTag().contains("-")) {
" (${locale.getDisplayCountry(locale).replaceFirstChar {
if (it.isLowerCase()) it.titlecase(locale) else it.toString()
}})"
} else {
""
},
onClick = { onChange(locale.toLanguageTag()) },
trailing = {
if (locale.toLanguageTag() == currentLocale) {
SelectedLabel()
}
},
ripple = isTv,
)
}
@@ -1,37 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Lan
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun lanTrafficItem(
isLanKillSwitchEnabled: Boolean,
onClick: (enable: Boolean) -> Unit,
): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.Lan, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.allow_lan_traffic),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
text = stringResource(R.string.bypass_lan_for_kill_switch),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = { ScaledSwitch(checked = isLanKillSwitchEnabled, onClick = onClick) },
onClick = { onClick(!isLanKillSwitchEnabled) },
)
}
@@ -1,33 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ViewHeadline
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
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.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
@Composable
fun localLoggingItem(
isLocalLoggingEnabled: Boolean,
onClick: (checked: Boolean) -> Unit,
): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.ViewHeadline, contentDescription = null) },
title = {
SelectionItemLabel(stringResource(R.string.local_logging), SelectionLabelType.TITLE)
},
description = {
SelectionItemLabel(
stringResource(R.string.enable_local_logging),
SelectionLabelType.DESCRIPTION,
)
},
trailing = { ScaledSwitch(checked = isLocalLoggingEnabled, onClick = onClick) },
onClick = { onClick(!isLocalLoggingEnabled) },
)
}
@@ -1,34 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun LocationServicesDialog(show: Boolean, onDismiss: () -> Unit, onAttest: () -> Unit) {
if (show) {
AlertDialog(
onDismissRequest = { onDismiss() },
confirmButton = {
TextButton(
onClick = {
onDismiss()
onAttest()
}
) {
Text(text = stringResource(R.string.okay))
}
},
dismissButton = {
TextButton(onClick = { onDismiss() }) {
Text(text = stringResource(R.string.cancel))
}
},
title = { Text(text = stringResource(R.string.location_services_not_detected)) },
text = { Text(text = stringResource(R.string.location_services_missing_message)) },
)
}
}
@@ -1,29 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Pin
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun pinLockItem(isPinLockEnabled: Boolean, onClick: (checked: Boolean) -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.Pin, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.enable_app_lock),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = { ScaledSwitch(checked = isPinLockEnabled, onClick = onClick) },
onClick = { onClick(!isPinLockEnabled) },
)
}
@@ -1,47 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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 com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
@Composable
fun appModeItem(appMode: AppMode, onClick: () -> Unit): SelectionItem {
val context = LocalContext.current
return SelectionItem(
leading = { Icon(ImageVector.vectorResource(R.drawable.sdk), contentDescription = null) },
trailing = {
IconButton(onClick = onClick) {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
modifier = Modifier.size(iconSize),
)
}
},
title = {
SelectionItemLabel(stringResource(R.string.backend_mode), SelectionLabelType.TITLE)
},
description = {
SelectionItemLabel(
stringResource(R.string.current_template, appMode.asTitleString(context)),
SelectionLabelType.DESCRIPTION,
)
},
onClick = onClick,
)
}
@@ -1,28 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun appearanceItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.AutoMirrored.Outlined.ViewQuilt, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.appearance),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = { ForwardButton { onClick() } },
onClick = onClick,
)
}
@@ -1,43 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Dns
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
@Composable
fun dnsSettingsItem(
appMode: AppMode,
onClick: () -> Unit,
onDisabledClick: () -> Unit,
): SelectionItem {
val context = LocalContext.current
val enabled by rememberSaveable(appMode) { mutableStateOf(appMode != AppMode.KERNEL) }
val click = if (enabled) onClick else onDisabledClick
return SelectionItem(
leading = { Icon(Icons.Outlined.Dns, null) },
trailing = { ForwardButton { click() } },
title = {
Text(
text = stringResource(R.string.dns_settings),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = click,
disabledReason =
context.getString(R.string.mode_disabled_template, appMode.asString(context)),
)
}
@@ -1,28 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun proxYSettingsItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(ImageVector.vectorResource(R.drawable.proxy), null) },
trailing = { ForwardButton { onClick() } },
title = {
Text(
text = stringResource(R.string.proxy_settings),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = onClick,
)
}
@@ -1,24 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ViewTimeline
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
@Composable
fun readLogsItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Filled.ViewTimeline, contentDescription = null) },
title = {
SelectionItemLabel(stringResource(R.string.read_logs), SelectionLabelType.TITLE)
},
trailing = { ForwardButton { onClick() } },
onClick = onClick,
)
}
@@ -1,28 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Android
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun systemFeaturesItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.Android, null) },
trailing = { ForwardButton { onClick.invoke() } },
title = {
Text(
text = stringResource(R.string.system_features),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = onClick,
)
}
@@ -1,33 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import android.R.attr.enabled
import android.R.attr.onClick
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import com.zaneschepke.wireguardautotunnel.R
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.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
@Composable
fun tunnelGlobalsSettingItem(
enabled: Boolean,
onClick: (Boolean) -> Unit,
onItemClick: () -> Unit,
): SelectionItem {
return SelectionItem(
leading = { Icon(ImageVector.vectorResource(R.drawable.globe), contentDescription = null) },
title = {
SelectionItemLabel(
stringResource(R.string.tunnel_global_overrides),
SelectionLabelType.TITLE,
)
},
trailing = { ScaledSwitch(checked = enabled, onClick = onClick) },
onClick = onItemClick,
)
}
@@ -1,47 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.MonitorHeart
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
@Composable
fun tunnelMonitoringItem(
appMode: AppMode,
onClick: () -> Unit,
onDisabledClick: () -> Unit,
): SelectionItem {
val context = LocalContext.current
val enabled by
rememberSaveable(appMode) {
mutableStateOf(appMode == AppMode.VPN || appMode == AppMode.KERNEL)
}
val click = if (enabled) onClick else onDisabledClick
return SelectionItem(
leading = { Icon(Icons.Outlined.MonitorHeart, null) },
trailing = { ForwardButton { click() } },
title = {
Text(
text = stringResource(R.string.tunnel_monitoring),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
isEnabled = enabled,
onClick = click,
disabledReason =
context.getString(R.string.mode_disabled_template, appMode.asString(context)),
)
}
@@ -4,15 +4,12 @@ import androidx.compose.animation.AnimatedVisibility
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Cloud
import androidx.compose.material.icons.outlined.Dns
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
@@ -33,47 +30,34 @@ fun DnsSettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val context = LocalContext.current
val settingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (settingsState.isLoading) return
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier =
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 16.dp),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
LabelledDropdown(
title = {
Text(
text = stringResource(R.string.dns_protocol),
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface
),
)
},
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
currentValue = settingsState.settings.dnsProtocol,
onSelected = { selected -> selected?.let { viewModel.setDnsProtocol(it) } },
options = DnsProtocol.entries,
optionToString = { (it ?: DnsProtocol.SYSTEM).asString(context) },
)
AnimatedVisibility(settingsState.settings.dnsProtocol != DnsProtocol.SYSTEM) {
Column {
LabelledDropdown(
title = {
Text(
text = stringResource(R.string.dns_provider),
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface
),
)
},
leading = { Icon(Icons.Outlined.Cloud, contentDescription = null) },
currentValue =
settingsState.settings.dnsEndpoint?.let { DnsProvider.fromAddress(it) }
?: DnsProvider.CLOUDFLARE,
onSelected = { selected -> selected?.let { viewModel.setDnsProvider(it) } },
options = DnsProvider.entries,
optionToString = { it?.name ?: DnsProvider.CLOUDFLARE.name },
title = stringResource(R.string.dns_protocol),
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
currentValue = settingsState.settings.dnsProtocol,
onSelected = { selected -> selected?.let { viewModel.setDnsProtocol(it) } },
options = DnsProtocol.entries,
optionToString = { (it ?: DnsProtocol.SYSTEM).asString(context) },
)
AnimatedVisibility(settingsState.settings.dnsProtocol != DnsProtocol.SYSTEM) {
LabelledDropdown(
title = stringResource(R.string.dns_provider),
leading = { Icon(Icons.Outlined.Cloud, contentDescription = null) },
currentValue =
settingsState.settings.dnsEndpoint?.let { DnsProvider.fromAddress(it) }
?: DnsProvider.CLOUDFLARE,
onSelected = { selected -> selected?.let { viewModel.setDnsProvider(it) } },
options = DnsProvider.entries,
optionToString = { it?.name ?: DnsProvider.CLOUDFLARE.name },
)
}
}
}
}
@@ -1,11 +1,15 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals
import android.R.attr.onClick
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.rememberScrollState
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.Settings
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -13,10 +17,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.components.globalConfigItem
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components.splitTunnelingItem
@Composable
fun TunnelGlobalsScreen(globalTunnelId: Int) {
@@ -24,17 +26,22 @@ fun TunnelGlobalsScreen(globalTunnelId: Int) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier =
Modifier.verticalScroll(rememberScrollState()).fillMaxSize().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.verticalScroll(rememberScrollState()).fillMaxSize(),
) {
SurfaceSelectionGroupButton(
listOf(
globalConfigItem { navController.push(Route.ConfigGlobal(globalTunnelId)) },
splitTunnelingItem(stringResource(R.string.splt_tunneling)) {
navController.push(Route.SplitTunnelGlobal(id = globalTunnelId))
},
Column {
SurfaceRow(
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
title = stringResource(R.string.configuration),
onClick = { navController.push(Route.ConfigGlobal(globalTunnelId)) },
)
)
SurfaceRow(
leading = {
Icon(Icons.AutoMirrored.Outlined.CallSplit, contentDescription = null)
},
title = stringResource(R.string.splt_tunneling),
onClick = { navController.push(Route.SplitTunnelGlobal(id = globalTunnelId)) },
)
}
}
}
@@ -1,24 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
@Composable
fun globalConfigItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
title = {
SelectionItemLabel(stringResource(R.string.configuration), SelectionLabelType.TITLE)
},
trailing = { ForwardButton { onClick() } },
onClick = onClick,
)
}
@@ -7,9 +7,12 @@ 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.NetworkPing
import androidx.compose.material.icons.outlined.QueryStats
import androidx.compose.material.icons.outlined.Replay
import androidx.compose.material.icons.outlined.Timer
import androidx.compose.material.icons.outlined.TimerOff
import androidx.compose.material.icons.outlined.ViewHeadline
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -22,40 +25,46 @@ 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.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.components.detailedPingStatsItem
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.components.enablePingMonitoringItem
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
@Composable
fun TunnelMonitoringScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val navController = LocalNavController.current
val settingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (settingsState.isLoading) return
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier =
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 16.dp),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
SurfaceSelectionGroupButton(
listOf(
enablePingMonitoringItem(settingsState.settings.isPingEnabled) {
viewModel.setPingEnabled(it)
}
Column {
GroupLabel(
stringResource(R.string.ping),
modifier = Modifier.padding(horizontal = 16.dp),
)
)
if (settingsState.settings.isPingEnabled) {
LabelledDropdown(
title = {
Text(
text = stringResource(R.string.tunnel_ping_interval),
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface
),
SurfaceRow(
leading = { Icon(Icons.Outlined.NetworkPing, contentDescription = null) },
title = stringResource(R.string.ping_monitor),
trailing = {
ScaledSwitch(
checked = settingsState.showDetailedPingStats,
onClick = { viewModel.setPingEnabled(it) },
)
},
onClick = { viewModel.setPingEnabled(!settingsState.settings.isPingEnabled) },
)
LabelledDropdown(
title = stringResource(R.string.tunnel_ping_interval),
leading = { Icon(Icons.Outlined.Timer, contentDescription = null) },
currentValue = settingsState.settings.tunnelPingIntervalSeconds,
onSelected = { selected ->
@@ -65,15 +74,7 @@ fun TunnelMonitoringScreen(viewModel: SettingsViewModel = hiltViewModel()) {
optionToString = { it?.toString() ?: stringResource(R.string._default) },
)
LabelledDropdown(
title = {
Text(
text = stringResource(R.string.attempts_per_interval),
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface
),
)
},
title = stringResource(R.string.attempts_per_interval),
leading = { Icon(Icons.Outlined.Replay, contentDescription = null) },
currentValue = settingsState.settings.tunnelPingAttempts,
onSelected = { selected -> selected?.let { viewModel.setTunnelPingAttempts(it) } },
@@ -81,15 +82,7 @@ fun TunnelMonitoringScreen(viewModel: SettingsViewModel = hiltViewModel()) {
optionToString = { it?.toString() ?: stringResource(R.string._default) },
)
LabelledDropdown(
title = {
Text(
text = stringResource(R.string.ping_timeout),
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface
),
)
},
title = stringResource(R.string.ping_timeout),
leading = { Icon(Icons.Outlined.TimerOff, contentDescription = null) },
currentValue = settingsState.settings.tunnelPingTimeoutSeconds,
description = {
@@ -107,12 +100,35 @@ fun TunnelMonitoringScreen(viewModel: SettingsViewModel = hiltViewModel()) {
options = (10..20).toList() + null,
optionToString = { it?.toString() ?: stringResource(R.string._default) },
)
SurfaceSelectionGroupButton(
listOf(
detailedPingStatsItem(settingsState.showDetailedPingStats) {
viewModel.setDetailedPingStats(it)
}
)
SurfaceRow(
leading = { Icon(Icons.Outlined.QueryStats, contentDescription = null) },
title = stringResource(R.string.display_detailed_ping_stats),
trailing = {
ScaledSwitch(
checked = settingsState.showDetailedPingStats,
onClick = { viewModel.setDetailedPingStats(it) },
)
},
onClick = { viewModel.setDetailedPingStats(!settingsState.showDetailedPingStats) },
)
}
Column {
GroupLabel(
stringResource(R.string.other),
modifier = Modifier.padding(horizontal = 16.dp),
)
SurfaceRow(
leading = { Icon(Icons.Outlined.ViewHeadline, contentDescription = null) },
title = stringResource(R.string.local_logging),
description = { DescriptionText(stringResource(R.string.enable_local_logging)) },
trailing = { modifier ->
SwitchWithDivider(
checked = settingsState.isLocalLoggingEnabled,
onClick = { viewModel.setLocalLogging(it) },
modifier = modifier,
)
},
onClick = { navController.push(Route.Logs) },
)
}
}
@@ -1,31 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.QueryStats
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun detailedPingStatsItem(
detailedPingStatsEnabled: Boolean,
onChange: (checked: Boolean) -> Unit,
): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.QueryStats, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.display_detailed_ping_stats),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = { ScaledSwitch(checked = detailedPingStatsEnabled, onClick = onChange) },
onClick = { onChange(!detailedPingStatsEnabled) },
)
}
@@ -1,31 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun enablePingMonitoringItem(
isPingEnabled: Boolean,
onChange: (checked: Boolean) -> Unit,
): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.NetworkPing, contentDescription = null) },
trailing = { ScaledSwitch(checked = isPingEnabled, onClick = onChange) },
title = {
Text(
text = stringResource(R.string.monitoring_ping),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { onChange(!isPingEnabled) },
)
}
@@ -26,10 +26,7 @@ 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.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
@@ -100,82 +97,68 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.fillMaxSize(),
) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
leading = { Icon(Icons.Outlined.Forward5, contentDescription = null) },
title = {
SelectionItemLabel(
stringResource(R.string.socks_5_proxy),
SelectionLabelType.TITLE,
)
},
trailing = {
ScaledSwitch(checked = socks5Enabled, onClick = { socks5Enabled = it })
},
onClick = { socks5Enabled = !socks5Enabled },
)
)
)
if (socks5Enabled) {
ConfigurationTextBox(
modifier = Modifier.padding(horizontal = 12.dp),
hint =
stringResource(
R.string.defaults_to_template,
AppProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
),
label = stringResource(R.string.socks_5_bind_address),
value = socksBindAddress,
isError = proxySettingsState.isSocks5BindAddressError,
onValueChange = {
if (proxySettingsState.isSocks5BindAddressError)
viewModel.clearSocks5BindError()
socksBindAddress = it
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SurfaceRow(
leading = { Icon(Icons.Outlined.Forward5, contentDescription = null) },
title = stringResource(R.string.socks_5_proxy),
trailing = {
ScaledSwitch(checked = socks5Enabled, onClick = { socks5Enabled = it })
},
onClick = { socks5Enabled = !socks5Enabled },
)
if (socks5Enabled) {
ConfigurationTextBox(
modifier = Modifier.padding(horizontal = 16.dp),
hint =
stringResource(
R.string.defaults_to_template,
AppProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
),
label = stringResource(R.string.socks_5_bind_address),
value = socksBindAddress,
isError = proxySettingsState.isSocks5BindAddressError,
onValueChange = {
if (proxySettingsState.isSocks5BindAddressError)
viewModel.clearSocks5BindError()
socksBindAddress = it
},
)
}
}
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
leading = { Icon(Icons.Outlined.Http, contentDescription = null) },
title = {
SelectionItemLabel(
stringResource(R.string.http_proxy),
SelectionLabelType.TITLE,
)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SurfaceRow(
leading = { Icon(Icons.Outlined.Http, contentDescription = null) },
title = stringResource(R.string.http_proxy),
trailing = { ScaledSwitch(checked = httpEnabled, onClick = { httpEnabled = it }) },
onClick = { httpEnabled = !httpEnabled },
)
if (httpEnabled) {
ConfigurationTextBox(
hint =
stringResource(
R.string.defaults_to_template,
AppProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
),
label = stringResource(R.string.http_bind_address),
value = httpBindAddress,
isError = proxySettingsState.isHttpBindAddressError,
onValueChange = {
if (proxySettingsState.isSocks5BindAddressError)
viewModel.clearHttpBindError()
httpBindAddress = it
},
trailing = {
ScaledSwitch(checked = httpEnabled, onClick = { httpEnabled = it })
},
onClick = { httpEnabled = !httpEnabled },
modifier = Modifier.padding(horizontal = 12.dp),
)
)
)
if (httpEnabled) {
ConfigurationTextBox(
hint =
stringResource(
R.string.defaults_to_template,
AppProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
),
label = stringResource(R.string.http_bind_address),
value = httpBindAddress,
isError = proxySettingsState.isHttpBindAddressError,
onValueChange = {
if (proxySettingsState.isSocks5BindAddressError) viewModel.clearHttpBindError()
httpBindAddress = it
},
modifier = Modifier.padding(horizontal = 12.dp),
)
}
}
if (socks5Enabled || httpEnabled) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
modifier = Modifier.padding(horizontal = 16.dp),
) {
GroupLabel(
stringResource(
@@ -194,7 +177,6 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
hint = "",
keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions,
modifier = Modifier.padding(horizontal = 12.dp),
)
ConfigurationTextBox(
value = proxyPassword,
@@ -207,7 +189,6 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) {
hint = "",
keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions,
modifier = Modifier.padding(horizontal = 12.dp),
trailing = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
@@ -0,0 +1,124 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.system
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.filled.AppShortcut
import androidx.compose.material.icons.filled.SmartToy
import androidx.compose.material.icons.outlined.AdminPanelSettings
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
import androidx.compose.runtime.getValue
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.style.TextOverflow
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.util.extensions.launchVpnSettings
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
@Composable
fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val context = LocalContext.current
val settingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val clipboard = rememberClipboardHelper()
SecureScreenFromRecording()
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
SurfaceRow(
leading = { Icon(Icons.Outlined.AdminPanelSettings, contentDescription = null) },
title = stringResource(R.string.native_kill_switch),
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
onClick = { context.launchVpnSettings() },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Restore, contentDescription = null) },
trailing = {
ScaledSwitch(
checked = settingsState.settings.isRestoreOnBootEnabled,
onClick = { viewModel.setRestoreOnBootEnabled(it) },
)
},
title = stringResource(R.string.restart_at_boot),
onClick = {
viewModel.setRestoreOnBootEnabled(!settingsState.settings.isRestoreOnBootEnabled)
},
)
SurfaceRow(
leading = { Icon(Icons.Outlined.VpnLock, contentDescription = null) },
trailing = {
ScaledSwitch(
checked = settingsState.settings.isAlwaysOnVpnEnabled,
onClick = { viewModel.setAlwaysOnVpnEnabled(it) },
)
},
title = stringResource(R.string.always_on_vpn_support),
onClick = {
viewModel.setAlwaysOnVpnEnabled(!settingsState.settings.isAlwaysOnVpnEnabled)
},
)
SurfaceRow(
leading = { Icon(Icons.Filled.AppShortcut, contentDescription = null) },
trailing = {
ScaledSwitch(
checked = settingsState.settings.isShortcutsEnabled,
onClick = { viewModel.setShortcutsEnabled(it) },
)
},
title = stringResource(R.string.enabled_app_shortcuts),
onClick = { viewModel.setShortcutsEnabled(!settingsState.settings.isShortcutsEnabled) },
)
SurfaceRow(
leading = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
trailing = {
ScaledSwitch(
checked = settingsState.isRemoteEnabled,
onClick = { viewModel.setRemoteEnabled(it) },
)
},
description = {
settingsState.remoteKey?.let { key ->
AnimatedVisibility(visible = settingsState.isRemoteEnabled) {
Text(
text = stringResource(R.string.remote_key_template, key),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.clickable { clipboard.copy(key) },
)
}
}
},
title = stringResource(R.string.enable_remote_app_control),
onClick = { viewModel.setRemoteEnabled(!settingsState.isRemoteEnabled) },
)
}
}
@@ -1,70 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.system
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.components.*
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
@Composable
fun SystemFeaturesScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val settingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val isTv = LocalIsAndroidTV.current
SecureScreenFromRecording()
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier =
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 16.dp),
) {
SurfaceSelectionGroupButton(buildList { if (!isTv) add(nativeKillSwitchItem()) })
SectionDivider()
SurfaceSelectionGroupButton(
buildList {
add(
restartAtBootItem(settingsState.settings.isRestoreOnBootEnabled) {
viewModel.setRestoreOnBootEnabled(it)
}
)
if (!isTv)
add(
alwaysOnVpnItem(settingsState.settings.isAlwaysOnVpnEnabled) {
viewModel.setAlwaysOnVpnEnabled(it)
}
)
}
)
SectionDivider()
SurfaceSelectionGroupButton(
buildList {
add(
appShortcutsItem(settingsState.settings.isShortcutsEnabled) {
viewModel.setShortcutsEnabled(it)
}
)
add(
remoteControlItem(settingsState.isRemoteEnabled, settingsState.remoteKey) {
viewModel.setRemoteEnabled(it)
}
)
}
)
}
}
@@ -1,38 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.components
import androidx.compose.material.icons.Icons
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
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun alwaysOnVpnItem(checked: Boolean, onChange: (checked: Boolean) -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.VpnLock, contentDescription = null) },
trailing = {
ScaledSwitch(
// enabled =
// !((uiState.generalSettings.isTunnelOnWifiEnabled ||
// uiState.generalSettings.isTunnelOnEthernetEnabled ||
// uiState.generalSettings.isTunnelOnMobileDataEnabled) &&
// uiState.generalSettings.isAutoTunnelEnabled),
checked = checked,
onClick = onChange,
)
},
title = {
Text(
text = stringResource(R.string.always_on_vpn_support),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { onChange(!checked) },
)
}
@@ -1,28 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AppShortcut
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun appShortcutsItem(checked: Boolean, onChange: (checked: Boolean) -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Filled.AppShortcut, contentDescription = null) },
trailing = { ScaledSwitch(checked = checked, onClick = onChange) },
title = {
Text(
text = stringResource(R.string.enabled_app_shortcuts),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { onChange(!checked) },
)
}
@@ -1,31 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AdminPanelSettings
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.LaunchButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
@Composable
fun nativeKillSwitchItem(): SelectionItem {
val context = LocalContext.current
return SelectionItem(
leading = { Icon(Icons.Outlined.AdminPanelSettings, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.native_kill_switch),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = { LaunchButton { context.launchVpnSettings() } },
onClick = { context.launchVpnSettings() },
)
}
@@ -1,55 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SmartToy
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
@Composable
fun remoteControlItem(
checked: Boolean,
key: String?,
onChange: (checked: Boolean) -> Unit,
): SelectionItem {
val clipboardManager = rememberClipboardHelper()
return SelectionItem(
leading = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
trailing = { ScaledSwitch(checked = checked, onClick = onChange) },
description = {
key?.let { key ->
AnimatedVisibility(visible = checked) {
Text(
text = stringResource(R.string.remote_key_template, key),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.clickable { clipboardManager.copy(key) },
)
}
}
},
title = {
Text(
text = stringResource(R.string.enable_remote_app_control),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { onChange(!checked) },
)
}
@@ -1,28 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun restartAtBootItem(enabled: Boolean, onChange: (checked: Boolean) -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.Restore, contentDescription = null) },
trailing = { ScaledSwitch(checked = enabled, onClick = onChange) },
title = {
Text(
text = stringResource(R.string.restart_at_boot),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { onChange(!enabled) },
)
}
@@ -1,144 +1,203 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support
import androidx.compose.foundation.layout.*
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.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.*
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.graphics.StrokeCap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
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.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.ContactSupportOptions
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.DonateSection
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.GeneralSupportOptions
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateSection
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.PermissionDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateDialog
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.canInstallPackages
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.requestInstallPackagesPermission
import com.zaneschepke.wireguardautotunnel.util.extensions.*
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
@Composable
fun SupportScreen(viewModel: SupportViewModel = hiltViewModel()) {
val context = LocalContext.current
val navController = LocalNavController.current
val supportState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val clipboardManager = rememberClipboardHelper()
val version = remember {
"v${BuildConfig.VERSION_NAME +
if(BuildConfig.DEBUG) "-debug" else "" }"
}
var showPermissionDialog by rememberSaveable { mutableStateOf(false) }
if (supportState.appUpdate != null) {
InfoDialog(
onDismiss = { viewModel.dismissUpdate() },
onAttest = {
if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR) {
supportState.appUpdate?.apkUrl?.let { context.openWebUrl(it) }
return@InfoDialog
}
if (context.canInstallPackages()) {
viewModel.downloadAndInstall()
} else {
showPermissionDialog = true
}
},
title = { Text(stringResource(R.string.update_available)) },
body = {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier = Modifier.fillMaxWidth(),
) {
val annotatedString = buildAnnotatedString {
append("${supportState.appUpdate?.version ?: ""}\n")
// Add clickable text for second line
withLink(
link =
LinkAnnotation.Clickable(
tag = stringResource(id = R.string.release_notes),
linkInteractionListener = { viewModel.viewReleaseNotes() },
styles =
TextLinkStyles(
style =
SpanStyle(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline,
)
),
)
) {
append(stringResource(R.string.release_notes))
}
}
Text(text = annotatedString)
if (supportState.isLoading) {
LinearProgressIndicator(
progress = { supportState.downloadProgress },
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
color = MaterialTheme.colorScheme.primary,
strokeCap = StrokeCap.Round,
)
}
}
},
confirmText = {
Text(
if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR)
stringResource(R.string.download)
else stringResource(R.string.download_and_install)
)
},
UpdateDialog(
viewModel = viewModel,
context = context,
onPermissionNeeded = { showPermissionDialog = true },
)
}
if (showPermissionDialog) {
InfoDialog(
onDismiss = { showPermissionDialog = false },
onAttest = {
context.requestInstallPackagesPermission()
showPermissionDialog = false
},
title = { Text(stringResource(R.string.permission_required)) },
body = { Text(stringResource(R.string.install_updated_permission)) },
confirmText = { Text(stringResource(R.string.allow)) },
)
PermissionDialog(context = context, onDismiss = { showPermissionDialog = false })
}
Column(
modifier =
Modifier.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
) {
GroupLabel(stringResource(R.string.thank_you), modifier = Modifier.padding(bottom = 12.dp))
UpdateSection { viewModel.checkForUpdate() }
SectionDivider()
DonateSection { navController.push(Route.Donate) }
SectionDivider()
GeneralSupportOptions()
SectionDivider()
ContactSupportOptions(context)
GroupLabel(
stringResource(R.string.thank_you),
modifier = Modifier.padding(horizontal = 16.dp),
MaterialTheme.colorScheme.onSurface,
)
Column {
GroupLabel(stringResource(R.string.resources), Modifier.padding(horizontal = 16.dp))
SurfaceRow(
stringResource(R.string.docs_description),
onClick = { context.openWebUrl(context.getString(R.string.docs_url)) },
leading = { Icon(Icons.Outlined.Book, contentDescription = null) },
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
)
SurfaceRow(
stringResource(R.string.website),
onClick = { context.openWebUrl(context.getString(R.string.website_url)) },
leading = { Icon(Icons.Outlined.Web, contentDescription = null) },
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Favorite, contentDescription = null) },
title = stringResource(R.string.donate),
onClick = { navController.push(Route.Donate) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Balance, contentDescription = null) },
title = stringResource(R.string.licenses),
onClick = { navController.push(Route.License) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Policy, contentDescription = null) },
title = stringResource(R.string.privacy_policy),
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
onClick = { context.openWebUrl(context.getString(R.string.privacy_policy_url)) },
)
if (BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR) {
SurfaceRow(
leading = { Icon(Icons.Outlined.Reviews, contentDescription = null) },
title = stringResource(R.string.review),
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
onClick = { context.launchPlayStoreReview() },
)
}
}
Column {
GroupLabel(
stringResource(R.string.contact),
modifier = Modifier.padding(horizontal = 16.dp),
)
SurfaceRow(
leading = {
Icon(
ImageVector.vectorResource(R.drawable.matrix),
contentDescription = null,
modifier = Modifier.size(24.dp),
)
},
title = stringResource(R.string.join_matrix),
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
onClick = { context.openWebUrl(context.getString(R.string.matrix_url)) },
)
SurfaceRow(
leading = {
Icon(
ImageVector.vectorResource(R.drawable.telegram),
contentDescription = null,
modifier = Modifier.size(24.dp),
)
},
title = stringResource(R.string.join_telegram),
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
onClick = { context.openWebUrl(context.getString(R.string.telegram_url)) },
)
SurfaceRow(
leading = {
Icon(
ImageVector.vectorResource(R.drawable.github),
contentDescription = null,
modifier = Modifier.size(24.dp),
)
},
title = stringResource(R.string.open_issue),
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
onClick = { context.openWebUrl(context.getString(R.string.github_url)) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Mail, contentDescription = null) },
title = stringResource(R.string.email_description),
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
onClick = { context.launchSupportEmail() },
)
}
Column {
GroupLabel(
stringResource(R.string.other),
modifier = Modifier.padding(horizontal = 16.dp),
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Memory, contentDescription = null) },
title = stringResource(R.string.about),
description = {
Column {
DescriptionText(stringResource(R.string.version_template, version))
DescriptionText(
stringResource(R.string.flavor_template, BuildConfig.FLAVOR)
)
}
},
onClick = { clipboardManager.copy(version) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.InstallMobile, contentDescription = null) },
title = stringResource(R.string.check_for_update),
onClick = {
if (BuildConfig.DEBUG)
return@SurfaceRow context.showToast(R.string.update_check_unsupported)
when (BuildConfig.FLAVOR) {
Constants.GOOGLE_PLAY_FLAVOR -> context.launchPlayStoreListing()
Constants.FDROID_FLAVOR -> context.launchFDroidListing()
else -> viewModel.checkForStandaloneUpdate()
}
},
trailing =
if (BuildConfig.FLAVOR == Constants.STANDALONE_FLAVOR) null
else {
{ Icon(Icons.AutoMirrored.Outlined.Launch, null) }
},
)
}
}
}
@@ -1,99 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Mail
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.LaunchButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@Composable
fun ContactSupportOptions(context: android.content.Context) {
SurfaceSelectionGroupButton(
items =
listOf(
SelectionItem(
leading = {
Icon(
ImageVector.vectorResource(R.drawable.matrix),
contentDescription = null,
Modifier.size(iconSize),
)
},
title = {
SelectionItemLabel(
stringResource(R.string.join_matrix),
SelectionLabelType.TITLE,
)
},
trailing = {
LaunchButton { context.openWebUrl(context.getString(R.string.matrix_url)) }
},
onClick = { context.openWebUrl(context.getString(R.string.matrix_url)) },
),
SelectionItem(
leading = {
Icon(
ImageVector.vectorResource(R.drawable.telegram),
contentDescription = null,
Modifier.size(iconSize),
)
},
title = {
SelectionItemLabel(
stringResource(R.string.join_telegram),
SelectionLabelType.TITLE,
)
},
trailing = {
LaunchButton {
context.openWebUrl(context.getString(R.string.telegram_url))
}
},
onClick = { context.openWebUrl(context.getString(R.string.telegram_url)) },
),
SelectionItem(
leading = {
Icon(
ImageVector.vectorResource(R.drawable.github),
contentDescription = null,
Modifier.size(iconSize),
)
},
title = {
SelectionItemLabel(
stringResource(R.string.open_issue),
SelectionLabelType.TITLE,
)
},
trailing = {
LaunchButton { context.openWebUrl(context.getString(R.string.github_url)) }
},
onClick = { context.openWebUrl(context.getString(R.string.github_url)) },
),
SelectionItem(
leading = { Icon(Icons.Outlined.Mail, contentDescription = null) },
title = {
SelectionItemLabel(
stringResource(R.string.email_description),
SelectionLabelType.TITLE,
)
},
trailing = { LaunchButton { context.launchSupportEmail() } },
onClick = { context.launchSupportEmail() },
),
)
)
}
@@ -1,29 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
@Composable
fun DonateSection(onClick: () -> Unit) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
leading = { Icon(Icons.Outlined.Favorite, contentDescription = null) },
title = {
SelectionItemLabel(stringResource(R.string.donate), SelectionLabelType.TITLE)
},
trailing = { ForwardButton { onClick() } },
onClick = onClick,
)
)
)
}
@@ -1,80 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Balance
import androidx.compose.material.icons.outlined.Book
import androidx.compose.material.icons.outlined.Policy
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.LaunchButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@Composable
fun GeneralSupportOptions() {
val context = LocalContext.current
val navController = LocalNavController.current
SurfaceSelectionGroupButton(
items =
buildList {
add(
SelectionItem(
leading = { Icon(Icons.Outlined.Book, contentDescription = null) },
title = {
SelectionItemLabel(
stringResource(R.string.docs_description),
SelectionLabelType.TITLE,
)
},
trailing = {
LaunchButton {
context.openWebUrl(context.getString(R.string.docs_url))
}
},
onClick = { context.openWebUrl(context.getString(R.string.docs_url)) },
)
)
add(
SelectionItem(
leading = { Icon(Icons.Outlined.Policy, contentDescription = null) },
title = {
SelectionItemLabel(
stringResource(R.string.privacy_policy),
SelectionLabelType.TITLE,
)
},
trailing = {
LaunchButton {
context.openWebUrl(context.getString(R.string.privacy_policy_url))
}
},
onClick = {
context.openWebUrl(context.getString(R.string.privacy_policy_url))
},
)
)
add(
SelectionItem(
leading = { Icon(Icons.Outlined.Balance, contentDescription = null) },
title = {
SelectionItemLabel(
stringResource(R.string.licenses),
SelectionLabelType.TITLE,
)
},
trailing = { ForwardButton { navController.push(Route.License) } },
onClick = { navController.push(Route.License) },
)
)
}
)
}
@@ -0,0 +1,23 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
import android.content.Context
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.util.extensions.requestInstallPackagesPermission
@Composable
fun PermissionDialog(context: Context, onDismiss: () -> Unit) {
InfoDialog(
onDismiss = onDismiss,
onAttest = {
context.requestInstallPackagesPermission()
onDismiss()
},
title = { Text(stringResource(R.string.permission_required)) },
body = { Text(stringResource(R.string.install_updated_permission)) },
confirmText = { Text(stringResource(R.string.allow)) },
)
}
@@ -0,0 +1,100 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.canInstallPackages
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun UpdateDialog(viewModel: SupportViewModel, context: Context, onPermissionNeeded: () -> Unit) {
val supportState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
InfoDialog(
onDismiss = { viewModel.dismissUpdate() },
onAttest = {
if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR) {
supportState.appUpdate?.apkUrl?.let { context.openWebUrl(it) }
return@InfoDialog
}
if (context.canInstallPackages()) {
viewModel.downloadAndInstall()
} else {
onPermissionNeeded()
}
},
title = { Text(stringResource(R.string.update_available)) },
body = {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier = Modifier.fillMaxWidth(),
) {
val annotatedString = buildAnnotatedString {
append("${supportState.appUpdate?.version ?: ""}\n")
// Add clickable text for second line
withLink(
link =
LinkAnnotation.Clickable(
tag = stringResource(id = R.string.release_notes),
linkInteractionListener = { viewModel.viewReleaseNotes() },
styles =
TextLinkStyles(
style =
SpanStyle(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline,
)
),
)
) {
append(stringResource(R.string.release_notes))
}
}
Text(text = annotatedString)
if (supportState.isLoading) {
val stroke = Stroke(cap = StrokeCap.Round)
LinearWavyProgressIndicator(
progress = { supportState.downloadProgress },
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
stroke = stroke,
trackStroke = stroke,
)
}
}
},
confirmText = {
Text(
if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR)
stringResource(R.string.download)
else stringResource(R.string.download_and_install)
)
},
)
}
@@ -1,52 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CloudDownload
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
@Composable
fun UpdateSection(onUpdateCheck: () -> Unit) {
val clipboardManager = rememberClipboardHelper()
val version = remember {
"v${BuildConfig.VERSION_NAME +
if(BuildConfig.DEBUG) "-debug" else "" }"
}
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
leading = { Icon(Icons.Outlined.CloudDownload, contentDescription = null) },
title = {
SelectionItemLabel(
stringResource(R.string.check_for_update),
SelectionLabelType.TITLE,
)
},
description = {
Column {
SelectionItemLabel(
stringResource(R.string.version_template, version),
SelectionLabelType.DESCRIPTION,
)
SelectionItemLabel(
stringResource(R.string.flavor_template, BuildConfig.FLAVOR),
SelectionLabelType.DESCRIPTION,
)
}
},
onClick = onUpdateCheck,
onLongPress = { clipboardManager.copy(version) },
)
)
)
}
@@ -4,36 +4,97 @@ 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.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.outlined.CurrencyBitcoin
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.unit.dp
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.components.DonationHeroSection
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.components.DonationOptions
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.components.GoogleDonationMessage
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@Composable
fun DonateScreen() {
val context = LocalContext.current
val navController = LocalNavController.current
val isGoogleFlavor = remember { BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR }
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier =
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
DonationHeroSection()
SectionDivider()
if (BuildConfig.FLAVOR != Constants.GOOGLE_PLAY_FLAVOR) {
DonationOptions { navController.push(Route.Addresses) }
} else {
GoogleDonationMessage()
Column {
GroupLabel(
stringResource(R.string.options),
Modifier.padding(bottom = if (isGoogleFlavor) 8.dp else 0.dp)
.padding(horizontal = 16.dp),
)
if (!isGoogleFlavor) {
SurfaceRow(
leading = { Icon(Icons.Outlined.CurrencyBitcoin, contentDescription = null) },
title = stringResource(R.string.crypto),
onClick = { navController.push(Route.Addresses) },
)
SurfaceRow(
leading = {
Icon(
ImageVector.vectorResource(R.drawable.github),
contentDescription = null,
modifier = Modifier.size(24.dp),
)
},
title = stringResource(R.string.github_sponsors),
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
onClick = {
context.openWebUrl(context.getString(R.string.github_sponsors_url))
},
)
SurfaceRow(
leading = {
Icon(
ImageVector.vectorResource(R.drawable.liberapay),
contentDescription = null,
modifier = Modifier.size(24.dp),
)
},
title = stringResource(R.string.liberapay),
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
onClick = { context.openWebUrl(context.getString(R.string.liberapay_url)) },
)
SurfaceRow(
leading = {
Icon(
ImageVector.vectorResource(R.drawable.kofi),
contentDescription = null,
modifier = Modifier.size(24.dp),
)
},
title = stringResource(R.string.kofi),
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
onClick = { context.openWebUrl(context.getString(R.string.kofi_url)) },
)
} else {
GoogleDonationMessage()
}
}
}
}
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -19,7 +20,7 @@ import com.zaneschepke.wireguardautotunnel.R
fun DonationHeroSection() {
Surface(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
@@ -1,97 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.components
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CurrencyBitcoin
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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 com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.LaunchButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@Composable
fun DonationOptions(onAddressesClick: () -> Unit) {
val context = LocalContext.current
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
leading = {
Icon(
Icons.Outlined.CurrencyBitcoin,
contentDescription = null,
modifier = Modifier.size(iconSize),
)
},
title = {
SelectionItemLabel(stringResource(R.string.crypto), SelectionLabelType.TITLE)
},
trailing = { ForwardButton { onAddressesClick() } },
onClick = onAddressesClick,
),
SelectionItem(
leading = {
Icon(
ImageVector.vectorResource(R.drawable.github),
contentDescription = null,
modifier = Modifier.size(iconSize),
)
},
title = {
SelectionItemLabel(
stringResource(R.string.github_sponsors),
SelectionLabelType.TITLE,
)
},
trailing = {
LaunchButton {
context.openWebUrl(context.getString(R.string.github_sponsors_url))
}
},
onClick = { context.openWebUrl(context.getString(R.string.github_sponsors_url)) },
),
SelectionItem(
leading = {
Icon(
ImageVector.vectorResource(R.drawable.liberapay),
contentDescription = null,
modifier = Modifier.size(iconSize),
)
},
title = {
SelectionItemLabel(stringResource(R.string.liberapay), SelectionLabelType.TITLE)
},
trailing = {
LaunchButton { context.openWebUrl(context.getString(R.string.liberapay_url)) }
},
onClick = { context.openWebUrl(context.getString(R.string.liberapay_url)) },
),
SelectionItem(
leading = {
Icon(
ImageVector.vectorResource(R.drawable.kofi),
contentDescription = null,
modifier = Modifier.size(iconSize),
)
},
title = {
SelectionItemLabel(stringResource(R.string.kofi), SelectionLabelType.TITLE)
},
trailing = {
LaunchButton { context.openWebUrl(context.getString(R.string.kofi_url)) }
},
onClick = { context.openWebUrl(context.getString(R.string.kofi_url)) },
),
)
)
}
@@ -4,9 +4,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -19,24 +16,18 @@ import com.zaneschepke.wireguardautotunnel.R
@Composable
fun GoogleDonationMessage() {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = stringResource(R.string.google_donation_message),
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface
),
textAlign = TextAlign.Start,
)
}
Text(
text = stringResource(R.string.google_donation_message),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onSurface
),
textAlign = TextAlign.Start,
)
}
}
@@ -3,28 +3,22 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.components.addressItem
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.components.AddressItem
@Composable
fun AddressesScreen() {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier =
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 16.dp),
verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
val clipboard = rememberClipboardHelper()
SurfaceSelectionGroupButton(
Address.allAddresses.map { addressItem(it) { address -> clipboard.copy(address) } }
)
Address.allAddresses.forEach { AddressItem(it) { address -> clipboard.copy(address) } }
}
}
@@ -19,23 +19,21 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.Address
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@Composable
fun addressItem(address: Address, onClick: (address: String) -> Unit): SelectionItem {
fun AddressItem(address: Address, onClick: (address: String) -> Unit) {
val context = LocalContext.current
val walletAddress = context.getString(address.address)
var expand by rememberSaveable { mutableStateOf(false) }
return SelectionItem(
SurfaceRow(
leading = {
Image(
painter = painterResource(id = address.icon),
contentDescription = null,
modifier = Modifier.size(iconSize),
modifier = Modifier.size(24.dp),
)
},
trailing = {
@@ -43,7 +41,7 @@ fun addressItem(address: Address, onClick: (address: String) -> Unit): Selection
Icon(Icons.Outlined.CopyAll, contentDescription = null)
}
},
title = { SelectionItemLabel(stringResource(address.name), SelectionLabelType.TITLE) },
title = stringResource(address.name),
description = {
Text(
text = walletAddress,
@@ -1,44 +1,30 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.license
import LicenseFileEntry
import android.content.Context
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components.LicenseList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import com.zaneschepke.wireguardautotunnel.viewmodel.LicenseViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun LicenseScreen() {
val context = LocalContext.current
var licenses by remember { mutableStateOf<List<LicenseFileEntry>>(emptyList()) }
fun LicenseScreen(viewModel: LicenseViewModel = hiltViewModel()) {
val licenseUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { licenses = loadLicenseeJson(context) }
if (licenses.isEmpty()) {
if (licenseUiState.isLoading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularWavyProgressIndicator(waveSpeed = 60.dp, modifier = Modifier.size(48.dp))
}
} else {
LicenseList(licenses)
}
}
suspend fun loadLicenseeJson(context: Context): List<LicenseFileEntry> {
return withContext(Dispatchers.IO) {
val json = Json { ignoreUnknownKeys = true }
val jsonResult = context.assets.open("licenses.json").bufferedReader().use { it.readText() }
json.decodeFromString(jsonResult)
LicenseList(licenseUiState.licenses)
}
}
@@ -1,10 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components
import LicenseFileEntry
import androidx.compose.foundation.focusable
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -12,21 +15,22 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.button.LinkIconButton
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@Composable
fun LicenseList(licenses: List<LicenseFileEntry>) {
val context = LocalContext.current
LazyColumn(
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(licenses) { entry ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier =
Modifier.clickable(enabled = entry.scm?.url != null) {
entry.scm?.url?.let { context.openWebUrl(it) }
}
.padding(horizontal = 16.dp, vertical = 8.dp),
) {
Column(modifier = Modifier.weight(1f)) {
Text(
@@ -42,11 +46,7 @@ fun LicenseList(licenses: List<LicenseFileEntry>) {
)
}
}
entry.scm?.url?.let { scmUrl ->
LinkIconButton(modifier = Modifier.size(20.dp).focusable()) {
context.openWebUrl(scmUrl)
}
}
entry.scm?.url?.let { Icon(Icons.AutoMirrored.Outlined.Launch, null) }
}
}
}
@@ -1,58 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components.MobileDataTunnelItem
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components.WifiTunnelItem
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components.ethernetTunnelItem
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
@Composable
fun TunnelAutoTunnelScreen(viewModel: TunnelViewModel) {
val tunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (tunnelState.isLoading) return
val tunnel = tunnelState.tunnel ?: return
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier =
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 16.dp),
) {
SurfaceSelectionGroupButton(
items =
buildList {
add(
MobileDataTunnelItem(tunnel.isMobileDataTunnel) {
viewModel.setMobileDataTunnel(it)
}
)
add(
ethernetTunnelItem(tunnel.isEthernetTunnel) {
viewModel.setEthernetTunnel(it)
}
)
add(
WifiTunnelItem(
tunnel.tunnelNetworks,
isWildcardsEnabled = tunnelState.isWildcardsEnabled,
onSaveTunnelNetwork = { viewModel.addTunnelNetwork(it) },
onDeleteTunnelNetwork = { viewModel.removeTunnelNetwork(it) },
)
)
}
)
}
}
@@ -1,34 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.PhoneAndroid
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun MobileDataTunnelItem(enabled: Boolean, onClick: (Boolean) -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.PhoneAndroid, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.mobile_tunnel),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
text = stringResource(R.string.mobile_data_tunnel),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = { ScaledSwitch(checked = enabled, onClick = onClick) },
onClick = { onClick(!enabled) },
)
}
@@ -1,70 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
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.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@Composable
fun WifiTunnelItem(
tunnelNetworks: Set<String>,
isWildcardsEnabled: Boolean,
onSaveTunnelNetwork: (String) -> Unit,
onDeleteTunnelNetwork: (String) -> Unit,
): SelectionItem {
var currentText by rememberSaveable { mutableStateOf("") }
LaunchedEffect(tunnelNetworks) { currentText = "" }
return SelectionItem(
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
) {
Icon(
imageVector = Icons.Outlined.Security,
contentDescription = stringResource(R.string.use_tunnel_on_wifi_name),
modifier = Modifier.size(iconSize),
)
Text(
text = stringResource(R.string.use_tunnel_on_wifi_name),
style =
MaterialTheme.typography.bodyMedium.copy(
MaterialTheme.colorScheme.onSurface
),
modifier = Modifier.padding(start = 16.dp),
)
}
},
description = {
TrustedNetworkTextBox(
trustedNetworks = tunnelNetworks,
onDelete = onDeleteTunnelNetwork,
currentText = currentText,
onSave = onSaveTunnelNetwork,
onValueChange = { currentText = it },
supporting = {
if (isWildcardsEnabled) {
WildcardsLabel()
}
},
)
},
)
}
@@ -1,34 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@Composable
fun ethernetTunnelItem(enabled: Boolean, onClick: (Boolean) -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.SettingsEthernet, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.ethernet_tunnel),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
text = stringResource(R.string.set_ethernet_tunnel),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = { ScaledSwitch(checked = enabled, onClick = onClick) },
onClick = { onClick(!enabled) },
)
}
@@ -1,26 +1,32 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.size
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.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelsViewModel
@@ -34,7 +40,6 @@ fun TunnelList(
sharedViewModel: SharedAppViewModel,
) {
val navController = LocalNavController.current
val isTv = LocalIsAndroidTV.current
val context = LocalContext.current
val lazyListState = rememberLazyListState()
@@ -58,7 +63,7 @@ fun TunnelList(
if (tunnelsState.tunnels.isEmpty()) {
item { GettingStartedLabel(onClick = { context.openWebUrl(it) }) }
}
items(tunnelsState.tunnels.toList(), key = { it.id }) { tunnel ->
items(tunnelsState.tunnels, key = { it.id }) { tunnel ->
val tunnelState =
remember(tunnelsState.activeTunnels) {
tunnelsState.activeTunnels[tunnel.id] ?: TunnelState()
@@ -67,35 +72,56 @@ fun TunnelList(
remember(tunnelsState.selectedTunnels) {
tunnelsState.selectedTunnels.any { it.id == tunnel.id }
}
TunnelRowItem(
state = tunnelState,
isSelected = selected,
tunnel = tunnel,
onTvClick = { navController.push(Route.TunnelOptions(tunnel.id)) },
onToggleSelectedTunnel = { tunnel -> viewModel.toggleSelectedTunnel(tunnel.id) },
onSwitchClick = { checked ->
if (checked) sharedViewModel.startTunnel(tunnel)
else sharedViewModel.stopTunnel(tunnel)
var leadingIconColor by
remember(
tunnelState.status,
tunnelState.logHealthState,
tunnelState.pingStates,
tunnelState.statistics,
) {
mutableStateOf(tunnelState.health().asColor())
}
SurfaceRow(
leading = {
Icon(
Icons.Rounded.Circle,
contentDescription = stringResource(R.string.tunnel_monitoring),
tint = leadingIconColor,
modifier = Modifier.size(14.dp),
)
},
title = tunnel.tunName,
onClick = {
if (tunnelsState.selectedTunnels.isNotEmpty()) {
viewModel.toggleSelectedTunnel(tunnel.id)
} else {
navController.push(Route.TunnelOptions(tunnel.id))
viewModel.clearSelectedTunnels()
}
},
selected = selected,
expandedContent =
if (!tunnelState.status.isDown()) {
{
TunnelStatisticsRow(
tunnelState,
tunnelsState.isPingEnabled,
tunnelsState.showPingStats,
)
}
} else null,
onLongClick = { viewModel.toggleSelectedTunnel(tunnel.id) },
trailing = { modifier ->
SwitchWithDivider(
checked = tunnelState.status.isUpOrStarting(),
onClick = { checked ->
if (checked) sharedViewModel.startTunnel(tunnel)
else sharedViewModel.stopTunnel(tunnel)
},
modifier = modifier,
)
},
isTv = isTv,
isPingEnabled = tunnelsState.isPingEnabled,
showDetailedStats = tunnelsState.showPingStats,
modifier =
(if (!isTv)
Modifier.combinedClickable(
onClick = {
if (tunnelsState.selectedTunnels.isNotEmpty()) {
viewModel.toggleSelectedTunnel(tunnel.id)
} else {
navController.push(Route.TunnelOptions(tunnel.id))
viewModel.clearSelectedTunnels()
}
},
onLongClick = { viewModel.toggleSelectedTunnel(tunnel.id) },
interactionSource = remember { MutableInteractionSource() },
indication = null,
)
else Modifier),
)
}
}
@@ -48,7 +48,7 @@ fun TunnelStatisticsRow(
statistics?.let { stats ->
Column(
modifier = Modifier.fillMaxWidth().padding(start = 32.dp, bottom = 10.dp, end = 10.dp),
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp),
horizontalAlignment = Alignment.Start,
) {
@@ -99,7 +99,6 @@ fun TunnelStatisticsRow(
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
// Peer ID row
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
@@ -110,7 +109,6 @@ fun TunnelStatisticsRow(
color = textColor,
)
}
// RX/TX row
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
@@ -140,7 +138,6 @@ fun TunnelStatisticsRow(
Text(formattedTx, style = textStyle, color = textColor)
}
}
// Handshake row
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
@@ -163,7 +160,6 @@ fun TunnelStatisticsRow(
)
}
}
// Ping block (per unique peer key)
AnimatedVisibility(visible = pingState != null && pingEnabled) {
pingState?.let {
val reachableText =
@@ -12,7 +12,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.SecureScreenFromRecording
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material3.*
@@ -12,7 +11,6 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@Composable
fun InterfaceDropdown(
@@ -29,7 +27,7 @@ fun InterfaceDropdown(
onMimicSip: () -> Unit,
) {
Column {
IconButton(modifier = Modifier.size(iconSize), onClick = { onExpandedChange(true) }) {
IconButton(onClick = { onExpandedChange(true) }) {
Icon(
Icons.Rounded.MoreVert,
contentDescription = stringResource(R.string.quick_actions),
@@ -17,7 +17,6 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@Composable
fun PeersSection(
@@ -44,20 +43,14 @@ fun PeersSection(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(
modifier = Modifier.size(iconSize),
onClick = { onRemove(index) },
) {
IconButton(onClick = { onRemove(index) }) {
Icon(
Icons.Rounded.Delete,
contentDescription = stringResource(R.string.delete),
)
}
Column {
IconButton(
modifier = Modifier.size(iconSize),
onClick = { isDropDownExpanded = true },
) {
IconButton(onClick = { isDropDownExpanded = true }) {
Icon(
Icons.Rounded.MoreVert,
contentDescription = stringResource(R.string.quick_actions),
@@ -7,19 +7,24 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.InstalledPackage
import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppListItem(
installedPackage: InstalledPackage,
@@ -37,23 +42,25 @@ fun AppListItem(
}
}
SelectionItemButton(
SurfaceRow(
leading = {
Image(
painter = rememberDrawablePainter(icon),
contentDescription = null,
modifier = Modifier.size(iconSize),
modifier = Modifier.size(24.dp),
)
},
buttonText = installedPackage.name,
description = installedPackage.packageName,
title = installedPackage.name,
description = { DescriptionText(installedPackage.packageName) },
onClick = { onToggle(!isSelected) },
trailing = {
Row(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(checked = isSelected, onCheckedChange = { onToggle(it) })
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
Checkbox(checked = isSelected, onCheckedChange = { onToggle(it) })
}
}
},
)
@@ -68,7 +68,7 @@ fun AppListSection(
label = {},
leading = { Icon(Icons.Outlined.Search, stringResource(R.string.search)) },
containerColor = MaterialTheme.colorScheme.surface,
modifier = Modifier.fillMaxWidth().height(inputHeight),
modifier = Modifier.fillMaxWidth().height(inputHeight).padding(horizontal = 16.dp),
singleLine = true,
keyboardOptions =
KeyboardOptions(
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.compo
import androidx.compose.foundation.background
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.material.icons.Icons
@@ -27,6 +28,7 @@ fun SplitOptionSelector(selectedOption: SplitOption, onOptionChange: (SplitOptio
Modifier.background(color = MaterialTheme.colorScheme.surface)
.fillMaxWidth()
.height(inputHeight)
.padding(horizontal = 16.dp)
) {
SplitOption.entries.forEachIndexed { index, entry ->
val active = selectedOption == entry
@@ -2,8 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.compo
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -21,7 +20,7 @@ fun SplitTunnelContent(
Column(
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
modifier = Modifier.fillMaxSize(),
) {
SplitOptionSelector(
selectedOption = splitConfig.first,
@@ -1,27 +1,49 @@
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.*
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
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
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
@@ -48,42 +70,141 @@ fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier =
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 16.dp),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
SurfaceSelectionGroupButton(
items =
buildList {
add(primaryTunnelItem(tunnel) { viewModel.togglePrimaryTunnel() })
add(autoTunnelingItem(tunnel))
add(
splitTunnelingItem(stringResource(R.string.splt_tunneling)) {
navController.push(Route.SplitTunnel(id = tunnel.id))
}
Column {
SurfaceRow(
leading = { Icon(Icons.Outlined.Star, contentDescription = null) },
title = stringResource(R.string.primary_tunnel),
description = {
Text(
text = stringResource(R.string.set_primary_tunnel),
style =
MaterialTheme.typography.bodySmall.copy(
MaterialTheme.colorScheme.outline
),
)
}
)
SectionDivider()
SurfaceSelectionGroupButton(
items =
buildList {
add(
dynamicDnsItem(tunnel.restartOnPingFailure) {
viewModel.setRestartOnPing(it)
}
},
trailing = {
ScaledSwitch(
checked = tunnel.isPrimaryTunnel,
onClick = { viewModel.togglePrimaryTunnel() },
)
if (tunnelState.appMode != AppMode.KERNEL)
add(
preferIpv6Item(!tunnel.isIpv4Preferred) {
viewModel.toggleIpv4Preferred()
},
onClick = { viewModel.togglePrimaryTunnel() },
)
SurfaceRow(
leading = {
Icon(Icons.AutoMirrored.Outlined.CallSplit, contentDescription = null)
},
title = stringResource(R.string.splt_tunneling),
onClick = { navController.push(Route.SplitTunnel(id = tunnel.id)) },
)
}
Column {
SurfaceRow(
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
title = stringResource(R.string.ddns_auto_update),
description = {
DescriptionText(stringResource(R.string.ddns_auto_update_description))
},
trailing = {
ScaledSwitch(
checked = tunnel.restartOnPingFailure,
onClick = { viewModel.setRestartOnPing(it) },
)
},
onClick = { viewModel.setRestartOnPing(!tunnel.restartOnPingFailure) },
)
SurfaceRow(
leading = {
Icon(ImageVector.vectorResource(R.drawable.host), contentDescription = null)
},
title = stringResource(R.string.prefer_ipv6_resolution),
trailing = {
ScaledSwitch(
checked = !tunnel.isIpv4Preferred,
onClick = { viewModel.toggleIpv4Preferred() },
)
},
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,
)
}
}
},
)
}
)
if (tunnelState.isPingEnabled) {
SectionDivider()
SurfaceSelectionGroupButton(
items = listOf(pingConfigItem(tunnel) { ip -> viewModel.setTunnelPingIp(ip) })
}
},
)
}
}
@@ -1,40 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
@Composable
fun autoTunnelingItem(tunnelConf: TunnelConf): SelectionItem {
val navController = LocalNavController.current
return SelectionItem(
leading = { Icon(Icons.Outlined.Bolt, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.auto_tunneling),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
text = stringResource(R.string.tunnel_specific_settings),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ForwardButton { navController.push(Route.TunnelAutoTunnel(id = tunnelConf.id)) }
},
onClick = { navController.push(Route.TunnelAutoTunnel(id = tunnelConf.id)) },
)
}

Some files were not shown because too many files have changed in this diff Show More