feat: copy split config from existing

This commit is contained in:
Zane Schepke
2025-12-18 16:28:44 -05:00
parent 05b950fbb3
commit 90a693abb4
8 changed files with 161 additions and 34 deletions
@@ -2,18 +2,18 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.backend.Tunnel as WgTunnel
import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.KernelWireguardNotSupported
import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.events.KernelTunnelName
import com.zaneschepke.wireguardautotunnel.domain.events.KernelWireguardNotSupported
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Row
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.outlined.ContentPasteGo
import androidx.compose.material.icons.outlined.CopyAll
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.Icon
@@ -279,12 +280,26 @@ fun currentRouteAsNavbarState(
},
topTitle = tunnelName ?: "",
topTrailing = {
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
Row {
IconButton(
onClick = {
sharedViewModel.postSideEffect(
LocalSideEffect.Modal.SelectTunnel
)
}
) {
Icon(
Icons.Outlined.ContentPasteGo,
stringResource(R.string.copy_from),
)
}
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
showBottomItems = true,
@@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.components.SelectTunnelModal
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.components.SplitTunnelContent
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.state.SplitOption
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
@@ -21,38 +22,57 @@ import org.orbitmvi.orbit.compose.collectSideEffect
@Composable
fun SplitTunnelScreen(viewModel: SplitTunnelViewModel) {
val sharedViewModel = LocalSharedVm.current
val splitTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (splitTunnelState.isLoading) {
val sharedUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
var showDialog by remember { mutableStateOf(false) }
if (uiState.isLoading) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularWavyProgressIndicator(waveSpeed = 60.dp, modifier = Modifier.size(48.dp))
}
return
}
val tunnel = splitTunnelState.tunnel ?: return
val tunnel = uiState.tunnel ?: return
val conf by remember { derivedStateOf { tunnel.toAmConfig() } }
var effectiveTunnel by remember { mutableStateOf(tunnel) }
var splitConfig by remember {
mutableStateOf(
when {
conf.`interface`.excludedApplications.isNotEmpty() ->
Pair(SplitOption.EXCLUDE, conf.`interface`.excludedApplications.toSet())
conf.`interface`.includedApplications.isNotEmpty() ->
Pair(SplitOption.INCLUDE, conf.`interface`.includedApplications.toSet())
else -> Pair(SplitOption.ALL, emptySet<String>())
}
)
}
val conf by remember(effectiveTunnel) { derivedStateOf { effectiveTunnel.toAmConfig() } }
var splitConfig by
remember(conf) {
mutableStateOf(
when {
conf.`interface`.excludedApplications.isNotEmpty() ->
Pair(SplitOption.EXCLUDE, conf.`interface`.excludedApplications.toSet())
conf.`interface`.includedApplications.isNotEmpty() ->
Pair(SplitOption.INCLUDE, conf.`interface`.includedApplications.toSet())
else -> Pair(SplitOption.ALL, emptySet<String>())
}
)
}
sharedViewModel.collectSideEffect { sideEffect ->
if (sideEffect is LocalSideEffect.SaveChanges)
viewModel.saveSplitTunnelSelection(splitConfig)
if (sideEffect is LocalSideEffect.Modal.SelectTunnel) showDialog = true
}
SelectTunnelModal(
showDialog,
sharedUiState.tunnels,
onAttest = { conf ->
if (conf == null) return@SelectTunnelModal
effectiveTunnel = conf
showDialog = false
},
onDismiss = { showDialog = false },
)
SplitTunnelContent(
splitConfig = splitConfig,
installedPackages = splitTunnelState.installedPackages,
installedPackages = uiState.installedPackages,
onSplitOptionChange = { splitConfig = Pair(it, splitConfig.second) },
onAppSelectionToggle = { appPackage, enabled ->
val updated =
@@ -0,0 +1,70 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.components
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.layout.Arrangement
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.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
@Composable
fun SelectTunnelModal(
show: Boolean,
tunnels: List<TunnelConfig>,
onAttest: (tunnelConf: TunnelConfig?) -> Unit,
onDismiss: () -> Unit,
) {
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
if (show) {
InfoDialog(
title = stringResource(R.string.copy_from),
body = {
LazyColumn(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier =
Modifier.pointerInput(Unit) { if (tunnels.isEmpty()) return@pointerInput }
.overscroll(rememberOverscrollEffect()),
state = rememberLazyListState(),
userScrollEnabled = true,
reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(),
) {
items(tunnels, key = { it.id }) { tunnel ->
SurfaceRow(
title = tunnel.name,
trailing =
if (selectedTunnel?.id == tunnel.id) {
{
Icon(
Icons.Outlined.Check,
stringResource(id = R.string.selected),
tint = MaterialTheme.colorScheme.primary,
)
}
} else null,
onClick = { selectedTunnel = tunnel },
)
}
}
},
onAttest = { onAttest(selectedTunnel) },
onDismiss = { onDismiss() },
confirmText = stringResource(R.string.copy),
)
}
}
@@ -3,11 +3,15 @@ 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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.InstalledPackage
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.state.SplitOption
@Composable
@@ -22,16 +26,28 @@ fun SplitTunnelContent(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize(),
) {
SplitOptionSelector(
selectedOption = splitConfig.first,
onOptionChange = onSplitOptionChange,
)
if (splitConfig.first != SplitOption.ALL) {
AppListSection(
installedPackages = installedPackages,
onAppSelectionToggle = onAppSelectionToggle,
splitConfig = splitConfig,
Column {
GroupLabel(
stringResource(R.string.mode),
modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 8.dp),
)
SplitOptionSelector(
selectedOption = splitConfig.first,
onOptionChange = onSplitOptionChange,
)
}
if (splitConfig.first != SplitOption.ALL) {
Column {
GroupLabel(
stringResource(R.string.app_selection),
modifier = Modifier.padding(16.dp).padding(bottom = 8.dp),
)
AppListSection(
installedPackages = installedPackages,
onAppSelectionToggle = onAppSelectionToggle,
splitConfig = splitConfig,
)
}
}
}
}
@@ -19,7 +19,7 @@ sealed class LocalSideEffect {
data object DeleteTunnels : Modal()
data object AddCustomApp : Modal()
data object SelectTunnel : Modal()
}
sealed class SelectedTunnels : LocalSideEffect() {
@@ -4,6 +4,7 @@ import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
@@ -166,7 +167,9 @@ constructor(
postSideEffect(GlobalSideEffect.Snackbar(message))
if (!accepted) return@intent
if (WgQuickBackend.hasKernelSupport())
Timber.i("Device supports kernel backend. WireGuard module is built in, switching to kernel backend.")
Timber.i(
"Device supports kernel backend. WireGuard module is built in, switching to kernel backend."
)
else {
Timber.e("Device does not support kernel backend!")
intent {
+3
View File
@@ -456,4 +456,7 @@
<string name="i3" translatable="false">I3</string>
<string name="i4" translatable="false">I4</string>
<string name="i5" translatable="false">I5</string>
<string name="copy_from">Copy from</string>
<string name="mode">Mode</string>
<string name="app_selection">App selection</string>
</resources>