mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
fix: nav crash bugfix
This commit is contained in:
@@ -18,19 +18,17 @@ import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
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 androidx.compose.ui.zIndex
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
@@ -88,7 +86,10 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.*
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
||||
import java.util.*
|
||||
@@ -253,17 +254,11 @@ class MainActivity : AppCompatActivity() {
|
||||
val currentTab by remember {
|
||||
derivedStateOf { Tab.fromRoute(currentRoute ?: Route.Tunnels) }
|
||||
}
|
||||
val selectedCount by
|
||||
rememberSaveable(appState.selectedTunnels) {
|
||||
mutableIntStateOf(appState.selectedTunnels.size)
|
||||
}
|
||||
|
||||
val navState by
|
||||
currentRouteAsNavbarState(
|
||||
appState,
|
||||
viewModel,
|
||||
currentRoute,
|
||||
selectedCount,
|
||||
navController,
|
||||
)
|
||||
|
||||
@@ -302,10 +297,6 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectTapGestures { viewModel.clearSelectedTunnels() }
|
||||
},
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier =
|
||||
@@ -362,15 +353,53 @@ class MainActivity : AppCompatActivity() {
|
||||
entry<Route.Tunnels> { TunnelsScreen() }
|
||||
entry<Route.Sort> { SortScreen() }
|
||||
entry<Route.TunnelOptions> { key ->
|
||||
TunnelOptionsScreen(key.id)
|
||||
val viewModel =
|
||||
hiltViewModel<
|
||||
TunnelViewModel,
|
||||
TunnelViewModel.Factory,
|
||||
>(
|
||||
creationCallback = { factory ->
|
||||
factory.create(key.id)
|
||||
}
|
||||
)
|
||||
TunnelOptionsScreen(viewModel)
|
||||
}
|
||||
entry<Route.SplitTunnel> { key ->
|
||||
SplitTunnelScreen(key.id)
|
||||
val viewModel =
|
||||
hiltViewModel<
|
||||
SplitTunnelViewModel,
|
||||
SplitTunnelViewModel.Factory,
|
||||
>(
|
||||
creationCallback = { factory ->
|
||||
factory.create(key.id)
|
||||
}
|
||||
)
|
||||
SplitTunnelScreen(viewModel)
|
||||
}
|
||||
entry<Route.TunnelAutoTunnel> { key ->
|
||||
TunnelAutoTunnelScreen(key.id)
|
||||
val viewModel =
|
||||
hiltViewModel<
|
||||
TunnelViewModel,
|
||||
TunnelViewModel.Factory,
|
||||
>(
|
||||
creationCallback = { factory ->
|
||||
factory.create(key.id)
|
||||
}
|
||||
)
|
||||
TunnelAutoTunnelScreen(viewModel)
|
||||
}
|
||||
entry<Route.Config> { key ->
|
||||
val viewModel =
|
||||
hiltViewModel<
|
||||
ConfigViewModel,
|
||||
ConfigViewModel.Factory,
|
||||
>(
|
||||
creationCallback = { factory ->
|
||||
factory.create(key.id)
|
||||
}
|
||||
)
|
||||
ConfigScreen(viewModel)
|
||||
}
|
||||
entry<Route.Config> { key -> ConfigScreen(key.id) }
|
||||
entry<Route.LocationDisclosure> {
|
||||
LocationDisclosureScreen()
|
||||
}
|
||||
@@ -393,10 +422,28 @@ class MainActivity : AppCompatActivity() {
|
||||
TunnelGlobalsScreen(key.id)
|
||||
}
|
||||
entry<Route.ConfigGlobal> { key ->
|
||||
ConfigScreen(key.id)
|
||||
val viewModel =
|
||||
hiltViewModel<
|
||||
ConfigViewModel,
|
||||
ConfigViewModel.Factory,
|
||||
>(
|
||||
creationCallback = { factory ->
|
||||
factory.create(key.id)
|
||||
}
|
||||
)
|
||||
ConfigScreen(viewModel)
|
||||
}
|
||||
entry<Route.SplitTunnelGlobal> { key ->
|
||||
SplitTunnelScreen(key.id)
|
||||
val viewModel =
|
||||
hiltViewModel<
|
||||
SplitTunnelViewModel,
|
||||
SplitTunnelViewModel.Factory,
|
||||
>(
|
||||
creationCallback = { factory ->
|
||||
factory.create(key.id)
|
||||
}
|
||||
)
|
||||
SplitTunnelScreen(viewModel)
|
||||
}
|
||||
entry<Route.ProxySettings> { ProxySettingsScreen() }
|
||||
entry<Route.Appearance> { AppearanceScreen() }
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import android.content.Context
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.RootShellUtils
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import io.ktor.utils.io.ioDispatcher
|
||||
import javax.inject.Provider
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
||||
@Module
|
||||
@@ -21,4 +25,13 @@ class ViewModelModule {
|
||||
): FileUtils {
|
||||
return FileUtils(context, ioDispatcher)
|
||||
}
|
||||
|
||||
@ViewModelScoped
|
||||
@Provides
|
||||
fun provideRootShellUtils(
|
||||
@AppShell rootShell: Provider<RootShell>,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
): RootShellUtils {
|
||||
return RootShellUtils(rootShell, ioDispatcher)
|
||||
}
|
||||
}
|
||||
|
||||
+17
-14
@@ -24,13 +24,12 @@ fun currentRouteAsNavbarState(
|
||||
sharedState: SharedAppUiState,
|
||||
sharedViewModel: SharedAppViewModel,
|
||||
route: Route?,
|
||||
selectedCount: Int,
|
||||
navController: NavController,
|
||||
): State<NavbarState> {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val context = LocalContext.current
|
||||
|
||||
return remember(route, selectedCount) {
|
||||
return remember(route, sharedState) {
|
||||
derivedStateOf {
|
||||
when (route) {
|
||||
Route.AdvancedAutoTunnel ->
|
||||
@@ -169,7 +168,7 @@ fun currentRouteAsNavbarState(
|
||||
},
|
||||
)
|
||||
is Route.Config -> {
|
||||
val tunnel = sharedState.tunnels.find { it.id == route.id }
|
||||
val tunnelName = sharedState.tunnelNames[route.id]
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
@@ -177,7 +176,7 @@ fun currentRouteAsNavbarState(
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = tunnel?.tunName ?: context.getString(R.string.new_tunnel),
|
||||
topTitle = tunnelName ?: context.getString(R.string.new_tunnel),
|
||||
topTrailing = {
|
||||
ActionIconButton(Icons.Rounded.Save, R.string.save) {
|
||||
keyboardController?.hide()
|
||||
@@ -187,14 +186,14 @@ fun currentRouteAsNavbarState(
|
||||
)
|
||||
}
|
||||
is Route.SplitTunnel -> {
|
||||
val tunnel = sharedState.tunnels.find { it.id == route.id }
|
||||
val tunnelName = sharedState.tunnelNames[route.id]
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.pop()
|
||||
}
|
||||
},
|
||||
topTitle = tunnel?.tunName ?: "",
|
||||
topTitle = tunnelName ?: "",
|
||||
topTrailing = {
|
||||
ActionIconButton(Icons.Rounded.Save, R.string.save) {
|
||||
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
|
||||
@@ -252,7 +251,7 @@ fun currentRouteAsNavbarState(
|
||||
showBottomItems = true,
|
||||
)
|
||||
is Route.TunnelAutoTunnel -> {
|
||||
val tunnel = sharedState.tunnels.find { it.id == route.id }
|
||||
val tunnelName = sharedState.tunnelNames[route.id]
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
@@ -260,7 +259,7 @@ fun currentRouteAsNavbarState(
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = tunnel?.tunName ?: "",
|
||||
topTitle = tunnelName ?: "",
|
||||
)
|
||||
}
|
||||
Route.TunnelMonitoring ->
|
||||
@@ -274,7 +273,7 @@ fun currentRouteAsNavbarState(
|
||||
showBottomItems = true,
|
||||
)
|
||||
is Route.TunnelOptions -> {
|
||||
val tunnel = sharedState.tunnels.find { it.id == route.id }
|
||||
val tunnelName = sharedState.tunnelNames[route.id]
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
@@ -282,7 +281,7 @@ fun currentRouteAsNavbarState(
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = tunnel?.tunName ?: "",
|
||||
topTitle = tunnelName ?: "",
|
||||
topTrailing = {
|
||||
Row {
|
||||
ActionIconButton(Icons.Rounded.QrCode2, R.string.show_qr) {
|
||||
@@ -299,7 +298,7 @@ fun currentRouteAsNavbarState(
|
||||
NavbarState(
|
||||
topTitle = context.getString(R.string.tunnels),
|
||||
topTrailing = {
|
||||
when (selectedCount) {
|
||||
when (sharedState.selectedTunnelCount) {
|
||||
0 ->
|
||||
Row {
|
||||
ActionIconButton(
|
||||
@@ -320,7 +319,9 @@ fun currentRouteAsNavbarState(
|
||||
Icons.Rounded.SelectAll,
|
||||
R.string.select_all,
|
||||
) {
|
||||
sharedViewModel.toggleSelectAllTunnels()
|
||||
sharedViewModel.postSideEffect(
|
||||
LocalSideEffect.SelectedTunnels.SelectAll
|
||||
)
|
||||
}
|
||||
// due to permissions, and SAF issues on TV, not support
|
||||
// less than Android
|
||||
@@ -336,9 +337,11 @@ fun currentRouteAsNavbarState(
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedCount == 1) {
|
||||
if (sharedState.selectedTunnelCount == 1) {
|
||||
ActionIconButton(Icons.Rounded.CopyAll, R.string.copy) {
|
||||
sharedViewModel.copySelectedTunnel()
|
||||
sharedViewModel.postSideEffect(
|
||||
LocalSideEffect.SelectedTunnels.Copy
|
||||
)
|
||||
}
|
||||
}
|
||||
ActionIconButton(
|
||||
|
||||
+8
-10
@@ -54,28 +54,26 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
|
||||
val showLocationServicesWarning by
|
||||
remember(
|
||||
autoTunnelState.connectivityState?.wifiState,
|
||||
autoTunnelState.generalSettings.trustedNetworkSSIDs,
|
||||
autoTunnelState.generalSettings.wifiDetectionMethod,
|
||||
autoTunnelState.settings.trustedNetworkSSIDs,
|
||||
autoTunnelState.settings.wifiDetectionMethod,
|
||||
) {
|
||||
derivedStateOf {
|
||||
autoTunnelState.connectivityState?.wifiState?.locationServicesEnabled == false &&
|
||||
autoTunnelState.generalSettings.wifiDetectionMethod
|
||||
.needsLocationPermissions() &&
|
||||
autoTunnelState.generalSettings.trustedNetworkSSIDs.isNotEmpty()
|
||||
autoTunnelState.settings.wifiDetectionMethod.needsLocationPermissions() &&
|
||||
autoTunnelState.settings.trustedNetworkSSIDs.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
val showLocationPermissionsWarning by
|
||||
remember(
|
||||
autoTunnelState.connectivityState?.wifiState,
|
||||
autoTunnelState.generalSettings.trustedNetworkSSIDs,
|
||||
autoTunnelState.generalSettings.wifiDetectionMethod,
|
||||
autoTunnelState.settings.trustedNetworkSSIDs,
|
||||
autoTunnelState.settings.wifiDetectionMethod,
|
||||
) {
|
||||
derivedStateOf {
|
||||
autoTunnelState.connectivityState?.wifiState?.locationPermissionsGranted == false &&
|
||||
autoTunnelState.generalSettings.wifiDetectionMethod
|
||||
.needsLocationPermissions() &&
|
||||
autoTunnelState.generalSettings.trustedNetworkSSIDs.isNotEmpty()
|
||||
autoTunnelState.settings.wifiDetectionMethod.needsLocationPermissions() &&
|
||||
autoTunnelState.settings.trustedNetworkSSIDs.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ fun AutoTunnelAdvancedScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
|
||||
leading = { Icon(Icons.Outlined.PauseCircle, null) },
|
||||
onSelected = { selected -> viewModel.setDebounceDelay(selected!!) },
|
||||
options = (0..10).toList(),
|
||||
currentValue = autoTunnelState.generalSettings.debounceDelaySeconds,
|
||||
currentValue = autoTunnelState.settings.debounceDelaySeconds,
|
||||
optionToString = { it?.toString() ?: stringResource(R.string._default) },
|
||||
)
|
||||
}
|
||||
|
||||
+6
-10
@@ -36,7 +36,7 @@ fun networkTunnelingItems(
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = autoTunnelState.generalSettings.isTunnelOnMobileDataEnabled,
|
||||
checked = autoTunnelState.settings.isTunnelOnMobileDataEnabled,
|
||||
onClick = { viewModel.setTunnelOnCellular(it) },
|
||||
)
|
||||
},
|
||||
@@ -58,9 +58,7 @@ fun networkTunnelingItems(
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setTunnelOnCellular(
|
||||
!autoTunnelState.generalSettings.isTunnelOnMobileDataEnabled
|
||||
)
|
||||
viewModel.setTunnelOnCellular(!autoTunnelState.settings.isTunnelOnMobileDataEnabled)
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
@@ -76,7 +74,7 @@ fun networkTunnelingItems(
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = autoTunnelState.generalSettings.isTunnelOnEthernetEnabled,
|
||||
checked = autoTunnelState.settings.isTunnelOnEthernetEnabled,
|
||||
onClick = { viewModel.setTunnelOnEthernet(it) },
|
||||
)
|
||||
},
|
||||
@@ -98,9 +96,7 @@ fun networkTunnelingItems(
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setTunnelOnEthernet(
|
||||
!autoTunnelState.generalSettings.isTunnelOnEthernetEnabled
|
||||
)
|
||||
viewModel.setTunnelOnEthernet(!autoTunnelState.settings.isTunnelOnEthernetEnabled)
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
@@ -123,13 +119,13 @@ fun networkTunnelingItems(
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = autoTunnelState.generalSettings.isStopOnNoInternetEnabled,
|
||||
checked = autoTunnelState.settings.isStopOnNoInternetEnabled,
|
||||
onClick = { viewModel.setStopOnNoInternetEnabled(it) },
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setStopOnNoInternetEnabled(
|
||||
!autoTunnelState.generalSettings.isStopOnNoInternetEnabled
|
||||
!autoTunnelState.settings.isStopOnNoInternetEnabled
|
||||
)
|
||||
},
|
||||
),
|
||||
|
||||
+9
-14
@@ -43,7 +43,7 @@ fun wifiTunnelingItems(
|
||||
|
||||
var currentText by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(autoTunnelState.generalSettings.trustedNetworkSSIDs) { currentText = "" }
|
||||
LaunchedEffect(autoTunnelState.settings.trustedNetworkSSIDs) { currentText = "" }
|
||||
|
||||
val baseItems =
|
||||
listOf(
|
||||
@@ -60,7 +60,7 @@ fun wifiTunnelingItems(
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = autoTunnelState.generalSettings.isTunnelOnWifiEnabled,
|
||||
checked = autoTunnelState.settings.isTunnelOnWifiEnabled,
|
||||
onClick = { viewModel.setAutoTunnelOnWifiEnabled(it) },
|
||||
)
|
||||
},
|
||||
@@ -104,13 +104,13 @@ fun wifiTunnelingItems(
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setAutoTunnelOnWifiEnabled(
|
||||
!autoTunnelState.generalSettings.isTunnelOnWifiEnabled
|
||||
!autoTunnelState.settings.isTunnelOnWifiEnabled
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return if (autoTunnelState.generalSettings.isTunnelOnWifiEnabled) {
|
||||
return if (autoTunnelState.settings.isTunnelOnWifiEnabled) {
|
||||
baseItems +
|
||||
listOf(
|
||||
SelectionItem(
|
||||
@@ -128,9 +128,7 @@ fun wifiTunnelingItems(
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.current_template,
|
||||
autoTunnelState.generalSettings.wifiDetectionMethod.asTitleString(
|
||||
context
|
||||
),
|
||||
autoTunnelState.settings.wifiDetectionMethod.asTitleString(context),
|
||||
),
|
||||
style =
|
||||
MaterialTheme.typography.bodySmall.copy(
|
||||
@@ -160,14 +158,12 @@ fun wifiTunnelingItems(
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = autoTunnelState.generalSettings.isWildcardsEnabled,
|
||||
checked = autoTunnelState.settings.isWildcardsEnabled,
|
||||
onClick = { viewModel.setWildcardsEnabled(it) },
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setWildcardsEnabled(
|
||||
!autoTunnelState.generalSettings.isWildcardsEnabled
|
||||
)
|
||||
viewModel.setWildcardsEnabled(!autoTunnelState.settings.isWildcardsEnabled)
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
@@ -204,14 +200,13 @@ fun wifiTunnelingItems(
|
||||
},
|
||||
description = {
|
||||
TrustedNetworkTextBox(
|
||||
autoTunnelState.generalSettings.trustedNetworkSSIDs,
|
||||
autoTunnelState.settings.trustedNetworkSSIDs,
|
||||
onDelete = { viewModel.removeTrustedNetworkName(it) },
|
||||
currentText = currentText,
|
||||
onSave = { ssid -> viewModel.saveTrustedNetworkName(ssid) },
|
||||
onValueChange = { currentText = it },
|
||||
supporting = {
|
||||
if (autoTunnelState.generalSettings.isWildcardsEnabled)
|
||||
WildcardsLabel()
|
||||
if (autoTunnelState.settings.isWildcardsEnabled) WildcardsLabel()
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
+2
-4
@@ -13,7 +13,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asDescriptionString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
|
||||
@@ -22,7 +21,6 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
||||
@Composable
|
||||
fun WifiDetectionMethodScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
|
||||
val context = LocalContext.current
|
||||
val sharedViewModel = LocalSharedVm.current
|
||||
|
||||
val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -36,8 +34,8 @@ fun WifiDetectionMethodScreen(viewModel: AutoTunnelViewModel = hiltViewModel())
|
||||
val description = it.asDescriptionString(context)
|
||||
IconSurfaceButton(
|
||||
title = title,
|
||||
onClick = { sharedViewModel.setWifiDetectionMethod(it) },
|
||||
selected = autoTunnelState.generalSettings.wifiDetectionMethod == it,
|
||||
onClick = { viewModel.setWifiDetectionMethod(it) },
|
||||
selected = autoTunnelState.settings.wifiDetectionMethod == it,
|
||||
description = description,
|
||||
)
|
||||
}
|
||||
|
||||
+11
-5
@@ -5,6 +5,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.saveable.rememberSaveable
|
||||
@@ -38,7 +39,6 @@ fun TunnelsScreen(viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
val navController = LocalNavController.current
|
||||
val clipboard = rememberClipboardHelper()
|
||||
|
||||
val sharedState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val tunnelsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
var showExportSheet by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -46,11 +46,17 @@ fun TunnelsScreen(viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
var showDeleteModal by rememberSaveable { mutableStateOf(false) }
|
||||
var showUrlDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(tunnelsState.selectedTunnels) {
|
||||
sharedViewModel.setSelectedTunnelCount(tunnelsState.selectedTunnels.size)
|
||||
}
|
||||
|
||||
sharedViewModel.collectSideEffect { sideEffect ->
|
||||
when (sideEffect) {
|
||||
LocalSideEffect.Sheet.ImportTunnels -> showImportSheet = true
|
||||
LocalSideEffect.Modal.DeleteTunnels -> showDeleteModal = true
|
||||
LocalSideEffect.Sheet.ExportTunnels -> showExportSheet = true
|
||||
LocalSideEffect.SelectedTunnels.Copy -> viewModel.copySelectedTunnel()
|
||||
LocalSideEffect.SelectedTunnels.SelectAll -> viewModel.toggleSelectAllTunnels()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
@@ -94,7 +100,7 @@ fun TunnelsScreen(viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
InfoDialog(
|
||||
onDismiss = { showDeleteModal = false },
|
||||
onAttest = {
|
||||
sharedViewModel.deleteSelectedTunnels()
|
||||
viewModel.deleteSelectedTunnels()
|
||||
showDeleteModal = false
|
||||
},
|
||||
title = { Text(text = stringResource(R.string.delete_tunnel)) },
|
||||
@@ -105,11 +111,11 @@ fun TunnelsScreen(viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
|
||||
if (showExportSheet) {
|
||||
ExportTunnelsBottomSheet({ type, uri ->
|
||||
sharedViewModel.exportSelectedTunnels(type, uri)
|
||||
viewModel.exportSelectedTunnels(type, uri)
|
||||
showExportSheet = false
|
||||
}) {
|
||||
showExportSheet = false
|
||||
sharedViewModel.clearSelectedTunnels()
|
||||
viewModel.clearSelectedTunnels()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,5 +146,5 @@ fun TunnelsScreen(viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
)
|
||||
}
|
||||
|
||||
TunnelList(tunnelsState, sharedState, modifier = Modifier.fillMaxSize(), sharedViewModel)
|
||||
TunnelList(tunnelsState, Modifier.fillMaxSize(), viewModel, sharedViewModel)
|
||||
}
|
||||
|
||||
+13
-20
@@ -7,30 +7,23 @@ 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.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.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
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.TunnelsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
|
||||
@Composable
|
||||
fun TunnelAutoTunnelScreen(tunnelId: Int, viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
val tunnelsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
fun TunnelAutoTunnelScreen(viewModel: TunnelViewModel) {
|
||||
val tunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
if (!tunnelsState.stateInitialized) return
|
||||
|
||||
val tunnelConf by
|
||||
remember(tunnelsState.tunnels) {
|
||||
derivedStateOf { tunnelsState.tunnels.find { it.id == tunnelId }!! }
|
||||
}
|
||||
if (tunnelState.isLoading) return
|
||||
val tunnel = tunnelState.tunnel ?: return
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -42,21 +35,21 @@ fun TunnelAutoTunnelScreen(tunnelId: Int, viewModel: TunnelsViewModel = hiltView
|
||||
items =
|
||||
buildList {
|
||||
add(
|
||||
MobileDataTunnelItem(tunnelConf.isMobileDataTunnel) {
|
||||
viewModel.setMobileDataTunnel(tunnelId, it)
|
||||
MobileDataTunnelItem(tunnel.isMobileDataTunnel) {
|
||||
viewModel.setMobileDataTunnel(it)
|
||||
}
|
||||
)
|
||||
add(
|
||||
ethernetTunnelItem(tunnelConf.isEthernetTunnel) {
|
||||
viewModel.setEthernetTunnel(tunnelId, it)
|
||||
ethernetTunnelItem(tunnel.isEthernetTunnel) {
|
||||
viewModel.setEthernetTunnel(it)
|
||||
}
|
||||
)
|
||||
add(
|
||||
WifiTunnelItem(
|
||||
tunnelConf.tunnelNetworks,
|
||||
tunnelsState.isWildcardsEnabled,
|
||||
onSaveTunnelNetwork = { viewModel.addTunnelNetwork(tunnelId, it) },
|
||||
onDeleteTunnelNetwork = { viewModel.removeTunnelNetwork(tunnelId, it) },
|
||||
tunnel.tunnelNetworks,
|
||||
isWildcardsEnabled = tunnelState.isWildcardsEnabled,
|
||||
onSaveTunnelNetwork = { viewModel.addTunnelNetwork(it) },
|
||||
onDeleteTunnelNetwork = { viewModel.removeTunnelNetwork(it) },
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+15
-14
@@ -3,6 +3,7 @@ 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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -19,17 +20,17 @@ import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.SharedAppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelsViewModel
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TunnelList(
|
||||
tunnelsState: TunnelsUiState,
|
||||
sharedState: SharedAppUiState,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: TunnelsViewModel,
|
||||
sharedViewModel: SharedAppViewModel,
|
||||
) {
|
||||
val navController = LocalNavController.current
|
||||
@@ -43,8 +44,10 @@ fun TunnelList(
|
||||
modifier =
|
||||
modifier
|
||||
.pointerInput(Unit) {
|
||||
if (tunnelsState.tunnels.isEmpty()) return@pointerInput
|
||||
sharedViewModel.clearSelectedTunnels()
|
||||
detectTapGestures {
|
||||
if (tunnelsState.tunnels.isEmpty()) return@detectTapGestures
|
||||
viewModel.clearSelectedTunnels()
|
||||
}
|
||||
}
|
||||
.overscroll(rememberOverscrollEffect()),
|
||||
state = lazyListState,
|
||||
@@ -55,23 +58,21 @@ fun TunnelList(
|
||||
if (tunnelsState.tunnels.isEmpty()) {
|
||||
item { GettingStartedLabel(onClick = { context.openWebUrl(it) }) }
|
||||
}
|
||||
items(tunnelsState.tunnels, key = { it.id }) { tunnel ->
|
||||
items(tunnelsState.tunnels.toList(), key = { it.id }) { tunnel ->
|
||||
val tunnelState =
|
||||
remember(tunnelsState.activeTunnels) {
|
||||
tunnelsState.activeTunnels[tunnel.id] ?: TunnelState()
|
||||
}
|
||||
val selected =
|
||||
remember(sharedState.selectedTunnels) {
|
||||
sharedState.selectedTunnels.any { it.id == tunnel.id }
|
||||
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 ->
|
||||
sharedViewModel.toggleSelectedTunnel(tunnel.id)
|
||||
},
|
||||
onToggleSelectedTunnel = { tunnel -> viewModel.toggleSelectedTunnel(tunnel.id) },
|
||||
onSwitchClick = { checked ->
|
||||
if (checked) sharedViewModel.startTunnel(tunnel)
|
||||
else sharedViewModel.stopTunnel(tunnel)
|
||||
@@ -83,14 +84,14 @@ fun TunnelList(
|
||||
(if (!isTv)
|
||||
Modifier.combinedClickable(
|
||||
onClick = {
|
||||
if (sharedState.selectedTunnels.isNotEmpty()) {
|
||||
sharedViewModel.toggleSelectedTunnel(tunnel.id)
|
||||
if (tunnelsState.selectedTunnels.isNotEmpty()) {
|
||||
viewModel.toggleSelectedTunnel(tunnel.id)
|
||||
} else {
|
||||
navController.push(Route.TunnelOptions(tunnel.id))
|
||||
sharedViewModel.clearSelectedTunnels()
|
||||
viewModel.clearSelectedTunnels()
|
||||
}
|
||||
},
|
||||
onLongClick = { sharedViewModel.toggleSelectedTunnel(tunnel.id) },
|
||||
onLongClick = { viewModel.toggleSelectedTunnel(tunnel.id) },
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
)
|
||||
|
||||
+14
-15
@@ -12,6 +12,7 @@ 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
|
||||
@@ -21,36 +22,34 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.
|
||||
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 org.orbitmvi.orbit.compose.collectSideEffect
|
||||
|
||||
@Composable
|
||||
fun ConfigScreen(tunnelId: Int? = null) {
|
||||
val viewModel = LocalSharedVm.current
|
||||
fun ConfigScreen(viewModel: ConfigViewModel) {
|
||||
val sharedViewModel = LocalSharedVm.current
|
||||
|
||||
val tunnelsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val configUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
val tunnelConf by
|
||||
remember(tunnelsState.tunnels) {
|
||||
derivedStateOf { tunnelsState.tunnels.find { it.id == tunnelId } }
|
||||
}
|
||||
if (configUiState.isLoading) return
|
||||
|
||||
var configProxy by remember {
|
||||
mutableStateOf(tunnelConf?.let { ConfigProxy.from(it.toAmConfig()) } ?: ConfigProxy())
|
||||
mutableStateOf(
|
||||
configUiState.tunnel?.let { ConfigProxy.from(it.toAmConfig()) } ?: ConfigProxy()
|
||||
)
|
||||
}
|
||||
|
||||
var tunnelName by remember { mutableStateOf(tunnelConf?.tunName ?: "") }
|
||||
var tunnelName by remember { mutableStateOf(configUiState.tunnel?.tunName ?: "") }
|
||||
val isGlobalConfig = rememberSaveable { tunnelName == TunnelConfig.GLOBAL_CONFIG_NAME }
|
||||
|
||||
val isTunnelNameTaken by
|
||||
remember(tunnelName, tunnelsState.tunnels) {
|
||||
derivedStateOf {
|
||||
tunnelsState.tunnels.any { it.tunName == tunnelName && it.id != tunnelConf?.id }
|
||||
}
|
||||
remember(tunnelName) {
|
||||
derivedStateOf { configUiState.unavailableNames.contains(tunnelName) }
|
||||
}
|
||||
|
||||
viewModel.collectSideEffect { sideEffect ->
|
||||
sharedViewModel.collectSideEffect { sideEffect ->
|
||||
if (sideEffect is LocalSideEffect.SaveChanges)
|
||||
viewModel.saveConfigProxy(tunnelId, configProxy, tunnelName)
|
||||
viewModel.saveConfigProxy(configProxy, tunnelName)
|
||||
}
|
||||
|
||||
SecureScreenFromRecording()
|
||||
|
||||
+5
-9
@@ -9,7 +9,6 @@ import androidx.compose.runtime.*
|
||||
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.LocalSharedVm
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.components.SplitTunnelContent
|
||||
@@ -20,22 +19,19 @@ import org.orbitmvi.orbit.compose.collectSideEffect
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun SplitTunnelScreen(tunnelId: Int, viewModel: SplitTunnelViewModel = hiltViewModel()) {
|
||||
fun SplitTunnelScreen(viewModel: SplitTunnelViewModel) {
|
||||
val sharedViewModel = LocalSharedVm.current
|
||||
val splitTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
if (!splitTunnelState.stateInitialized) {
|
||||
if (splitTunnelState.isLoading) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularWavyProgressIndicator(waveSpeed = 60.dp, modifier = Modifier.size(48.dp))
|
||||
}
|
||||
return
|
||||
}
|
||||
val tunnelConf by
|
||||
remember(splitTunnelState.tunnels) {
|
||||
derivedStateOf { splitTunnelState.tunnels.find { it.id == tunnelId }!! }
|
||||
}
|
||||
val tunnel = splitTunnelState.tunnel ?: return
|
||||
|
||||
val conf by remember { derivedStateOf { tunnelConf.toAmConfig() } }
|
||||
val conf by remember { derivedStateOf { tunnel.toAmConfig() } }
|
||||
|
||||
var splitConfig by remember {
|
||||
mutableStateOf(
|
||||
@@ -51,7 +47,7 @@ fun SplitTunnelScreen(tunnelId: Int, viewModel: SplitTunnelViewModel = hiltViewM
|
||||
|
||||
sharedViewModel.collectSideEffect { sideEffect ->
|
||||
if (sideEffect is LocalSideEffect.SaveChanges)
|
||||
viewModel.saveSplitTunnelSelection(tunnelId, splitConfig)
|
||||
viewModel.saveSplitTunnelSelection(splitConfig)
|
||||
}
|
||||
|
||||
SplitTunnelContent(
|
||||
|
||||
+16
-27
@@ -12,7 +12,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.data.model.AppMode
|
||||
@@ -23,25 +22,18 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelec
|
||||
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.viewmodel.TunnelsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
import org.orbitmvi.orbit.compose.collectSideEffect
|
||||
|
||||
@Composable
|
||||
fun TunnelOptionsScreen(tunnelId: Int, viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
|
||||
val navController = LocalNavController.current
|
||||
val sharedViewModel = LocalSharedVm.current
|
||||
|
||||
val tunnelsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val tunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
if (!tunnelsState.stateInitialized) return
|
||||
|
||||
val tunnelConf by
|
||||
remember(tunnelsState.tunnels) {
|
||||
derivedStateOf { tunnelsState.tunnels.find { it.id == tunnelId }!! }
|
||||
}
|
||||
|
||||
val ipv6Preferred by
|
||||
remember(tunnelConf.isIpv4Preferred) { mutableStateOf(!tunnelConf.isIpv4Preferred) }
|
||||
if (tunnelState.isLoading) return
|
||||
val tunnel = tunnelState.tunnel ?: return
|
||||
|
||||
var showQrModal by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
@@ -50,7 +42,7 @@ fun TunnelOptionsScreen(tunnelId: Int, viewModel: TunnelsViewModel = hiltViewMod
|
||||
}
|
||||
|
||||
if (showQrModal) {
|
||||
QrCodeDialog(tunnelConf = tunnelConf, onDismiss = { showQrModal = false })
|
||||
QrCodeDialog(tunnelConf = tunnel, onDismiss = { showQrModal = false })
|
||||
}
|
||||
|
||||
Column(
|
||||
@@ -62,11 +54,11 @@ fun TunnelOptionsScreen(tunnelId: Int, viewModel: TunnelsViewModel = hiltViewMod
|
||||
SurfaceSelectionGroupButton(
|
||||
items =
|
||||
buildList {
|
||||
add(primaryTunnelItem(tunnelConf) { viewModel.togglePrimaryTunnel(tunnelId) })
|
||||
add(autoTunnelingItem(tunnelConf))
|
||||
add(primaryTunnelItem(tunnel) { viewModel.togglePrimaryTunnel() })
|
||||
add(autoTunnelingItem(tunnel))
|
||||
add(
|
||||
splitTunnelingItem(stringResource(R.string.splt_tunneling)) {
|
||||
navController.push(Route.SplitTunnel(id = tunnelConf.id))
|
||||
navController.push(Route.SplitTunnel(id = tunnel.id))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -76,25 +68,22 @@ fun TunnelOptionsScreen(tunnelId: Int, viewModel: TunnelsViewModel = hiltViewMod
|
||||
items =
|
||||
buildList {
|
||||
add(
|
||||
dynamicDnsItem(tunnelConf.restartOnPingFailure) {
|
||||
viewModel.setRestartOnPing(tunnelId, it)
|
||||
dynamicDnsItem(tunnel.restartOnPingFailure) {
|
||||
viewModel.setRestartOnPing(it)
|
||||
}
|
||||
)
|
||||
if (tunnelsState.appMode != AppMode.KERNEL)
|
||||
if (tunnelState.appMode != AppMode.KERNEL)
|
||||
add(
|
||||
preferIpv6Item(ipv6Preferred) {
|
||||
viewModel.toggleIpv4Preferred(tunnelId)
|
||||
preferIpv6Item(!tunnel.isIpv4Preferred) {
|
||||
viewModel.toggleIpv4Preferred()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
if (tunnelsState.isPingEnabled) {
|
||||
if (tunnelState.isPingEnabled) {
|
||||
SectionDivider()
|
||||
SurfaceSelectionGroupButton(
|
||||
items =
|
||||
listOf(
|
||||
pingConfigItem(tunnelConf) { ip -> viewModel.setTunnelPingIp(ip, tunnelId) }
|
||||
)
|
||||
items = listOf(pingConfigItem(tunnel) { ip -> viewModel.setTunnelPingIp(ip) })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,10 @@ sealed class LocalSideEffect {
|
||||
|
||||
data object DeleteTunnels : Modal()
|
||||
}
|
||||
|
||||
sealed class SelectedTunnels : LocalSideEffect() {
|
||||
data object SelectAll : SelectedTunnels()
|
||||
|
||||
data object Copy : SelectedTunnels()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
data class AutoTunnelUiState(
|
||||
val autoTunnelActive: Boolean = false,
|
||||
val connectivityState: ConnectivityState? = null,
|
||||
val generalSettings: GeneralSettings = GeneralSettings(),
|
||||
val settings: GeneralSettings = GeneralSettings(),
|
||||
val isBatteryOptimizationShown: Boolean = false,
|
||||
val isLocationDisclosureShown: Boolean = false,
|
||||
val stateInitialized: Boolean = false,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.state
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
|
||||
data class ConfigUiState(
|
||||
val unavailableNames: Set<String> = emptySet(),
|
||||
val isLoading: Boolean = true,
|
||||
val tunnel: TunnelConf? = null,
|
||||
)
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.state
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||
|
||||
@@ -10,10 +9,10 @@ data class SharedAppUiState(
|
||||
val theme: Theme = Theme.AUTOMATIC,
|
||||
val locale: String = LocaleUtil.OPTION_PHONE_LANGUAGE,
|
||||
val pinLockEnabled: Boolean = false,
|
||||
val tunnelNames: Map<Int, String> = emptyMap(),
|
||||
val selectedTunnelCount: Int = 0,
|
||||
val isAuthorized: Boolean = false,
|
||||
val isAutoTunnelActive: Boolean = false,
|
||||
val isLocationDisclosureShown: Boolean = false,
|
||||
val tunnels: List<TunnelConf> = emptyList(),
|
||||
val selectedTunnels: Set<TunnelConf> = setOf(),
|
||||
val settings: GeneralSettings = GeneralSettings(),
|
||||
)
|
||||
|
||||
+2
-2
@@ -5,6 +5,6 @@ import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
|
||||
data class SplitTunnelUiState(
|
||||
val installedPackages: List<InstalledPackage> = emptyList(),
|
||||
val stateInitialized: Boolean = false,
|
||||
val tunnels: List<TunnelConf> = emptyList(),
|
||||
val isLoading: Boolean = true,
|
||||
val tunnel: TunnelConf? = null,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.state
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
|
||||
data class TunnelUiState(
|
||||
val tunnel: TunnelConf? = null,
|
||||
val isActive: Boolean = false,
|
||||
val isLoading: Boolean = true,
|
||||
val isPingEnabled: Boolean = false,
|
||||
val isWildcardsEnabled: Boolean = false,
|
||||
val appMode: AppMode = AppMode.VPN,
|
||||
)
|
||||
@@ -5,7 +5,8 @@ import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
|
||||
data class TunnelsUiState(
|
||||
val tunnels: List<TunnelConf> = listOf(),
|
||||
val tunnels: List<TunnelConf> = emptyList(),
|
||||
val selectedTunnels: List<TunnelConf> = emptyList(),
|
||||
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
|
||||
val isPingEnabled: Boolean = false,
|
||||
val appMode: AppMode = AppMode.VPN,
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.QuickConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelName
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.getInputStreamFromUri
|
||||
@@ -19,7 +20,10 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class FileUtils(private val context: Context, private val ioDispatcher: CoroutineDispatcher) {
|
||||
class FileUtils(
|
||||
private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Creates a configuration file for a given tunnel if the config string is valid.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.di.AppShell
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import javax.inject.Provider
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RootShellUtils(
|
||||
@AppShell private val rootShell: Provider<RootShell>,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
|
||||
suspend fun requestRoot(): Boolean =
|
||||
withContext(ioDispatcher) {
|
||||
val accepted =
|
||||
try {
|
||||
rootShell.get().start()
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
accepted
|
||||
}
|
||||
}
|
||||
+59
-12
@@ -1,15 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AutoTunnelUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.RootShellUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
@@ -17,6 +20,7 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.orbitmvi.orbit.ContainerHost
|
||||
import org.orbitmvi.orbit.viewmodel.container
|
||||
import rikka.shizuku.Shizuku
|
||||
|
||||
@HiltViewModel
|
||||
class AutoTunnelViewModel
|
||||
@@ -27,6 +31,7 @@ constructor(
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
private val appStateRepository: AppStateRepository,
|
||||
private val rootShellUtils: RootShellUtils,
|
||||
) : ContainerHost<AutoTunnelUiState, Nothing>, ViewModel() {
|
||||
|
||||
override val container =
|
||||
@@ -44,7 +49,7 @@ constructor(
|
||||
AutoTunnelUiState(
|
||||
autoTunnelActive = active,
|
||||
connectivityState = connectivity,
|
||||
generalSettings = settings,
|
||||
settings = settings,
|
||||
isBatteryOptimizationShown = appState.isBatteryOptimizationDisableShown,
|
||||
isLocationDisclosureShown = appState.isLocationDisclosureShown,
|
||||
stateInitialized = true,
|
||||
@@ -64,7 +69,7 @@ constructor(
|
||||
|
||||
fun toggleAutoTunnel() = intent {
|
||||
if (!state.autoTunnelActive) {
|
||||
when (state.generalSettings.appMode) {
|
||||
when (state.settings.appMode) {
|
||||
AppMode.VPN ->
|
||||
if (!serviceManager.hasVpnPermission())
|
||||
return@intent postSideEffect(
|
||||
@@ -82,45 +87,87 @@ constructor(
|
||||
}
|
||||
|
||||
fun setAutoTunnelOnWifiEnabled(to: Boolean) = intent {
|
||||
settingsRepository.save(state.generalSettings.copy(isTunnelOnWifiEnabled = to))
|
||||
settingsRepository.save(state.settings.copy(isTunnelOnWifiEnabled = to))
|
||||
}
|
||||
|
||||
fun setWildcardsEnabled(to: Boolean) = intent {
|
||||
settingsRepository.save(state.generalSettings.copy(isWildcardsEnabled = to))
|
||||
settingsRepository.save(state.settings.copy(isWildcardsEnabled = to))
|
||||
}
|
||||
|
||||
fun setStopOnNoInternetEnabled(to: Boolean) = intent {
|
||||
settingsRepository.save(state.generalSettings.copy(isStopOnNoInternetEnabled = to))
|
||||
settingsRepository.save(state.settings.copy(isStopOnNoInternetEnabled = to))
|
||||
}
|
||||
|
||||
fun saveTrustedNetworkName(name: String) = intent {
|
||||
if (name.isEmpty()) return@intent
|
||||
val trimmed = name.trim()
|
||||
if (state.generalSettings.trustedNetworkSSIDs.contains(name)) {
|
||||
if (state.settings.trustedNetworkSSIDs.contains(name)) {
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.error_ssid_exists))
|
||||
)
|
||||
}
|
||||
setTrustedNetworkNames((state.generalSettings.trustedNetworkSSIDs + trimmed).toMutableSet())
|
||||
setTrustedNetworkNames((state.settings.trustedNetworkSSIDs + trimmed).toMutableSet())
|
||||
}
|
||||
|
||||
fun setTrustedNetworkNames(to: Set<String>) = intent {
|
||||
settingsRepository.save(state.generalSettings.copy(trustedNetworkSSIDs = to))
|
||||
settingsRepository.save(state.settings.copy(trustedNetworkSSIDs = to))
|
||||
}
|
||||
|
||||
fun removeTrustedNetworkName(name: String) = intent {
|
||||
setTrustedNetworkNames((state.generalSettings.trustedNetworkSSIDs - name).toMutableSet())
|
||||
setTrustedNetworkNames((state.settings.trustedNetworkSSIDs - name).toMutableSet())
|
||||
}
|
||||
|
||||
fun setTunnelOnCellular(to: Boolean) = intent {
|
||||
settingsRepository.save(state.generalSettings.copy(isTunnelOnMobileDataEnabled = to))
|
||||
settingsRepository.save(state.settings.copy(isTunnelOnMobileDataEnabled = to))
|
||||
}
|
||||
|
||||
fun setTunnelOnEthernet(to: Boolean) = intent {
|
||||
settingsRepository.save(state.generalSettings.copy(isTunnelOnEthernetEnabled = to))
|
||||
settingsRepository.save(state.settings.copy(isTunnelOnEthernetEnabled = to))
|
||||
}
|
||||
|
||||
fun setDebounceDelay(to: Int) = intent {
|
||||
settingsRepository.save(state.generalSettings.copy(debounceDelaySeconds = to))
|
||||
settingsRepository.save(state.settings.copy(debounceDelaySeconds = to))
|
||||
}
|
||||
|
||||
fun setWifiDetectionMethod(method: WifiDetectionMethod) = intent {
|
||||
when (method) {
|
||||
WifiDetectionMethod.ROOT -> {
|
||||
val accepted = rootShellUtils.requestRoot()
|
||||
val message =
|
||||
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
||||
else StringValue.StringResource(R.string.root_accepted)
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
if (!accepted) return@intent
|
||||
}
|
||||
WifiDetectionMethod.SHIZUKU -> {
|
||||
requestShizuku()
|
||||
return@intent
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
settingsRepository.save(state.settings.copy(wifiDetectionMethod = method))
|
||||
}
|
||||
|
||||
private fun requestShizuku() = intent {
|
||||
Shizuku.addRequestPermissionResultListener(
|
||||
Shizuku.OnRequestPermissionResultListener { requestCode: Int, grantResult: Int ->
|
||||
if (grantResult != PERMISSION_GRANTED) return@OnRequestPermissionResultListener
|
||||
intent {
|
||||
settingsRepository.save(
|
||||
state.settings.copy(wifiDetectionMethod = WifiDetectionMethod.SHIZUKU)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
try {
|
||||
if (Shizuku.checkSelfPermission() != PERMISSION_GRANTED) Shizuku.requestPermission(123)
|
||||
settingsRepository.save(
|
||||
state.settings.copy(wifiDetectionMethod = WifiDetectionMethod.SHIZUKU)
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.shizuku_not_detected))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import org.amnezia.awg.config.BadConfigException
|
||||
import org.orbitmvi.orbit.ContainerHost
|
||||
import org.orbitmvi.orbit.viewmodel.container
|
||||
import timber.log.Timber
|
||||
|
||||
@HiltViewModel(assistedFactory = ConfigViewModel.Factory::class)
|
||||
class ConfigViewModel
|
||||
@AssistedInject
|
||||
constructor(
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
@Assisted val tunnelId: Int?,
|
||||
) : ContainerHost<ConfigUiState, Nothing>, ViewModel() {
|
||||
|
||||
override val container =
|
||||
container<ConfigUiState, Nothing>(
|
||||
ConfigUiState(),
|
||||
buildSettings = { repeatOnSubscribedStopTimeout = 5000L },
|
||||
) {
|
||||
tunnelRepository.flow.collect { tuns ->
|
||||
reduce {
|
||||
val tunnel = tuns.firstOrNull { it.id == tunnelId }
|
||||
ConfigUiState(
|
||||
tuns.filter { it.id != tunnelId }.map { it.tunName }.toSet(),
|
||||
false,
|
||||
tunnel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveConfigProxy(configProxy: ConfigProxy, tunnelName: String) = intent {
|
||||
if (state.unavailableNames.contains(tunnelName))
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.tunnel_name_taken))
|
||||
)
|
||||
runCatching {
|
||||
val (wg, am) = configProxy.buildConfigs()
|
||||
val tunnelConf =
|
||||
if (tunnelId == null) {
|
||||
TunnelConf.tunnelConfFromQuick(am.toAwgQuickString(true, false), tunnelName)
|
||||
} else {
|
||||
state.tunnel?.copy(
|
||||
tunName = tunnelName,
|
||||
amQuick = am.toAwgQuickString(true, false),
|
||||
wgQuick = wg.toWgQuickString(true),
|
||||
)
|
||||
}
|
||||
if (tunnelConf != null) {
|
||||
tunnelRepository.save(tunnelConf)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.config_changes_saved)
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it)
|
||||
val message =
|
||||
when (it) {
|
||||
is BadConfigException -> it.asStringValue()
|
||||
is com.wireguard.config.BadConfigException -> it.asStringValue()
|
||||
else -> StringValue.StringResource(R.string.unknown_error)
|
||||
}
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun postSideEffect(globalSideEffect: GlobalSideEffect) {
|
||||
globalEffectRepository.post(globalSideEffect)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(tunnelId: Int?): ConfigViewModel
|
||||
}
|
||||
}
|
||||
+17
-217
@@ -1,18 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
|
||||
import com.zaneschepke.wireguardautotunnel.di.AppShell
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
@@ -20,29 +12,17 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectReposit
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.SharedAppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||
import com.zaneschepke.wireguardautotunnel.util.RootShellUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.io.File
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.awg.config.BadConfigException
|
||||
import org.orbitmvi.orbit.ContainerHost
|
||||
import org.orbitmvi.orbit.viewmodel.container
|
||||
import rikka.shizuku.Shizuku
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
@HiltViewModel
|
||||
@@ -55,9 +35,7 @@ constructor(
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val fileUtils: FileUtils,
|
||||
@AppShell private val rootShell: Provider<RootShell>,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
private val rootShellUtils: RootShellUtils,
|
||||
) : ContainerHost<SharedAppUiState, LocalSideEffect>, ViewModel() {
|
||||
|
||||
val globalSideEffect = globalEffectRepository.flow
|
||||
@@ -70,16 +48,18 @@ constructor(
|
||||
intent {
|
||||
combine(
|
||||
appStateRepository.flow,
|
||||
tunnelRepository.userTunnelsFlow.map { tuns ->
|
||||
tuns.associate { it.id to it.tunName }
|
||||
},
|
||||
serviceManager.autoTunnelService.map { it != null },
|
||||
settingsRepository.flow,
|
||||
tunnelRepository.flow,
|
||||
) { appState, autoTunnelActive, settings, tunnels ->
|
||||
) { appState, tunnelNames, autoTunnelActive, settings ->
|
||||
state.copy(
|
||||
theme = appState.theme,
|
||||
locale = appState.locale ?: LocaleUtil.OPTION_PHONE_LANGUAGE,
|
||||
pinLockEnabled = appState.isPinLockEnabled,
|
||||
isAutoTunnelActive = autoTunnelActive,
|
||||
tunnels = tunnels,
|
||||
tunnelNames = tunnelNames,
|
||||
settings = settings,
|
||||
isLocationDisclosureShown = appState.isLocationDisclosureShown,
|
||||
isAppLoaded = true,
|
||||
@@ -127,44 +107,8 @@ constructor(
|
||||
appStateRepository.setPinLockEnabled(enabled)
|
||||
}
|
||||
|
||||
fun saveConfigProxy(tunnelId: Int?, configProxy: ConfigProxy, tunnelName: String) = intent {
|
||||
if (state.tunnels.any { it.tunName == tunnelName && it.id != tunnelId })
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.tunnel_name_taken))
|
||||
)
|
||||
runCatching {
|
||||
val (wg, am) = configProxy.buildConfigs()
|
||||
val tunnelConf =
|
||||
if (tunnelId == null) {
|
||||
TunnelConf.tunnelConfFromQuick(am.toAwgQuickString(true, false), tunnelName)
|
||||
} else {
|
||||
val latestTunnel = state.tunnels.find { it.id == tunnelId }
|
||||
latestTunnel?.copy(
|
||||
tunName = tunnelName,
|
||||
amQuick = am.toAwgQuickString(true, false),
|
||||
wgQuick = wg.toWgQuickString(true),
|
||||
)
|
||||
}
|
||||
if (tunnelConf != null) {
|
||||
tunnelRepository.save(tunnelConf)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.config_changes_saved)
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it)
|
||||
val message =
|
||||
when (it) {
|
||||
is BadConfigException -> it.asStringValue()
|
||||
is com.wireguard.config.BadConfigException -> it.asStringValue()
|
||||
else -> StringValue.StringResource(R.string.unknown_error)
|
||||
}
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
}
|
||||
fun setSelectedTunnelCount(count: Int) = intent {
|
||||
reduce { state.copy(selectedTunnelCount = count) }
|
||||
}
|
||||
|
||||
fun stopTunnel(tunnelConf: TunnelConf) = intent { tunnelManager.stopTunnel(tunnelConf.id) }
|
||||
@@ -180,7 +124,14 @@ constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
AppMode.KERNEL -> if (!requestRoot()) return@intent
|
||||
AppMode.KERNEL -> {
|
||||
val accepted = rootShellUtils.requestRoot()
|
||||
val message =
|
||||
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
||||
else StringValue.StringResource(R.string.root_accepted)
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
if (!accepted) return@intent
|
||||
}
|
||||
}
|
||||
settingsRepository.save(state.settings.copy(appMode = appMode))
|
||||
}
|
||||
@@ -204,155 +155,4 @@ constructor(
|
||||
fun disableBatteryOptimizationsShown() = intent {
|
||||
appStateRepository.setBatteryOptimizationDisableShown(true)
|
||||
}
|
||||
|
||||
fun setWifiDetectionMethod(method: WifiDetectionMethod) = intent {
|
||||
when (method) {
|
||||
WifiDetectionMethod.ROOT -> if (!requestRoot()) return@intent
|
||||
WifiDetectionMethod.SHIZUKU -> {
|
||||
requestShizuku()
|
||||
return@intent
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
settingsRepository.save(state.settings.copy(wifiDetectionMethod = method))
|
||||
}
|
||||
|
||||
private fun requestShizuku() = intent {
|
||||
Shizuku.addRequestPermissionResultListener(
|
||||
Shizuku.OnRequestPermissionResultListener { requestCode: Int, grantResult: Int ->
|
||||
if (grantResult != PERMISSION_GRANTED) return@OnRequestPermissionResultListener
|
||||
intent {
|
||||
settingsRepository.save(
|
||||
state.settings.copy(wifiDetectionMethod = WifiDetectionMethod.SHIZUKU)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
try {
|
||||
if (Shizuku.checkSelfPermission() != PERMISSION_GRANTED) Shizuku.requestPermission(123)
|
||||
settingsRepository.save(
|
||||
state.settings.copy(wifiDetectionMethod = WifiDetectionMethod.SHIZUKU)
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.shizuku_not_detected))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestRoot(): Boolean =
|
||||
withContext(ioDispatcher) {
|
||||
val accepted =
|
||||
try {
|
||||
rootShell.get().start()
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
val message =
|
||||
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
||||
else StringValue.StringResource(R.string.root_accepted)
|
||||
intent { postSideEffect(GlobalSideEffect.Snackbar(message)) }
|
||||
accepted
|
||||
}
|
||||
|
||||
fun toggleSelectAllTunnels() = intent {
|
||||
if (state.selectedTunnels.size != state.tunnels.size) {
|
||||
return@intent reduce { state.copy(selectedTunnels = state.tunnels.toSet()) }
|
||||
}
|
||||
reduce { state.copy(selectedTunnels = emptySet()) }
|
||||
}
|
||||
|
||||
fun clearSelectedTunnels() = intent { reduce { state.copy(selectedTunnels = emptySet()) } }
|
||||
|
||||
fun toggleSelectedTunnel(tunnelId: Int) = intent {
|
||||
reduce {
|
||||
state.copy(
|
||||
selectedTunnels =
|
||||
state.selectedTunnels.toMutableSet().apply {
|
||||
val removed = removeIf { it.id == tunnelId }
|
||||
if (!removed) addAll(state.tunnels.filter { it.id == tunnelId })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteSelectedTunnels() = intent {
|
||||
val activeTunIds = tunnelManager.activeTunnels.firstOrNull()?.map { it.key }
|
||||
if (state.selectedTunnels.any { activeTunIds?.contains(it.id) == true })
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.delete_active_message)
|
||||
)
|
||||
)
|
||||
tunnelRepository.delete(state.selectedTunnels.toList())
|
||||
clearSelectedTunnels()
|
||||
}
|
||||
|
||||
fun copySelectedTunnel() = intent {
|
||||
val selected = state.selectedTunnels.firstOrNull() ?: return@intent
|
||||
val copy = TunnelConf.tunnelConfFromQuick(selected.amQuick, selected.tunName)
|
||||
tunnelRepository.saveTunnelsUniquely(listOf(copy), state.tunnels)
|
||||
clearSelectedTunnels()
|
||||
}
|
||||
|
||||
fun exportSelectedTunnels(configType: ConfigType, uri: Uri?) = intent {
|
||||
val (files, shareFileName) =
|
||||
when (configType) {
|
||||
ConfigType.AM ->
|
||||
Pair(
|
||||
createAmFiles(state.selectedTunnels),
|
||||
"am-export_${Instant.now().epochSecond}.zip",
|
||||
)
|
||||
ConfigType.WG ->
|
||||
Pair(
|
||||
createWgFiles(state.selectedTunnels),
|
||||
"wg-export_${Instant.now().epochSecond}.zip",
|
||||
)
|
||||
}
|
||||
val onFailure = { action: Throwable ->
|
||||
intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(
|
||||
R.string.export_failed,
|
||||
": ${action.localizedMessage}",
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
Unit
|
||||
}
|
||||
fileUtils
|
||||
.createNewShareFile(shareFileName)
|
||||
.onSuccess {
|
||||
fileUtils.zipAll(it, files).onFailure(onFailure)
|
||||
fileUtils.exportFile(it, uri, FileUtils.ZIP_FILE_MIME_TYPE).onFailure(onFailure)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.export_success))
|
||||
)
|
||||
clearSelectedTunnels()
|
||||
}
|
||||
.onFailure(onFailure)
|
||||
}
|
||||
|
||||
suspend fun createWgFiles(tunnels: Collection<TunnelConf>): List<File> =
|
||||
withContext(ioDispatcher) {
|
||||
tunnels.mapNotNull { config ->
|
||||
if (config.wgQuick.isNotBlank()) {
|
||||
fileUtils.createFile(config.tunName, config.wgQuick)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createAmFiles(tunnels: Collection<TunnelConf>): List<File> =
|
||||
withContext(ioDispatcher) {
|
||||
tunnels.mapNotNull { config ->
|
||||
if (
|
||||
config.amQuick != TunnelConfig.AM_QUICK_DEFAULT && config.amQuick.isNotBlank()
|
||||
) {
|
||||
fileUtils.createFile(config.tunName, config.amQuick)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-39
@@ -11,20 +11,24 @@ import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.SplitTunnelUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.orbitmvi.orbit.ContainerHost
|
||||
import org.orbitmvi.orbit.viewmodel.container
|
||||
|
||||
@HiltViewModel
|
||||
@HiltViewModel(assistedFactory = SplitTunnelViewModel.Factory::class)
|
||||
class SplitTunnelViewModel
|
||||
@Inject
|
||||
@AssistedInject
|
||||
constructor(
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val packageRepository: InstalledPackageRepository,
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
@Assisted val tunnelId: Int,
|
||||
) : ContainerHost<SplitTunnelUiState, Nothing>, ViewModel() {
|
||||
|
||||
override val container =
|
||||
@@ -37,8 +41,13 @@ constructor(
|
||||
emit(packages)
|
||||
}
|
||||
|
||||
combine(packagesFlow, tunnelRepository.flow) { packages, tunnels ->
|
||||
SplitTunnelUiState(packages, true, tunnels)
|
||||
combine(
|
||||
packagesFlow,
|
||||
tunnelRepository.userTunnelsFlow.map {
|
||||
it.firstOrNull { tun -> tun.id == tunnelId }
|
||||
},
|
||||
) { packages, tunnel ->
|
||||
SplitTunnelUiState(packages, tunnel == null, tunnel)
|
||||
}
|
||||
.collect { reduce { it } }
|
||||
}
|
||||
@@ -47,39 +56,36 @@ constructor(
|
||||
globalEffectRepository.post(globalSideEffect)
|
||||
}
|
||||
|
||||
fun saveSplitTunnelSelection(tunnelId: Int, splitConfig: Pair<SplitOption, Set<String>>) =
|
||||
intent {
|
||||
val latestTunnel = state.tunnels.find { it.id == tunnelId }
|
||||
if (latestTunnel != null) {
|
||||
val config = latestTunnel.toAmConfig()
|
||||
val (option, pkgs) = splitConfig
|
||||
val configProxy = ConfigProxy.from(config)
|
||||
val interfaceProxy = InterfaceProxy.from(config.`interface`)
|
||||
val (included, excluded) =
|
||||
when (option) {
|
||||
SplitOption.INCLUDE -> Pair(pkgs, emptySet<String>())
|
||||
SplitOption.ALL -> Pair(emptySet(), emptySet())
|
||||
SplitOption.EXCLUDE -> Pair(emptySet(), pkgs)
|
||||
}
|
||||
val updatedInterface =
|
||||
interfaceProxy.copy(
|
||||
includedApplications = included,
|
||||
excludedApplications = excluded,
|
||||
)
|
||||
val updatedConfig = configProxy.copy(`interface` = updatedInterface)
|
||||
val (wg, am) = updatedConfig.buildConfigs()
|
||||
tunnelRepository.save(
|
||||
latestTunnel.copy(
|
||||
amQuick = am.toAwgQuickString(true, false),
|
||||
wgQuick = wg.toWgQuickString(true),
|
||||
)
|
||||
)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved)
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
fun saveSplitTunnelSelection(splitConfig: Pair<SplitOption, Set<String>>) = intent {
|
||||
val tunnel = state.tunnel ?: return@intent
|
||||
val config = tunnel.toAmConfig()
|
||||
val (option, pkgs) = splitConfig
|
||||
val configProxy = ConfigProxy.from(config)
|
||||
val interfaceProxy = InterfaceProxy.from(config.`interface`)
|
||||
val (included, excluded) =
|
||||
when (option) {
|
||||
SplitOption.INCLUDE -> Pair(pkgs, emptySet<String>())
|
||||
SplitOption.ALL -> Pair(emptySet(), emptySet())
|
||||
SplitOption.EXCLUDE -> Pair(emptySet(), pkgs)
|
||||
}
|
||||
}
|
||||
val updatedInterface =
|
||||
interfaceProxy.copy(includedApplications = included, excludedApplications = excluded)
|
||||
val updatedConfig = configProxy.copy(`interface` = updatedInterface)
|
||||
val (wg, am) = updatedConfig.buildConfigs()
|
||||
tunnelRepository.save(
|
||||
tunnel.copy(
|
||||
amQuick = am.toAwgQuickString(true, false),
|
||||
wgQuick = wg.toWgQuickString(true),
|
||||
)
|
||||
)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(tunnelId: Int): SplitTunnelViewModel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelUiState
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.orbitmvi.orbit.ContainerHost
|
||||
import org.orbitmvi.orbit.viewmodel.container
|
||||
|
||||
@HiltViewModel(assistedFactory = TunnelViewModel.Factory::class)
|
||||
class TunnelViewModel
|
||||
@AssistedInject
|
||||
constructor(
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
private val tunnelManager: TunnelManager,
|
||||
@Assisted val tunnelId: Int,
|
||||
) : ContainerHost<TunnelUiState, Nothing>, ViewModel() {
|
||||
|
||||
override val container =
|
||||
container<TunnelUiState, Nothing>(
|
||||
TunnelUiState(),
|
||||
buildSettings = { repeatOnSubscribedStopTimeout = 5000L },
|
||||
) {
|
||||
combine(
|
||||
tunnelRepository.userTunnelsFlow.map {
|
||||
it.firstOrNull { tun -> tun.id == tunnelId }
|
||||
},
|
||||
tunnelManager.activeTunnels.map { it.containsKey(tunnelId) },
|
||||
settingsRepository.flow,
|
||||
) { tunnel, active, settings ->
|
||||
state.copy(
|
||||
tunnel = tunnel,
|
||||
isActive = active,
|
||||
isLoading = tunnel == null,
|
||||
isWildcardsEnabled = settings.isWildcardsEnabled,
|
||||
isPingEnabled = settings.isPingEnabled,
|
||||
appMode = settings.appMode,
|
||||
)
|
||||
}
|
||||
.collect { reduce { it } }
|
||||
}
|
||||
|
||||
fun setTunnelPingIp(ip: String) = intent {
|
||||
val tunnel = state.tunnel ?: return@intent
|
||||
tunnelRepository.save(tunnel.copy(pingTarget = ip.ifBlank { null }))
|
||||
}
|
||||
|
||||
fun addTunnelNetwork(ssid: String) = intent {
|
||||
val tunnel = state.tunnel ?: return@intent
|
||||
tunnelRepository.save(
|
||||
tunnel.copy(tunnelNetworks = tunnel.tunnelNetworks.toMutableSet().apply { add(ssid) })
|
||||
)
|
||||
}
|
||||
|
||||
fun removeTunnelNetwork(ssid: String) = intent {
|
||||
val tunnel = state.tunnel ?: return@intent
|
||||
tunnelRepository.save(
|
||||
tunnel.copy(
|
||||
tunnelNetworks = tunnel.tunnelNetworks.toMutableSet().apply { remove(ssid) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun setRestartOnPing(to: Boolean) = intent {
|
||||
val tunnel = state.tunnel ?: return@intent
|
||||
tunnelRepository.save(tunnel.copy(restartOnPingFailure = to))
|
||||
}
|
||||
|
||||
fun togglePrimaryTunnel() = intent {
|
||||
val tunnel = state.tunnel ?: return@intent
|
||||
val update = if (tunnel.isPrimaryTunnel) null else tunnel
|
||||
tunnelRepository.updatePrimaryTunnel(update)
|
||||
}
|
||||
|
||||
fun setMobileDataTunnel(to: Boolean) = intent {
|
||||
val tunnel = state.tunnel ?: return@intent
|
||||
tunnelRepository.save(tunnel.copy(isMobileDataTunnel = to))
|
||||
}
|
||||
|
||||
fun setEthernetTunnel(to: Boolean) = intent {
|
||||
val tunnel = state.tunnel ?: return@intent
|
||||
tunnelRepository.save(tunnel.copy(isEthernetTunnel = to))
|
||||
}
|
||||
|
||||
fun toggleIpv4Preferred() = intent {
|
||||
val latestTunnel = state.tunnel ?: return@intent
|
||||
tunnelRepository.save(latestTunnel.copy(isIpv4Preferred = !latestTunnel.isIpv4Preferred))
|
||||
}
|
||||
|
||||
private suspend fun postSideEffect(globalSideEffect: GlobalSideEffect) {
|
||||
globalEffectRepository.post(globalSideEffect)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(tunnelId: Int): TunnelViewModel
|
||||
}
|
||||
}
|
||||
+99
-68
@@ -5,6 +5,8 @@ import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
@@ -19,10 +21,13 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelName
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import org.amnezia.awg.config.BadConfigException
|
||||
import org.orbitmvi.orbit.ContainerHost
|
||||
import org.orbitmvi.orbit.viewmodel.container
|
||||
@@ -89,74 +94,6 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun setTunnelPingIp(ip: String, tunnelId: Int) = intent {
|
||||
val latestTunnel = state.tunnels.find { it.id == tunnelId }
|
||||
if (latestTunnel != null) {
|
||||
tunnelRepository.save(latestTunnel.copy(pingTarget = ip.ifBlank { null }))
|
||||
}
|
||||
}
|
||||
|
||||
fun addTunnelNetwork(tunnelId: Int, ssid: String) = intent {
|
||||
val latestTunnel = state.tunnels.find { it.id == tunnelId }
|
||||
if (latestTunnel != null) {
|
||||
tunnelRepository.save(
|
||||
latestTunnel.copy(
|
||||
tunnelNetworks = latestTunnel.tunnelNetworks.toMutableSet().apply { add(ssid) }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeTunnelNetwork(tunnelId: Int, ssid: String) = intent {
|
||||
val latestTunnel = state.tunnels.find { it.id == tunnelId }
|
||||
if (latestTunnel != null) {
|
||||
tunnelRepository.save(
|
||||
latestTunnel.copy(
|
||||
tunnelNetworks =
|
||||
latestTunnel.tunnelNetworks.toMutableSet().apply { remove(ssid) }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setRestartOnPing(tunnelId: Int, to: Boolean) = intent {
|
||||
val latestTunnel = state.tunnels.find { it.id == tunnelId }
|
||||
if (latestTunnel != null) {
|
||||
tunnelRepository.save(latestTunnel.copy(restartOnPingFailure = to))
|
||||
}
|
||||
}
|
||||
|
||||
fun togglePrimaryTunnel(tunnelId: Int) = intent {
|
||||
val latestTunnel = state.tunnels.find { it.id == tunnelId }
|
||||
if (latestTunnel != null) {
|
||||
val update = if (latestTunnel.isPrimaryTunnel) null else latestTunnel
|
||||
tunnelRepository.updatePrimaryTunnel(update)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMobileDataTunnel(tunnelId: Int, to: Boolean) = intent {
|
||||
val latestTunnel = state.tunnels.find { it.id == tunnelId }
|
||||
if (latestTunnel != null) {
|
||||
tunnelRepository.save(latestTunnel.copy(isMobileDataTunnel = to))
|
||||
}
|
||||
}
|
||||
|
||||
fun setEthernetTunnel(tunnelId: Int, to: Boolean) = intent {
|
||||
val latestTunnel = state.tunnels.find { it.id == tunnelId }
|
||||
if (latestTunnel != null) {
|
||||
tunnelRepository.save(latestTunnel.copy(isEthernetTunnel = to))
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleIpv4Preferred(tunnelId: Int) = intent {
|
||||
val latestTunnel = state.tunnels.find { it.id == tunnelId }
|
||||
if (latestTunnel != null) {
|
||||
tunnelRepository.save(
|
||||
latestTunnel.copy(isIpv4Preferred = !latestTunnel.isIpv4Preferred)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun importFromClipboard(conf: String) {
|
||||
importTunnelConfigs(mapOf(conf to null))
|
||||
}
|
||||
@@ -191,4 +128,98 @@ constructor(
|
||||
postSideEffect(GlobalSideEffect.Toast(message))
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSelectAllTunnels() = intent {
|
||||
if (state.selectedTunnels.size != state.tunnels.size) {
|
||||
return@intent reduce { state.copy(selectedTunnels = state.tunnels) }
|
||||
}
|
||||
reduce { state.copy(selectedTunnels = emptyList()) }
|
||||
}
|
||||
|
||||
fun clearSelectedTunnels() = intent { reduce { state.copy(selectedTunnels = emptyList()) } }
|
||||
|
||||
fun toggleSelectedTunnel(tunnelId: Int) = intent {
|
||||
reduce {
|
||||
state.copy(
|
||||
selectedTunnels =
|
||||
state.selectedTunnels.toMutableList().apply {
|
||||
val removed = removeIf { it.id == tunnelId }
|
||||
if (!removed) addAll(state.tunnels.filter { it.id == tunnelId })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteSelectedTunnels() = intent {
|
||||
val activeTunIds = tunnelManager.activeTunnels.firstOrNull()?.map { it.key }
|
||||
if (state.selectedTunnels.any { activeTunIds?.contains(it.id) == true })
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.delete_active_message)
|
||||
)
|
||||
)
|
||||
tunnelRepository.delete(state.selectedTunnels)
|
||||
clearSelectedTunnels()
|
||||
}
|
||||
|
||||
fun copySelectedTunnel() = intent {
|
||||
val selected = state.selectedTunnels.firstOrNull() ?: return@intent
|
||||
val copy = TunnelConf.tunnelConfFromQuick(selected.amQuick, selected.tunName)
|
||||
tunnelRepository.saveTunnelsUniquely(listOf(copy), state.tunnels)
|
||||
clearSelectedTunnels()
|
||||
}
|
||||
|
||||
fun exportSelectedTunnels(configType: ConfigType, uri: Uri?) = intent {
|
||||
val (files, shareFileName) =
|
||||
when (configType) {
|
||||
ConfigType.AM ->
|
||||
Pair(
|
||||
createAmFiles(state.selectedTunnels),
|
||||
"am-export_${Instant.now().epochSecond}.zip",
|
||||
)
|
||||
ConfigType.WG ->
|
||||
Pair(
|
||||
createWgFiles(state.selectedTunnels),
|
||||
"wg-export_${Instant.now().epochSecond}.zip",
|
||||
)
|
||||
}
|
||||
val onFailure = { action: Throwable ->
|
||||
intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(
|
||||
R.string.export_failed,
|
||||
": ${action.localizedMessage}",
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
Unit
|
||||
}
|
||||
fileUtils
|
||||
.createNewShareFile(shareFileName)
|
||||
.onSuccess {
|
||||
fileUtils.zipAll(it, files).onFailure(onFailure)
|
||||
fileUtils.exportFile(it, uri, FileUtils.ZIP_FILE_MIME_TYPE).onFailure(onFailure)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.export_success))
|
||||
)
|
||||
clearSelectedTunnels()
|
||||
}
|
||||
.onFailure(onFailure)
|
||||
}
|
||||
|
||||
suspend fun createWgFiles(tunnels: Collection<TunnelConf>): List<File> =
|
||||
tunnels.mapNotNull { config ->
|
||||
if (config.wgQuick.isNotBlank()) {
|
||||
fileUtils.createFile(config.tunName, config.wgQuick)
|
||||
} else null
|
||||
}
|
||||
|
||||
suspend fun createAmFiles(tunnels: Collection<TunnelConf>): List<File> =
|
||||
tunnels.mapNotNull { config ->
|
||||
if (config.amQuick != TunnelConfig.AM_QUICK_DEFAULT && config.amQuick.isNotBlank()) {
|
||||
fileUtils.createFile(config.tunName, config.amQuick)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user