mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
@@ -91,6 +91,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
@@ -179,7 +180,7 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.globalSideEffect.collect { sideEffect ->
|
||||
viewModel.globalSideEffect.collectLatest { sideEffect ->
|
||||
when (sideEffect) {
|
||||
GlobalSideEffect.ConfigChanged -> restartApp()
|
||||
GlobalSideEffect.PopBackStack -> navController.pop()
|
||||
|
||||
@@ -22,6 +22,7 @@ class DataStoreManager(
|
||||
private val preferencesKey = "preferences"
|
||||
val Context.dataStore by preferencesDataStore(name = preferencesKey)
|
||||
val dataStore = context.dataStore
|
||||
|
||||
companion object {
|
||||
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||
val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||
@@ -51,7 +52,8 @@ class DataStoreManager(
|
||||
|
||||
suspend fun <T> removeFromDataStore(key: Preferences.Key<T>) {
|
||||
withContext(ioDispatcher) {
|
||||
try { dataStore.edit { it.remove(key) }
|
||||
try {
|
||||
dataStore.edit { it.remove(key) }
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} catch (e: Exception) {
|
||||
|
||||
+5
-5
@@ -211,7 +211,7 @@ fun currentRouteAsNavbarState(
|
||||
},
|
||||
)
|
||||
is Config -> {
|
||||
val tunnelName = sharedState.tunnelNames[route.id]
|
||||
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
IconButton(onClick = { navController.pop() }) {
|
||||
@@ -236,7 +236,7 @@ fun currentRouteAsNavbarState(
|
||||
)
|
||||
}
|
||||
is SplitTunnel -> {
|
||||
val tunnelName = sharedState.tunnelNames[route.id]
|
||||
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
IconButton(onClick = { navController.pop() }) {
|
||||
@@ -337,7 +337,7 @@ fun currentRouteAsNavbarState(
|
||||
showBottomItems = true,
|
||||
)
|
||||
is TunnelOptions -> {
|
||||
val tunnelName = sharedState.tunnelNames[route.id]
|
||||
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
IconButton(onClick = { navController.pop() }) {
|
||||
@@ -369,7 +369,7 @@ fun currentRouteAsNavbarState(
|
||||
NavbarState(
|
||||
topTitle = context.getString(R.string.tunnels),
|
||||
topTrailing = {
|
||||
when (sharedState.selectedTunnelCount) {
|
||||
when (sharedState.selectedTunnels.size) {
|
||||
0 ->
|
||||
Row {
|
||||
IconButton(onClick = { navController.push(Sort) }) {
|
||||
@@ -423,7 +423,7 @@ fun currentRouteAsNavbarState(
|
||||
}
|
||||
}
|
||||
|
||||
if (sharedState.selectedTunnelCount == 1) {
|
||||
if (sharedState.selectedTunnels.size == 1) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
sharedViewModel.postSideEffect(
|
||||
|
||||
+17
-1
@@ -23,6 +23,7 @@ import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
@@ -39,6 +40,7 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
|
||||
import java.util.*
|
||||
|
||||
@@ -50,6 +52,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
|
||||
|
||||
val locale = remember { Locale.getDefault() }
|
||||
|
||||
val sharedState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val settingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
if (settingsState.isLoading) return
|
||||
@@ -77,7 +80,20 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
|
||||
}
|
||||
}
|
||||
|
||||
if (showBackupSheet) BackupBottomSheet { showBackupSheet = false }
|
||||
fun performBackupRestore(action: () -> Unit) {
|
||||
if (sharedState.activeTunnels.isNotEmpty() || sharedState.isAutoTunnelActive)
|
||||
return context.showToast(R.string.all_services_disabled)
|
||||
showBackupSheet = false
|
||||
action()
|
||||
}
|
||||
|
||||
if (showBackupSheet)
|
||||
BackupBottomSheet(
|
||||
{ performBackupRestore { (context as? MainActivity)?.performBackup() } },
|
||||
{ performBackupRestore { (context as? MainActivity)?.performRestore() } },
|
||||
) {
|
||||
showBackupSheet = false
|
||||
}
|
||||
if (showAppModeSheet)
|
||||
AppModeBottomSheet(sharedViewModel::setAppMode, settingsState.settings.appMode) {
|
||||
showAppModeSheet = false
|
||||
|
||||
+3
-10
@@ -8,14 +8,13 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BackupBottomSheet(onDismiss: () -> Unit) {
|
||||
fun BackupBottomSheet(onBackup: () -> Unit, onRestore: () -> Unit, onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
|
||||
CustomBottomSheet(
|
||||
@@ -23,18 +22,12 @@ fun BackupBottomSheet(onDismiss: () -> Unit) {
|
||||
SheetOption(
|
||||
ImageVector.vectorResource(R.drawable.database),
|
||||
stringResource(R.string.backup_application),
|
||||
onClick = {
|
||||
onDismiss()
|
||||
(context as? MainActivity)?.performBackup()
|
||||
},
|
||||
onClick = onBackup,
|
||||
),
|
||||
SheetOption(
|
||||
Icons.Outlined.Restore,
|
||||
stringResource(R.string.restore_application),
|
||||
onClick = {
|
||||
onDismiss()
|
||||
(context as? MainActivity)?.performRestore()
|
||||
},
|
||||
onClick = onRestore,
|
||||
),
|
||||
)
|
||||
) {
|
||||
|
||||
+7
-15
@@ -8,7 +8,6 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
@@ -26,29 +25,22 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.components.UrlImpo
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelsViewModel
|
||||
import org.orbitmvi.orbit.compose.collectSideEffect
|
||||
|
||||
@Composable
|
||||
fun TunnelsScreen(viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
val sharedViewModel = LocalSharedVm.current
|
||||
fun TunnelsScreen() {
|
||||
val viewModel = LocalSharedVm.current
|
||||
val navController = LocalNavController.current
|
||||
val clipboard = rememberClipboardHelper()
|
||||
|
||||
val tunnelsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val sharedState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
var showExportSheet by rememberSaveable { mutableStateOf(false) }
|
||||
var showImportSheet by rememberSaveable { mutableStateOf(false) }
|
||||
var showDeleteModal by rememberSaveable { mutableStateOf(false) }
|
||||
var showUrlDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (tunnelsState.isLoading) return
|
||||
|
||||
LaunchedEffect(tunnelsState.selectedTunnels) {
|
||||
sharedViewModel.setSelectedTunnelCount(tunnelsState.selectedTunnels.size)
|
||||
}
|
||||
|
||||
sharedViewModel.collectSideEffect { sideEffect ->
|
||||
viewModel.collectSideEffect { sideEffect ->
|
||||
when (sideEffect) {
|
||||
LocalSideEffect.Sheet.ImportTunnels -> showImportSheet = true
|
||||
LocalSideEffect.Modal.DeleteTunnels -> showDeleteModal = true
|
||||
@@ -62,7 +54,7 @@ fun TunnelsScreen(viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
val tunnelFileImportResultLauncher =
|
||||
rememberFileImportLauncherForResult(
|
||||
onNoFileExplorer = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
viewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.error_no_file_explorer)
|
||||
)
|
||||
},
|
||||
@@ -82,7 +74,7 @@ fun TunnelsScreen(viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted
|
||||
->
|
||||
if (!isGranted) {
|
||||
sharedViewModel.showSnackMessage(
|
||||
viewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.camera_permission_required)
|
||||
)
|
||||
return@rememberLauncherForActivityResult
|
||||
@@ -142,5 +134,5 @@ fun TunnelsScreen(viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
)
|
||||
}
|
||||
|
||||
TunnelList(tunnelsState, Modifier.fillMaxSize(), viewModel, sharedViewModel)
|
||||
TunnelList(sharedState, Modifier.fillMaxSize(), viewModel)
|
||||
}
|
||||
|
||||
+15
-17
@@ -25,19 +25,17 @@ import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.SharedAppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelsViewModel
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TunnelList(
|
||||
tunnelsState: TunnelsUiState,
|
||||
sharedState: SharedAppUiState,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: TunnelsViewModel,
|
||||
sharedViewModel: SharedAppViewModel,
|
||||
viewModel: SharedAppViewModel,
|
||||
) {
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
@@ -50,7 +48,7 @@ fun TunnelList(
|
||||
modifier
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures {
|
||||
if (tunnelsState.tunnels.isEmpty()) return@detectTapGestures
|
||||
if (sharedState.tunnels.isEmpty()) return@detectTapGestures
|
||||
viewModel.clearSelectedTunnels()
|
||||
}
|
||||
}
|
||||
@@ -60,17 +58,17 @@ fun TunnelList(
|
||||
reverseLayout = false,
|
||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
) {
|
||||
if (tunnelsState.tunnels.isEmpty()) {
|
||||
if (sharedState.tunnels.isEmpty()) {
|
||||
item { GettingStartedLabel(onClick = { context.openWebUrl(it) }) }
|
||||
}
|
||||
items(tunnelsState.tunnels, key = { it.id }) { tunnel ->
|
||||
items(sharedState.tunnels, key = { it.id }) { tunnel ->
|
||||
val tunnelState =
|
||||
remember(tunnelsState.activeTunnels) {
|
||||
tunnelsState.activeTunnels[tunnel.id] ?: TunnelState()
|
||||
remember(sharedState.activeTunnels) {
|
||||
sharedState.activeTunnels[tunnel.id] ?: TunnelState()
|
||||
}
|
||||
val selected =
|
||||
remember(tunnelsState.selectedTunnels) {
|
||||
tunnelsState.selectedTunnels.any { it.id == tunnel.id }
|
||||
remember(sharedState.selectedTunnels) {
|
||||
sharedState.selectedTunnels.any { it.id == tunnel.id }
|
||||
}
|
||||
var leadingIconColor by
|
||||
remember(
|
||||
@@ -93,7 +91,7 @@ fun TunnelList(
|
||||
},
|
||||
title = tunnel.name,
|
||||
onClick = {
|
||||
if (tunnelsState.selectedTunnels.isNotEmpty()) {
|
||||
if (sharedState.selectedTunnels.isNotEmpty()) {
|
||||
viewModel.toggleSelectedTunnel(tunnel.id)
|
||||
} else {
|
||||
navController.push(Route.TunnelOptions(tunnel.id))
|
||||
@@ -106,8 +104,8 @@ fun TunnelList(
|
||||
{
|
||||
TunnelStatisticsRow(
|
||||
tunnelState,
|
||||
tunnelsState.isPingEnabled,
|
||||
tunnelsState.showPingStats,
|
||||
sharedState.isPingEnabled,
|
||||
sharedState.showPingStats,
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
@@ -116,8 +114,8 @@ fun TunnelList(
|
||||
SwitchWithDivider(
|
||||
checked = tunnelState.status.isUpOrStarting(),
|
||||
onClick = { checked ->
|
||||
if (checked) sharedViewModel.startTunnel(tunnel)
|
||||
else sharedViewModel.stopTunnel(tunnel)
|
||||
if (checked) viewModel.startTunnel(tunnel)
|
||||
else viewModel.stopTunnel(tunnel)
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
+7
-11
@@ -25,7 +25,6 @@ import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
@@ -33,25 +32,22 @@ import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isSortedBy
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelsViewModel
|
||||
import org.orbitmvi.orbit.compose.collectSideEffect
|
||||
import sh.calvin.reorderable.DragGestureDetector
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@Composable
|
||||
fun SortScreen(viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
val sharedViewModel = LocalSharedVm.current
|
||||
val tunnelsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
fun SortScreen() {
|
||||
val viewModel = LocalSharedVm.current
|
||||
val sharedState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
|
||||
if (tunnelsState.isLoading) return
|
||||
|
||||
var sortAscending by rememberSaveable { mutableStateOf<Boolean?>(null) }
|
||||
var editableTunnels by rememberSaveable { mutableStateOf(tunnelsState.tunnels) }
|
||||
var editableTunnels by rememberSaveable { mutableStateOf(sharedState.tunnels) }
|
||||
|
||||
sharedViewModel.collectSideEffect { sideEffect ->
|
||||
viewModel.collectSideEffect { sideEffect ->
|
||||
when (sideEffect) {
|
||||
LocalSideEffect.SaveChanges -> {
|
||||
viewModel.saveSortChanges(editableTunnels)
|
||||
@@ -67,7 +63,7 @@ fun SortScreen(viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
when (sortAscending) {
|
||||
true -> editableTunnels.sortedBy { it.name }
|
||||
false -> editableTunnels.sortedByDescending { it.name }
|
||||
null -> tunnelsState.tunnels
|
||||
null -> sharedState.tunnels
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
@@ -90,7 +86,7 @@ fun SortScreen(viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
|
||||
modifier =
|
||||
Modifier.pointerInput(Unit) { if (tunnelsState.tunnels.isEmpty()) return@pointerInput }
|
||||
Modifier.pointerInput(Unit) { if (sharedState.tunnels.isEmpty()) return@pointerInput }
|
||||
.overscroll(rememberOverscrollEffect()),
|
||||
state = lazyListState,
|
||||
userScrollEnabled = true,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.state
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||
|
||||
@@ -9,8 +11,11 @@ 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 tunnels: List<TunnelConfig> = emptyList(),
|
||||
val selectedTunnels: List<TunnelConfig> = emptyList(),
|
||||
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
|
||||
val isPingEnabled: Boolean = false,
|
||||
val showPingStats: Boolean = false,
|
||||
val isPinVerified: Boolean = false,
|
||||
val isAutoTunnelActive: Boolean = false,
|
||||
val isLocationDisclosureShown: Boolean = false,
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.state
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
|
||||
data class TunnelsUiState(
|
||||
val tunnels: List<TunnelConfig> = emptyList(),
|
||||
val selectedTunnels: List<TunnelConfig> = emptyList(),
|
||||
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
|
||||
val isPingEnabled: Boolean = false,
|
||||
val showPingStats: Boolean = false,
|
||||
val isLoading: Boolean = true,
|
||||
)
|
||||
|
||||
+190
-12
@@ -1,26 +1,44 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
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.repository.MonitoringSettingsRepository
|
||||
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.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.QuickConfig
|
||||
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 kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.map
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.amnezia.awg.config.BadConfigException
|
||||
import org.orbitmvi.orbit.ContainerHost
|
||||
import org.orbitmvi.orbit.viewmodel.container
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
@@ -35,7 +53,9 @@ constructor(
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val monitoringSettingsRepository: MonitoringSettingsRepository,
|
||||
private val rootShellUtils: RootShellUtils,
|
||||
private val fileUtils: FileUtils,
|
||||
) : ContainerHost<SharedAppUiState, LocalSideEffect>, ViewModel() {
|
||||
|
||||
val globalSideEffect = globalEffectRepository.flow
|
||||
@@ -47,22 +67,22 @@ constructor(
|
||||
) {
|
||||
intent {
|
||||
combine(
|
||||
appStateRepository.flow,
|
||||
tunnelRepository.userTunnelsFlow.map { tuns ->
|
||||
tuns.associate { it.id to it.name }
|
||||
},
|
||||
tunnelRepository.userTunnelsFlow,
|
||||
serviceManager.autoTunnelService.map { it != null },
|
||||
settingsRepository.flow,
|
||||
) { appState, tunnelNames, autoTunnelActive, settings ->
|
||||
tunnelManager.activeTunnels,
|
||||
monitoringSettingsRepository.flow,
|
||||
) { tunnels, autoTunnelActive, settings, activeTunnels, monitoring ->
|
||||
state.copy(
|
||||
theme = settings.theme,
|
||||
locale = settings.locale ?: LocaleUtil.OPTION_PHONE_LANGUAGE,
|
||||
pinLockEnabled = settings.isPinLockEnabled,
|
||||
isAutoTunnelActive = autoTunnelActive,
|
||||
tunnelNames = tunnelNames,
|
||||
settings = settings,
|
||||
isLocationDisclosureShown = appState.isLocationDisclosureShown,
|
||||
isBatteryOptimizationShown = appState.isBatteryOptimizationDisableShown,
|
||||
tunnels = tunnels,
|
||||
activeTunnels = activeTunnels,
|
||||
isPingEnabled = monitoring.isPingEnabled,
|
||||
showPingStats = monitoring.showDetailedPingStats,
|
||||
isAppLoaded = true,
|
||||
)
|
||||
}
|
||||
@@ -75,6 +95,17 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
intent {
|
||||
appStateRepository.flow.collect {
|
||||
reduce {
|
||||
state.copy(
|
||||
isLocationDisclosureShown = it.isLocationDisclosureShown,
|
||||
isBatteryOptimizationShown = it.isBatteryOptimizationDisableShown,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
intent {
|
||||
tunnelManager.messageEvents.collect { (_, message) ->
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message.toStringValue()))
|
||||
@@ -114,10 +145,6 @@ constructor(
|
||||
settingsRepository.upsert(state.settings.copy(isPinLockEnabled = enabled))
|
||||
}
|
||||
|
||||
fun setSelectedTunnelCount(count: Int) = intent {
|
||||
reduce { state.copy(selectedTunnelCount = count) }
|
||||
}
|
||||
|
||||
fun stopTunnel(tunnelConfig: TunnelConfig) = intent {
|
||||
tunnelManager.stopTunnel(tunnelConfig.id)
|
||||
}
|
||||
@@ -164,4 +191,155 @@ constructor(
|
||||
fun disableBatteryOptimizationsShown() = intent {
|
||||
appStateRepository.setBatteryOptimizationDisableShown(true)
|
||||
}
|
||||
|
||||
fun saveSortChanges(tunnels: List<TunnelConfig>) = intent {
|
||||
tunnelRepository.saveAll(tunnels.mapIndexed { index, conf -> conf.copy(position = index) })
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
|
||||
fun importTunnelConfigs(configs: Map<QuickConfig, TunnelName>) = intent {
|
||||
try {
|
||||
val tunnelConfigs =
|
||||
configs.map { (config, name) -> TunnelConfig.tunnelConfFromQuick(config, name) }
|
||||
tunnelRepository.saveTunnelsUniquely(tunnelConfigs, state.tunnels)
|
||||
} catch (_: IOException) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.read_failed))
|
||||
)
|
||||
} catch (e: BadConfigException) {
|
||||
postSideEffect(GlobalSideEffect.Snackbar(e.asStringValue()))
|
||||
}
|
||||
}
|
||||
|
||||
fun importFromClipboard(conf: String) {
|
||||
importTunnelConfigs(mapOf(conf to null))
|
||||
}
|
||||
|
||||
fun importFromQr(conf: String) = intent { importFromClipboard(conf) }
|
||||
|
||||
fun importFromUrl(url: String) = intent {
|
||||
runCatching {
|
||||
val url = URL(url)
|
||||
val uri = url.toURI().toString().toUri()
|
||||
importFromUri(uri)
|
||||
}
|
||||
.onFailure {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.error_download_failed)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun importFromUri(uri: Uri) = intent {
|
||||
fileUtils
|
||||
.readConfigsFromUri(uri)
|
||||
.onSuccess { configs -> importTunnelConfigs(configs) }
|
||||
.onFailure {
|
||||
val message =
|
||||
when (it) {
|
||||
is IOException -> StringValue.StringResource(R.string.error_download_failed)
|
||||
else -> StringValue.StringResource(R.string.error_file_extension)
|
||||
}
|
||||
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 = TunnelConfig.tunnelConfFromQuick(selected.amQuick, selected.name)
|
||||
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<TunnelConfig>): List<File> =
|
||||
tunnels.mapNotNull { config ->
|
||||
if (config.wgQuick.isNotBlank()) {
|
||||
fileUtils.createFile(config.name, config.wgQuick)
|
||||
} else null
|
||||
}
|
||||
|
||||
suspend fun createAmFiles(tunnels: Collection<TunnelConfig>): List<File> =
|
||||
tunnels.mapNotNull { config ->
|
||||
if (config.amQuick.isNotBlank()) {
|
||||
fileUtils.createFile(config.name, config.amQuick)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
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
|
||||
@@ -20,7 +18,6 @@ class TunnelViewModel
|
||||
@AssistedInject
|
||||
constructor(
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
private val tunnelManager: TunnelManager,
|
||||
@Assisted val tunnelId: Int,
|
||||
) : ContainerHost<TunnelUiState, Nothing>, ViewModel() {
|
||||
@@ -41,11 +38,6 @@ constructor(
|
||||
.collect { reduce { it } }
|
||||
}
|
||||
|
||||
fun setTunnelPingIp(ip: String) = intent {
|
||||
val tunnel = state.tunnel ?: return@intent
|
||||
tunnelRepository.save(tunnel.copy(pingTarget = ip.ifBlank { null }))
|
||||
}
|
||||
|
||||
fun setRestartOnPing(to: Boolean) = intent {
|
||||
val tunnel = state.tunnel ?: return@intent
|
||||
tunnelRepository.save(tunnel.copy(restartOnPingFailure = to))
|
||||
@@ -57,25 +49,11 @@ constructor(
|
||||
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
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import android.net.Uri
|
||||
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.domain.enums.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.QuickConfig
|
||||
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
|
||||
|
||||
@HiltViewModel
|
||||
class TunnelsViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val monitoringRepository: MonitoringSettingsRepository,
|
||||
private val fileUtils: FileUtils,
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
private val tunnelManager: TunnelManager,
|
||||
) : ContainerHost<TunnelsUiState, Nothing>, ViewModel() {
|
||||
|
||||
override val container =
|
||||
container<TunnelsUiState, Nothing>(
|
||||
TunnelsUiState(),
|
||||
buildSettings = { repeatOnSubscribedStopTimeout = 5000L },
|
||||
) {
|
||||
combine(
|
||||
tunnelRepository.userTunnelsFlow,
|
||||
tunnelManager.activeTunnels,
|
||||
monitoringRepository.flow,
|
||||
) { tunnels, activeTunnels, monitoring ->
|
||||
state.copy(
|
||||
tunnels = tunnels,
|
||||
activeTunnels = activeTunnels,
|
||||
isPingEnabled = monitoring.isPingEnabled,
|
||||
showPingStats = monitoring.showDetailedPingStats,
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
.collect { reduce { it } }
|
||||
}
|
||||
|
||||
suspend fun postSideEffect(globalSideEffect: GlobalSideEffect) {
|
||||
globalEffectRepository.post(globalSideEffect)
|
||||
}
|
||||
|
||||
fun saveSortChanges(tunnels: List<TunnelConfig>) = intent {
|
||||
tunnelRepository.saveAll(tunnels.mapIndexed { index, conf -> conf.copy(position = index) })
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
|
||||
fun importTunnelConfigs(configs: Map<QuickConfig, TunnelName>) = intent {
|
||||
try {
|
||||
val tunnelConfigs =
|
||||
configs.map { (config, name) -> TunnelConfig.tunnelConfFromQuick(config, name) }
|
||||
tunnelRepository.saveTunnelsUniquely(tunnelConfigs, state.tunnels)
|
||||
} catch (_: IOException) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.read_failed))
|
||||
)
|
||||
} catch (e: BadConfigException) {
|
||||
postSideEffect(GlobalSideEffect.Snackbar(e.asStringValue()))
|
||||
}
|
||||
}
|
||||
|
||||
fun importFromClipboard(conf: String) {
|
||||
importTunnelConfigs(mapOf(conf to null))
|
||||
}
|
||||
|
||||
fun importFromQr(conf: String) = intent { importFromClipboard(conf) }
|
||||
|
||||
fun importFromUrl(url: String) = intent {
|
||||
runCatching {
|
||||
val url = URL(url)
|
||||
val uri = url.toURI().toString().toUri()
|
||||
importFromUri(uri)
|
||||
}
|
||||
.onFailure {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.error_download_failed)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun importFromUri(uri: Uri) = intent {
|
||||
fileUtils
|
||||
.readConfigsFromUri(uri)
|
||||
.onSuccess { configs -> importTunnelConfigs(configs) }
|
||||
.onFailure {
|
||||
val message =
|
||||
when (it) {
|
||||
is IOException -> StringValue.StringResource(R.string.error_download_failed)
|
||||
else -> StringValue.StringResource(R.string.error_file_extension)
|
||||
}
|
||||
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 = TunnelConfig.tunnelConfFromQuick(selected.amQuick, selected.name)
|
||||
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<TunnelConfig>): List<File> =
|
||||
tunnels.mapNotNull { config ->
|
||||
if (config.wgQuick.isNotBlank()) {
|
||||
fileUtils.createFile(config.name, config.wgQuick)
|
||||
} else null
|
||||
}
|
||||
|
||||
suspend fun createAmFiles(tunnels: Collection<TunnelConfig>): List<File> =
|
||||
tunnels.mapNotNull { config ->
|
||||
if (config.amQuick.isNotBlank()) {
|
||||
fileUtils.createFile(config.name, config.amQuick)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
@@ -406,4 +406,5 @@
|
||||
<string name="network_name">Network: </string>
|
||||
<string name="wifi">Wi-Fi</string>
|
||||
<string name="text_too_large_for_qr">Text is too large to display QR.</string>
|
||||
<string name="all_services_disabled">Auto-tunnel and tunnels must be disabled to perform this action.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user