From 0874db8bbfeda823824e3e97ddeee84ce1570cd9 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Sat, 1 Nov 2025 01:24:19 -0400 Subject: [PATCH] refactor: restart modal on config changes for proxy and tuns --- .../core/tunnel/TunnelManager.kt | 41 +++++++ .../lockdown/LockdownSettingsScreen.kt | 4 +- .../settings/proxy/ProxySettingsScreen.kt | 100 ++++++++++-------- .../ui/screens/tunnels/config/ConfigScreen.kt | 43 ++++++-- .../ui/state/ConfigUiState.kt | 1 + .../ui/state/ProxySettingsUiState.kt | 3 + .../viewmodel/ConfigViewModel.kt | 8 ++ .../viewmodel/LockdownViewModel.kt | 10 +- .../viewmodel/ProxySettingsViewModel.kt | 24 ++++- 9 files changed, 167 insertions(+), 67 deletions(-) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt index 0b1a1e6b..5995a473 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt @@ -397,6 +397,46 @@ constructor( } } + suspend fun restartActiveTunnel(id: Int) = + withContext(ioDispatcher) { + val activeIds = activeTunnels.value.keys.toList() + if (activeIds.isEmpty()) return@withContext + if (!activeIds.contains(id)) return@withContext + val tunnel = tunnelsRepository.getById(id) ?: return@withContext + restartTunnel(tunnel) + } + + suspend fun restartActiveTunnels() = + withContext(ioDispatcher) { + val activeIds = activeTunnels.value.keys.toList() + if (activeIds.isEmpty()) return@withContext + + val tunnels = tunnelsRepository.getAll() + if (tunnels.isEmpty()) return@withContext + + supervisorScope { + activeIds.forEach { id -> + val tunnel = + tunnels.find { it.id == id } + ?: run { + Timber.w("Tunnel config $id not found; skipping restart") + return@forEach + } + restartTunnel(tunnel) + } + } + } + + private suspend fun restartTunnel(tunnel: TunnelConfig) { + runCatching { stopTunnel(tunnel.id) } + .onFailure { e -> Timber.e(e, "Failed to stop tunnel ${tunnel.id} during restart") } + + delay(RESTART_TUNNEL_DELAY) + + runCatching { startTunnel(tunnel) } + .onFailure { e -> Timber.e(e, "Failed to restart tunnel ${tunnel.id}") } + } + private suspend fun handleDynamicDnsMonitoring( activeTuns: Map, configs: List, @@ -513,5 +553,6 @@ constructor( companion object { const val BASE_BACKOFF = 30_000L const val MAX_BACKOFF_TIME = 300_000L + const val RESTART_TUNNEL_DELAY = 300L } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/lockdown/LockdownSettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/lockdown/LockdownSettingsScreen.kt index 10253a25..0e1afe54 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/lockdown/LockdownSettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/lockdown/LockdownSettingsScreen.kt @@ -43,6 +43,8 @@ fun LockdownSettingsScreen(viewModel: LockdownViewModel = hiltViewModel()) { val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() + if (uiState.isLoading) return + var metered by remember { mutableStateOf(uiState.lockdownSettings.metered) } var dualStack by remember { mutableStateOf(uiState.lockdownSettings.dualStack) } var bypassLan by remember { mutableStateOf(uiState.lockdownSettings.bypassLan) } @@ -51,8 +53,6 @@ fun LockdownSettingsScreen(viewModel: LockdownViewModel = hiltViewModel()) { if (it is LocalSideEffect.SaveChanges) viewModel.setShowSaveModal(true) } - if (uiState.isLoading) return - if (uiState.showSaveModal) { InfoDialog( onDismiss = { viewModel.setShowSaveModal(false) }, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/proxy/ProxySettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/proxy/ProxySettingsScreen.kt index 794c2526..d67c6b86 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/proxy/ProxySettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/proxy/ProxySettingsScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.material.icons.outlined.Http import androidx.compose.material.icons.outlined.RemoveRedEye import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -26,71 +27,82 @@ import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch +import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect import com.zaneschepke.wireguardautotunnel.viewmodel.ProxySettingsViewModel +import java.util.Locale import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) { val sharedViewModel = LocalSharedVm.current - val proxySettingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle() - if (proxySettingsState.isLoading) return + val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() - val proxySettings by - remember(proxySettingsState) { mutableStateOf(proxySettingsState.proxySettings) } + if (uiState.isLoading) return + + val locale = remember { Locale.getDefault() } + + val proxySettings by remember(uiState) { mutableStateOf(uiState.proxySettings) } var socks5Enabled by - remember(proxySettings) { - mutableStateOf(proxySettingsState.proxySettings.socks5ProxyEnabled) - } + remember(proxySettings) { mutableStateOf(uiState.proxySettings.socks5ProxyEnabled) } var httpEnabled by - remember(proxySettings) { - mutableStateOf(proxySettingsState.proxySettings.httpProxyEnabled) - } + remember(proxySettings) { mutableStateOf(uiState.proxySettings.httpProxyEnabled) } var socksBindAddress by remember(proxySettings) { - mutableStateOf(proxySettingsState.proxySettings.socks5ProxyBindAddress ?: "") + mutableStateOf(uiState.proxySettings.socks5ProxyBindAddress ?: "") } var httpBindAddress by - remember(proxySettings) { - mutableStateOf(proxySettingsState.proxySettings.httpProxyBindAddress ?: "") - } + remember(proxySettings) { mutableStateOf(uiState.proxySettings.httpProxyBindAddress ?: "") } var proxyUsername by - remember(proxySettings) { - mutableStateOf(proxySettingsState.proxySettings.proxyUsername ?: "") - } + remember(proxySettings) { mutableStateOf(uiState.proxySettings.proxyUsername ?: "") } var proxyPassword by - remember(proxySettings) { - mutableStateOf(proxySettingsState.proxySettings.proxyPassword ?: "") - } - var passwordVisible by - remember(proxySettings) { mutableStateOf(proxySettingsState.passwordVisible) } + remember(proxySettings) { mutableStateOf(uiState.proxySettings.proxyPassword ?: "") } + var passwordVisible by remember(proxySettings) { mutableStateOf(uiState.passwordVisible) } val keyboardController = LocalSoftwareKeyboardController.current val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) + fun saveChanges() { + viewModel.save( + ProxySettings( + socks5ProxyEnabled = socks5Enabled, + socks5ProxyBindAddress = socksBindAddress, + httpProxyEnabled = httpEnabled, + httpProxyBindAddress = httpBindAddress, + proxyUsername = proxyUsername, + proxyPassword = proxyPassword, + ) + ) + } + sharedViewModel.collectSideEffect { sideEffect -> - when (sideEffect) { - LocalSideEffect.SaveChanges -> { - viewModel.save( - ProxySettings( - socks5ProxyEnabled = socks5Enabled, - socks5ProxyBindAddress = socksBindAddress, - httpProxyEnabled = httpEnabled, - httpProxyBindAddress = httpBindAddress, - proxyUsername = proxyUsername, - proxyPassword = proxyPassword, + if (sideEffect is LocalSideEffect.SaveChanges) { + if (uiState.activeTuns.isNotEmpty()) viewModel.setShowSaveModal(true) else saveChanges() + } + } + + if (uiState.showSaveModal) { + InfoDialog( + onDismiss = { viewModel.setShowSaveModal(false) }, + onAttest = { saveChanges() }, + title = stringResource(R.string.save_changes), + body = { + Text( + stringResource( + R.string.restart_message_template, + stringResource(R.string.tunnels).lowercase(locale), ) ) - } - else -> Unit - } + }, + confirmText = stringResource(R.string._continue), + ) } SecureScreenFromRecording() @@ -119,10 +131,9 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) { ), label = stringResource(R.string.socks_5_bind_address), value = socksBindAddress, - isError = proxySettingsState.isSocks5BindAddressError, + isError = uiState.isSocks5BindAddressError, onValueChange = { - if (proxySettingsState.isSocks5BindAddressError) - viewModel.clearSocks5BindError() + if (uiState.isSocks5BindAddressError) viewModel.clearSocks5BindError() socksBindAddress = it }, ) @@ -144,10 +155,9 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) { ), label = stringResource(R.string.http_bind_address), value = httpBindAddress, - isError = proxySettingsState.isHttpBindAddressError, + isError = uiState.isHttpBindAddressError, onValueChange = { - if (proxySettingsState.isSocks5BindAddressError) - viewModel.clearHttpBindError() + if (uiState.isSocks5BindAddressError) viewModel.clearHttpBindError() httpBindAddress = it }, modifier = Modifier.padding(horizontal = 12.dp), @@ -169,11 +179,11 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) { ConfigurationTextBox( value = proxyUsername, onValueChange = { - if (proxySettingsState.isUserNameError) viewModel.clearUsernameError() + if (uiState.isUserNameError) viewModel.clearUsernameError() proxyUsername = it }, label = stringResource(R.string.username), - isError = proxySettingsState.isUserNameError, + isError = uiState.isUserNameError, hint = "", keyboardActions = keyboardActions, keyboardOptions = keyboardOptions, @@ -181,11 +191,11 @@ fun ProxySettingsScreen(viewModel: ProxySettingsViewModel = hiltViewModel()) { ConfigurationTextBox( value = proxyPassword, onValueChange = { - if (proxySettingsState.isUserNameError) viewModel.clearPasswordError() + if (uiState.isUserNameError) viewModel.clearPasswordError() proxyPassword = it }, label = stringResource(R.string.password), - isError = proxySettingsState.isPasswordError, + isError = uiState.isPasswordError, hint = "", keyboardActions = keyboardActions, keyboardOptions = keyboardOptions, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/ConfigScreen.kt index 2269eab8..21f6e690 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/config/ConfigScreen.kt @@ -5,14 +5,18 @@ 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.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 androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm +import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.security.SecureScreenFromRecording import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.AddPeerButton import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.InterfaceSection @@ -21,33 +25,50 @@ import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel +import java.util.Locale import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun ConfigScreen(viewModel: ConfigViewModel) { val sharedViewModel = LocalSharedVm.current - val configUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() + val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() - if (configUiState.isLoading) return + if (uiState.isLoading) return + + val locale = remember { Locale.getDefault() } var configProxy by remember { - mutableStateOf( - configUiState.tunnel?.let { ConfigProxy.from(it.toAmConfig()) } ?: ConfigProxy() - ) + mutableStateOf(uiState.tunnel?.let { ConfigProxy.from(it.toAmConfig()) } ?: ConfigProxy()) } - var tunnelName by remember { mutableStateOf(configUiState.tunnel?.name ?: "") } + var tunnelName by remember { mutableStateOf(uiState.tunnel?.name ?: "") } val isGlobalConfig = rememberSaveable { tunnelName == TunnelConfig.GLOBAL_CONFIG_NAME } val isTunnelNameTaken by - remember(tunnelName) { - derivedStateOf { configUiState.unavailableNames.contains(tunnelName) } - } + remember(tunnelName) { derivedStateOf { uiState.unavailableNames.contains(tunnelName) } } sharedViewModel.collectSideEffect { sideEffect -> if (sideEffect is LocalSideEffect.SaveChanges) - viewModel.saveConfigProxy(configProxy, tunnelName) + if (uiState.isRunning) viewModel.setShowSaveModal(true) + else viewModel.saveConfigProxy(configProxy, tunnelName) + } + + if (uiState.showSaveModal) { + InfoDialog( + onDismiss = { viewModel.setShowSaveModal(false) }, + onAttest = { viewModel.saveConfigProxy(configProxy, tunnelName) }, + title = stringResource(R.string.save_changes), + body = { + Text( + stringResource( + R.string.restart_message_template, + stringResource(R.string.tunnels).lowercase(locale), + ) + ) + }, + confirmText = stringResource(R.string._continue), + ) } SecureScreenFromRecording() @@ -60,7 +81,7 @@ fun ConfigScreen(viewModel: ConfigViewModel) { InterfaceSection( isGlobalConfig, configProxy = configProxy, - configUiState.isRunning, + uiState.isRunning, tunnelName, isTunnelNameTaken, onInterfaceChange = { configProxy = configProxy.copy(`interface` = it) }, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/ConfigUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/ConfigUiState.kt index 785024f8..af334482 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/ConfigUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/ConfigUiState.kt @@ -7,4 +7,5 @@ data class ConfigUiState( val isLoading: Boolean = true, val tunnel: TunnelConfig? = null, val isRunning: Boolean = false, + val showSaveModal: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/ProxySettingsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/ProxySettingsUiState.kt index 0e62d0f7..1f95308a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/ProxySettingsUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/ProxySettingsUiState.kt @@ -1,13 +1,16 @@ package com.zaneschepke.wireguardautotunnel.ui.state import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings +import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState data class ProxySettingsUiState( val proxySettings: ProxySettings = ProxySettings(), + val activeTuns: Map = emptyMap(), val isSocks5BindAddressError: Boolean = false, val isHttpBindAddressError: Boolean = false, val isUserNameError: Boolean = false, val isPasswordError: Boolean = false, val passwordVisible: Boolean = false, val isLoading: Boolean = true, + val showSaveModal: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/ConfigViewModel.kt index d7fe3c9c..229cb0ba 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/ConfigViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/ConfigViewModel.kt @@ -51,6 +51,7 @@ constructor( } fun saveConfigProxy(configProxy: ConfigProxy, tunnelName: String) = intent { + reduce { state.copy(showSaveModal = false) } if (state.unavailableNames.contains(tunnelName)) return@intent postSideEffect( GlobalSideEffect.Toast(StringValue.StringResource(R.string.tunnel_name_taken)) @@ -72,12 +73,15 @@ constructor( } if (tunnelConfig != null) { tunnelRepository.save(tunnelConfig) + postSideEffect( GlobalSideEffect.Toast( StringValue.StringResource(R.string.config_changes_saved) ) ) postSideEffect(GlobalSideEffect.PopBackStack) + + if (state.isRunning) tunnelManager.restartActiveTunnel(tunnelConfig.id) } } .onFailure { @@ -96,6 +100,10 @@ constructor( globalEffectRepository.post(globalSideEffect) } + fun setShowSaveModal(showSaveModal: Boolean) = intent { + reduce { state.copy(showSaveModal = showSaveModal) } + } + @AssistedFactory interface Factory { fun create(tunnelId: Int?): ConfigViewModel diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/LockdownViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/LockdownViewModel.kt index 68ac2c75..696d1e00 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/LockdownViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/LockdownViewModel.kt @@ -38,6 +38,12 @@ constructor( fun setLockdownSettings(lockdownSettings: LockdownSettings) = intent { reduce { state.copy(showSaveModal = false) } lockdownSettingsRepository.upsert(lockdownSettings) + + postSideEffect(GlobalSideEffect.PopBackStack) + postSideEffect( + GlobalSideEffect.Toast(StringValue.StringResource(R.string.config_changes_saved)) + ) + tunnelManager.setBackendMode(BackendMode.Inactive) val allowedIps = if (lockdownSettings.bypassLan) TunnelConfig.LAN_BYPASS_ALLOWED_IPS else emptySet() @@ -48,10 +54,6 @@ constructor( dualStack = lockdownSettings.dualStack, ) ) - postSideEffect(GlobalSideEffect.PopBackStack) - postSideEffect( - GlobalSideEffect.Toast(StringValue.StringResource(R.string.config_changes_saved)) - ) } suspend fun postSideEffect(globalSideEffect: GlobalSideEffect) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/ProxySettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/ProxySettingsViewModel.kt index 0d85fb43..2bda2c0d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/ProxySettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/ProxySettingsViewModel.kt @@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.viewmodel import androidx.lifecycle.ViewModel import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository @@ -11,6 +12,7 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.extensions.isValidAndroidProxyBindAddress import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlinx.coroutines.flow.combine import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container @@ -20,6 +22,7 @@ class ProxySettingsViewModel constructor( private val proxySettingsRepository: ProxySettingsRepository, private val globalEffectRepository: GlobalEffectRepository, + private val tunnelManager: TunnelManager, ) : ContainerHost, ViewModel() { override val container = @@ -27,12 +30,16 @@ constructor( ProxySettingsUiState(), buildSettings = { repeatOnSubscribedStopTimeout = 5000L }, ) { - proxySettingsRepository.flow.collect { - reduce { state.copy(proxySettings = it, isLoading = false) } - } + combine(tunnelManager.activeTunnels, proxySettingsRepository.flow) { + activeTuns, + settings -> + state.copy(proxySettings = settings, isLoading = false, activeTuns = activeTuns) + } + .collect { reduce { it } } } fun save(proxySettings: ProxySettings) = intent { + reduce { state.copy(showSaveModal = false) } val updated = state.proxySettings.copy( socks5ProxyEnabled = proxySettings.socks5ProxyEnabled, @@ -94,19 +101,22 @@ constructor( ) } } - // Validate password for whitespace + if (updated.proxyPassword?.any { it.isWhitespace() } == true) { postSideEffect( GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.password_no_spaces)) ) return@intent reduce { state.copy(isPasswordError = true) } } - // Save if all validations pass + proxySettingsRepository.upsert(updated) + postSideEffect( GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved)) ) postSideEffect(GlobalSideEffect.PopBackStack) + + if (state.activeTuns.isNotEmpty()) tunnelManager.restartActiveTunnels() } fun clearHttpBindError() = intent { reduce { state.copy(isHttpBindAddressError = false) } } @@ -117,6 +127,10 @@ constructor( fun clearPasswordError() = intent { reduce { state.copy(isPasswordError = false) } } + fun setShowSaveModal(showSaveModal: Boolean) = intent { + reduce { state.copy(showSaveModal = showSaveModal) } + } + suspend fun postSideEffect(globalSideEffect: GlobalSideEffect) { globalEffectRepository.post(globalSideEffect) }