mirror of
https://github.com/wgtunnel/android.git
synced 2026-06-02 00:29:08 +02:00
refactor: ui state optimizations
This commit is contained in:
@@ -300,9 +300,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (
|
||||
uiState.shouldShowDonationSnackbar && !uiState.settings.alreadyDonated
|
||||
) {
|
||||
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
|
||||
viewModel.setShouldShowDonationSnackbar(false)
|
||||
snackbarState.showSnackbar(
|
||||
SnackbarInfo(
|
||||
@@ -333,7 +331,7 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (uiState.settings.appMode == AppMode.LOCK_DOWN) {
|
||||
if (uiState.appMode == AppMode.LOCK_DOWN) {
|
||||
AppAlertBanner(
|
||||
stringResource(R.string.locked_down)
|
||||
.uppercase(Locale.current.platformLocale),
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.koin.androidx.workmanager.koin.workManagerFactory
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.context.GlobalContext.startKoin
|
||||
import org.koin.core.lazyModules
|
||||
import org.koin.core.option.viewModelScopeFactory
|
||||
import org.koin.core.qualifier.named
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -48,6 +49,7 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
if (BuildConfig.DEBUG) androidLogger()
|
||||
workManagerFactory()
|
||||
modules(dispatchersModule, appModule, databaseModule, tunnelModule, workerModule)
|
||||
options(viewModelScopeFactory())
|
||||
lazyModules(networkModule)
|
||||
}
|
||||
instance = this
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
@@ -15,4 +16,16 @@ interface GeneralSettingsDao {
|
||||
|
||||
@Query("SELECT * FROM general_settings LIMIT 1")
|
||||
fun getGeneralSettingsFlow(): Flow<GeneralSettings?>
|
||||
|
||||
@Query("UPDATE general_settings SET theme = :theme WHERE id = 1")
|
||||
suspend fun updateTheme(theme: String)
|
||||
|
||||
@Query("UPDATE general_settings SET locale = :locale WHERE id = 1")
|
||||
suspend fun updateLocale(locale: String)
|
||||
|
||||
@Query("UPDATE general_settings SET is_pin_lock_enabled = :enabled WHERE id = 1")
|
||||
suspend fun updatePinLockEnabled(enabled: Boolean)
|
||||
|
||||
@Query("UPDATE general_settings SET app_mode = :appMode WHERE id = 1")
|
||||
suspend fun updateAppMode(appMode: AppMode)
|
||||
}
|
||||
|
||||
+22
-5
@@ -4,31 +4,48 @@ import com.zaneschepke.wireguardautotunnel.data.dao.GeneralSettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralSettings as Entity
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings as Domain
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RoomSettingsRepository(
|
||||
private val settingsDoa: GeneralSettingsDao,
|
||||
private val settingsDao: GeneralSettingsDao,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) : GeneralSettingRepository {
|
||||
|
||||
override suspend fun upsert(generalSettings: Domain) {
|
||||
withContext(ioDispatcher) { settingsDoa.upsert(generalSettings.toEntity()) }
|
||||
withContext(ioDispatcher) { settingsDao.upsert(generalSettings.toEntity()) }
|
||||
}
|
||||
|
||||
override val flow =
|
||||
settingsDoa
|
||||
settingsDao
|
||||
.getGeneralSettingsFlow()
|
||||
.map { (it ?: Entity()).toDomain() }
|
||||
.flowOn(ioDispatcher)
|
||||
|
||||
override suspend fun getGeneralSettings(): Domain {
|
||||
return withContext(ioDispatcher) {
|
||||
(settingsDoa.getGeneralSettings() ?: Entity()).toDomain()
|
||||
(settingsDao.getGeneralSettings() ?: Entity()).toDomain()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateTheme(theme: Theme) {
|
||||
withContext(ioDispatcher) { settingsDao.updateTheme(theme.name) }
|
||||
}
|
||||
|
||||
override suspend fun updateLocale(locale: String) {
|
||||
withContext(ioDispatcher) { settingsDao.updateLocale(locale) }
|
||||
}
|
||||
|
||||
override suspend fun updatePinLockEnabled(enabled: Boolean) {
|
||||
withContext(ioDispatcher) { settingsDao.updatePinLockEnabled(enabled) }
|
||||
}
|
||||
|
||||
override suspend fun updateAppMode(appMode: AppMode) {
|
||||
withContext(ioDispatcher) { settingsDao.updateAppMode(appMode) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
||||
@@ -30,13 +31,17 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.annotation.KoinExperimentalAPI
|
||||
import org.koin.core.module.dsl.scopedOf
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModel
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import org.koin.viewmodel.scope.viewModelScope
|
||||
|
||||
@OptIn(KoinExperimentalAPI::class)
|
||||
val appModule = module {
|
||||
single<CoroutineScope>(named(Scope.APPLICATION)) {
|
||||
CoroutineScope(SupervisorJob() + get<CoroutineDispatcher>(named(Dispatcher.DEFAULT)))
|
||||
@@ -59,11 +64,16 @@ val appModule = module {
|
||||
)
|
||||
}
|
||||
|
||||
single<ShortcutManager> { DynamicShortcutManager(androidContext(), get(named(Dispatcher.IO))) }
|
||||
single { FileUtils(androidContext(), get(named(Dispatcher.IO))) }
|
||||
single { NetworkUtils(get(named(Dispatcher.IO))) }
|
||||
viewModelScope {
|
||||
scoped { FileUtils(androidContext(), get(named(Dispatcher.IO))) }
|
||||
scoped<ShortcutManager> {
|
||||
DynamicShortcutManager(androidContext(), get(named(Dispatcher.IO)))
|
||||
}
|
||||
scopedOf(::GlobalEffectRepository)
|
||||
scopedOf(::SelectedTunnelsRepository)
|
||||
}
|
||||
|
||||
singleOf(::GlobalEffectRepository)
|
||||
single { NetworkUtils(get(named(Dispatcher.IO))) }
|
||||
|
||||
viewModelOf(::AutoTunnelViewModel)
|
||||
viewModelOf(::ConfigViewModel)
|
||||
|
||||
+10
@@ -1,6 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface GeneralSettingRepository {
|
||||
@@ -9,4 +11,12 @@ interface GeneralSettingRepository {
|
||||
val flow: Flow<GeneralSettings>
|
||||
|
||||
suspend fun getGeneralSettings(): GeneralSettings
|
||||
|
||||
suspend fun updateTheme(theme: Theme)
|
||||
|
||||
suspend fun updateLocale(locale: String)
|
||||
|
||||
suspend fun updatePinLockEnabled(enabled: Boolean)
|
||||
|
||||
suspend fun updateAppMode(appMode: AppMode)
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class SelectedTunnelsRepository {
|
||||
private val _selectedTunnelsFlow = MutableStateFlow<List<TunnelConfig>>(emptyList())
|
||||
val flow = _selectedTunnelsFlow.asStateFlow()
|
||||
|
||||
fun add(tunnelConfig: TunnelConfig) {
|
||||
_selectedTunnelsFlow.update { it.toMutableList().apply { add(tunnelConfig) } }
|
||||
}
|
||||
|
||||
fun remove(tunnelConfig: TunnelConfig) {
|
||||
_selectedTunnelsFlow.update { it.toMutableList().apply { remove(tunnelConfig) } }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_selectedTunnelsFlow.update { emptyList() }
|
||||
}
|
||||
|
||||
fun set(tunnelConfigs: List<TunnelConfig>) {
|
||||
_selectedTunnelsFlow.update { tunnelConfigs }
|
||||
}
|
||||
}
|
||||
+9
-10
@@ -23,13 +23,13 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route.*
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.TunnelNetwork
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.GlobalAppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.NavbarState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.SharedAppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
|
||||
@Composable
|
||||
fun currentRouteAsNavbarState(
|
||||
sharedState: SharedAppUiState,
|
||||
globalState: GlobalAppUiState,
|
||||
sharedViewModel: SharedAppViewModel,
|
||||
route: Route?,
|
||||
navController: NavController,
|
||||
@@ -37,7 +37,7 @@ fun currentRouteAsNavbarState(
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val context = LocalContext.current
|
||||
|
||||
return remember(route, sharedState) {
|
||||
return remember(route, globalState) {
|
||||
derivedStateOf {
|
||||
when (route) {
|
||||
AdvancedAutoTunnel ->
|
||||
@@ -70,7 +70,7 @@ fun currentRouteAsNavbarState(
|
||||
NavbarState(
|
||||
showBottomItems = true,
|
||||
topTitle =
|
||||
if (!sharedState.isLocationDisclosureShown) null
|
||||
if (!globalState.isLocationDisclosureShown) null
|
||||
else {
|
||||
context.getString(R.string.auto_tunnel)
|
||||
},
|
||||
@@ -238,7 +238,7 @@ fun currentRouteAsNavbarState(
|
||||
is Config,
|
||||
is ConfigGlobal -> {
|
||||
val tunnelName =
|
||||
if (route is Config) sharedState.tunnels.find { it.id == route.id }?.name
|
||||
if (route is Config) globalState.tunnelNames[route.id]
|
||||
else context.getString(R.string.global_dns_servers)
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
@@ -266,8 +266,7 @@ fun currentRouteAsNavbarState(
|
||||
is SplitTunnel,
|
||||
is SplitTunnelGlobal -> {
|
||||
val tunnelName =
|
||||
if (route is SplitTunnel)
|
||||
sharedState.tunnels.find { it.id == route.id }?.name
|
||||
if (route is SplitTunnel) globalState.tunnelNames[route.id]
|
||||
else context.getString(R.string.global_split_tunneling)
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
@@ -337,7 +336,7 @@ fun currentRouteAsNavbarState(
|
||||
showBottomItems = true,
|
||||
)
|
||||
is TunnelSettings -> {
|
||||
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
|
||||
val tunnelName = globalState.tunnelNames[route.id]
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
IconButton(onClick = { navController.pop() }) {
|
||||
@@ -369,7 +368,7 @@ fun currentRouteAsNavbarState(
|
||||
NavbarState(
|
||||
topTitle = context.getString(R.string.tunnels),
|
||||
topTrailing = {
|
||||
when (sharedState.selectedTunnels.size) {
|
||||
when (globalState.selectedTunnelCount) {
|
||||
0 ->
|
||||
Row {
|
||||
IconButton(onClick = { navController.push(Sort) }) {
|
||||
@@ -423,7 +422,7 @@ fun currentRouteAsNavbarState(
|
||||
}
|
||||
}
|
||||
|
||||
if (sharedState.selectedTunnels.size == 1) {
|
||||
if (globalState.selectedTunnelCount == 1) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
sharedViewModel.postSideEffect(
|
||||
|
||||
+3
-3
@@ -69,7 +69,7 @@ fun AutoTunnelScreen(
|
||||
val navController = LocalNavController.current
|
||||
val clipboard = rememberClipboardHelper()
|
||||
|
||||
val sharedUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val globalUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
if (uiState.isLoading) return
|
||||
@@ -123,9 +123,9 @@ fun AutoTunnelScreen(
|
||||
}
|
||||
|
||||
fun onAutoTunnelClick() {
|
||||
if (!sharedUiState.isBatteryOptimizationShown)
|
||||
if (!globalUiState.isBatteryOptimizationShown)
|
||||
return requestDisableBatteryOptimizations()
|
||||
viewModel.toggleAutoTunnel(sharedUiState.settings.appMode)
|
||||
viewModel.toggleAutoTunnel(globalUiState.appMode)
|
||||
}
|
||||
|
||||
SurfaceRow(
|
||||
|
||||
+11
-10
@@ -69,7 +69,7 @@ fun SettingsScreen(
|
||||
|
||||
val locale = Locale.current.platformLocale
|
||||
|
||||
val sharedUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val globalUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
if (uiState.isLoading) return
|
||||
@@ -86,7 +86,7 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
fun performBackupRestore(action: () -> Unit) {
|
||||
if (sharedUiState.activeTunnels.isNotEmpty() || sharedUiState.isAutoTunnelActive)
|
||||
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive)
|
||||
return context.showToast(R.string.all_services_disabled)
|
||||
showBackupSheet = false
|
||||
action()
|
||||
@@ -165,22 +165,22 @@ fun SettingsScreen(
|
||||
Icons.AutoMirrored.Outlined.CallSplit,
|
||||
contentDescription = null,
|
||||
tint =
|
||||
if (sharedUiState.proxyEnabled) Disabled
|
||||
if (globalUiState.appMode == AppMode.PROXY) Disabled
|
||||
else MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
},
|
||||
enabled = !sharedUiState.proxyEnabled,
|
||||
enabled = globalUiState.appMode != AppMode.PROXY,
|
||||
title = stringResource(R.string.global_split_tunneling),
|
||||
trailing = { modifier ->
|
||||
SwitchWithDivider(
|
||||
checked = uiState.settings.isGlobalSplitTunnelEnabled,
|
||||
onClick = { viewModel.setGlobalSplitTunneling(it) },
|
||||
modifier = modifier,
|
||||
enabled = !sharedUiState.proxyEnabled,
|
||||
enabled = globalUiState.appMode != AppMode.PROXY,
|
||||
)
|
||||
},
|
||||
description =
|
||||
if (sharedUiState.proxyEnabled) {
|
||||
if (globalUiState.appMode == AppMode.PROXY) {
|
||||
{
|
||||
DescriptionText(
|
||||
stringResource(R.string.unavailable_in_mode),
|
||||
@@ -211,14 +211,15 @@ fun SettingsScreen(
|
||||
Icons.Outlined.NetworkPing,
|
||||
contentDescription = null,
|
||||
tint =
|
||||
if (!sharedUiState.proxyEnabled) MaterialTheme.colorScheme.onSurface
|
||||
if (globalUiState.appMode != AppMode.PROXY)
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
else Disabled,
|
||||
)
|
||||
},
|
||||
title = stringResource(R.string.ping_monitor),
|
||||
enabled = !sharedUiState.proxyEnabled,
|
||||
enabled = globalUiState.appMode != AppMode.PROXY,
|
||||
description =
|
||||
if (sharedUiState.proxyEnabled) {
|
||||
if (globalUiState.appMode == AppMode.PROXY) {
|
||||
{
|
||||
DescriptionText(
|
||||
stringResource(R.string.unavailable_in_mode),
|
||||
@@ -230,7 +231,7 @@ fun SettingsScreen(
|
||||
SwitchWithDivider(
|
||||
checked = uiState.monitoring.isPingEnabled,
|
||||
onClick = { viewModel.setPingEnabled(it) },
|
||||
enabled = !sharedUiState.proxyEnabled,
|
||||
enabled = globalUiState.appMode != AppMode.PROXY,
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
|
||||
+4
-2
@@ -37,7 +37,9 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
val navController = LocalNavController.current
|
||||
val clipboard = rememberClipboardHelper()
|
||||
|
||||
val sharedState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val uiState by sharedViewModel.tunnelsUiState.collectAsStateWithLifecycle()
|
||||
|
||||
if (uiState.isLoading) return
|
||||
|
||||
var showExportSheet by rememberSaveable { mutableStateOf(false) }
|
||||
var showImportSheet by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -148,5 +150,5 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
)
|
||||
}
|
||||
|
||||
TunnelList(sharedState, Modifier.fillMaxSize(), sharedViewModel)
|
||||
TunnelList(uiState, Modifier.fillMaxSize(), sharedViewModel)
|
||||
}
|
||||
|
||||
+17
-13
@@ -12,7 +12,11 @@ import androidx.compose.foundation.rememberOverscrollEffect
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Circle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
@@ -25,7 +29,7 @@ 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.SharedAppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
@@ -33,7 +37,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TunnelList(
|
||||
sharedState: SharedAppUiState,
|
||||
uiState: TunnelsUiState,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SharedAppViewModel,
|
||||
) {
|
||||
@@ -48,7 +52,7 @@ fun TunnelList(
|
||||
modifier
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures {
|
||||
if (sharedState.tunnels.isEmpty()) return@detectTapGestures
|
||||
if (uiState.tunnels.isEmpty()) return@detectTapGestures
|
||||
viewModel.clearSelectedTunnels()
|
||||
}
|
||||
}
|
||||
@@ -58,7 +62,7 @@ fun TunnelList(
|
||||
reverseLayout = false,
|
||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
) {
|
||||
if (sharedState.tunnels.isEmpty()) {
|
||||
if (uiState.tunnels.isEmpty()) {
|
||||
item {
|
||||
GettingStartedLabel(
|
||||
onClick = { context.openWebUrl(it) },
|
||||
@@ -66,14 +70,14 @@ fun TunnelList(
|
||||
)
|
||||
}
|
||||
}
|
||||
items(sharedState.tunnels, key = { it.id }) { tunnel ->
|
||||
items(uiState.tunnels, key = { it.id }) { tunnel ->
|
||||
val tunnelState =
|
||||
remember(sharedState.activeTunnels) {
|
||||
sharedState.activeTunnels[tunnel.id] ?: TunnelState()
|
||||
remember(uiState.activeTunnels) {
|
||||
uiState.activeTunnels[tunnel.id] ?: TunnelState()
|
||||
}
|
||||
val selected =
|
||||
remember(sharedState.selectedTunnels) {
|
||||
sharedState.selectedTunnels.any { it.id == tunnel.id }
|
||||
remember(uiState.selectedTunnels) {
|
||||
uiState.selectedTunnels.any { it.id == tunnel.id }
|
||||
}
|
||||
var leadingIconColor by
|
||||
remember(
|
||||
@@ -97,7 +101,7 @@ fun TunnelList(
|
||||
},
|
||||
title = tunnel.name,
|
||||
onClick = {
|
||||
if (sharedState.selectedTunnels.isNotEmpty()) {
|
||||
if (uiState.selectedTunnels.isNotEmpty()) {
|
||||
viewModel.toggleSelectedTunnel(tunnel.id)
|
||||
} else {
|
||||
navController.push(Route.TunnelSettings(tunnel.id))
|
||||
@@ -111,8 +115,8 @@ fun TunnelList(
|
||||
TunnelStatisticsRow(
|
||||
tunnel,
|
||||
tunnelState,
|
||||
sharedState.isPingEnabled,
|
||||
sharedState.showPingStats,
|
||||
uiState.isPingEnabled,
|
||||
uiState.showPingStats,
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
|
||||
+8
-7
@@ -28,6 +28,7 @@ import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
|
||||
@@ -102,14 +103,14 @@ fun TunnelSettingsScreen(
|
||||
Icons.AutoMirrored.Outlined.CallSplit,
|
||||
contentDescription = null,
|
||||
tint =
|
||||
if (sharedUiState.proxyEnabled) Disabled
|
||||
if (sharedUiState.appMode == AppMode.PROXY) Disabled
|
||||
else MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
},
|
||||
enabled = !sharedUiState.proxyEnabled,
|
||||
enabled = sharedUiState.appMode != AppMode.PROXY,
|
||||
title = stringResource(R.string.splt_tunneling),
|
||||
description =
|
||||
if (sharedUiState.proxyEnabled) {
|
||||
if (sharedUiState.appMode == AppMode.PROXY) {
|
||||
{
|
||||
DescriptionText(
|
||||
stringResource(R.string.unavailable_in_mode),
|
||||
@@ -159,14 +160,14 @@ fun TunnelSettingsScreen(
|
||||
Icons.Outlined.DataUsage,
|
||||
contentDescription = null,
|
||||
tint =
|
||||
if (sharedUiState.proxyEnabled) Disabled
|
||||
if (sharedUiState.appMode == AppMode.PROXY) Disabled
|
||||
else MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
},
|
||||
title = stringResource(R.string.metered_tunnel),
|
||||
enabled = !sharedUiState.proxyEnabled,
|
||||
enabled = sharedUiState.appMode != AppMode.PROXY,
|
||||
description =
|
||||
if (sharedUiState.proxyEnabled) {
|
||||
if (sharedUiState.appMode == AppMode.PROXY) {
|
||||
{
|
||||
DescriptionText(
|
||||
stringResource(R.string.unavailable_in_mode),
|
||||
@@ -178,7 +179,7 @@ fun TunnelSettingsScreen(
|
||||
ThemedSwitch(
|
||||
checked = tunnel.isMetered,
|
||||
onClick = { viewModel.setMetered(it) },
|
||||
enabled = !sharedUiState.proxyEnabled,
|
||||
enabled = sharedUiState.appMode != AppMode.PROXY,
|
||||
)
|
||||
},
|
||||
onClick = { viewModel.setMetered(!tunnel.isMetered) },
|
||||
|
||||
+6
-4
@@ -40,12 +40,12 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@Composable
|
||||
fun SortScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
|
||||
val sharedState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val tunnelsUiState by sharedViewModel.tunnelsUiState.collectAsStateWithLifecycle()
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
|
||||
var sortAscending by rememberSaveable { mutableStateOf<Boolean?>(null) }
|
||||
var editableTunnels by rememberSaveable { mutableStateOf(sharedState.tunnels) }
|
||||
var editableTunnels by rememberSaveable { mutableStateOf(tunnelsUiState.tunnels) }
|
||||
|
||||
sharedViewModel.collectSideEffect { sideEffect ->
|
||||
when (sideEffect) {
|
||||
@@ -63,7 +63,7 @@ fun SortScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
|
||||
when (sortAscending) {
|
||||
true -> editableTunnels.sortedBy { it.name }
|
||||
false -> editableTunnels.sortedByDescending { it.name }
|
||||
null -> sharedState.tunnels
|
||||
null -> tunnelsUiState.tunnels
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
@@ -86,7 +86,9 @@ fun SortScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
|
||||
modifier =
|
||||
Modifier.pointerInput(Unit) { if (sharedState.tunnels.isEmpty()) return@pointerInput }
|
||||
Modifier.pointerInput(Unit) {
|
||||
if (tunnelsUiState.tunnels.isEmpty()) return@pointerInput
|
||||
}
|
||||
.overscroll(rememberOverscrollEffect()),
|
||||
state = lazyListState,
|
||||
userScrollEnabled = true,
|
||||
|
||||
+2
-2
@@ -31,7 +31,7 @@ fun SplitTunnelScreen(
|
||||
sharedViewModel: SharedAppViewModel = koinActivityViewModel(),
|
||||
) {
|
||||
|
||||
val sharedUiState by sharedViewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
val tunnelsUiState by sharedViewModel.tunnelsUiState.collectAsStateWithLifecycle()
|
||||
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
@@ -69,7 +69,7 @@ fun SplitTunnelScreen(
|
||||
|
||||
SelectTunnelModal(
|
||||
showDialog,
|
||||
sharedUiState.tunnels,
|
||||
tunnelsUiState.tunnels,
|
||||
onAttest = { conf ->
|
||||
if (conf == null) return@SelectTunnelModal
|
||||
effectiveTunnel = conf
|
||||
|
||||
+8
-13
@@ -1,26 +1,21 @@
|
||||
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.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||
|
||||
data class SharedAppUiState(
|
||||
data class GlobalAppUiState(
|
||||
val isAppLoaded: Boolean = false,
|
||||
val theme: Theme = Theme.AUTOMATIC,
|
||||
val locale: String = LocaleUtil.OPTION_PHONE_LANGUAGE,
|
||||
val pinLockEnabled: Boolean = false,
|
||||
val appMode: AppMode = AppMode.VPN,
|
||||
val shouldShowDonationSnackbar: Boolean = false,
|
||||
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,
|
||||
val isBatteryOptimizationShown: Boolean = false,
|
||||
val proxyEnabled: Boolean = false,
|
||||
val settings: GeneralSettings = GeneralSettings(),
|
||||
val isAutoTunnelActive: Boolean = false,
|
||||
val tunnelNames: Map<Int, String> = emptyMap(),
|
||||
val selectedTunnelCount: Int = 0,
|
||||
val alreadyDonated: Boolean = false,
|
||||
val isPinVerified: Boolean = false,
|
||||
)
|
||||
@@ -15,4 +15,5 @@ data class SettingUiState(
|
||||
val globalTunnelConfig: TunnelConfig? = null,
|
||||
val tunnels: List<TunnelConfig> = emptyList(),
|
||||
val monitoring: MonitoringSettings = MonitoringSettings(),
|
||||
val tunnelActive: Boolean = false,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
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 activeTunnels: Map<Int, TunnelState> = emptyMap(),
|
||||
val selectedTunnels: List<TunnelConfig> = emptyList(),
|
||||
val isPingEnabled: Boolean = false,
|
||||
val showPingStats: Boolean = false,
|
||||
val isLoading: Boolean = true,
|
||||
)
|
||||
|
||||
+4
-4
@@ -5,17 +5,17 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
|
||||
suspend fun TunnelRepository.saveTunnelsUniquely(
|
||||
tunnels: List<TunnelConfig>,
|
||||
existing: List<TunnelConfig>,
|
||||
existingNames: List<String>,
|
||||
) {
|
||||
val uniqueTunnels = generateUniquelyNamedConfigs(tunnels, existing)
|
||||
val uniqueTunnels = generateUniquelyNamedConfigs(tunnels, existingNames)
|
||||
saveAll(uniqueTunnels)
|
||||
}
|
||||
|
||||
private fun generateUniquelyNamedConfigs(
|
||||
incoming: List<TunnelConfig>,
|
||||
existing: List<TunnelConfig>,
|
||||
existingNames: List<String>,
|
||||
): List<TunnelConfig> {
|
||||
val usedNames = existing.map { it.name }.toMutableSet()
|
||||
val usedNames = existingNames.toMutableSet()
|
||||
val result = mutableListOf<TunnelConfig>()
|
||||
val regex = Regex("(.+)\\s*\\((\\d+)\\)$")
|
||||
|
||||
|
||||
+7
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
@@ -11,6 +12,8 @@ import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.SettingUiState
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.orbitmvi.orbit.ContainerHost
|
||||
import org.orbitmvi.orbit.viewmodel.container
|
||||
|
||||
@@ -20,6 +23,7 @@ class SettingsViewModel(
|
||||
private val tunnelsRepository: TunnelRepository,
|
||||
private val monitoringRepository: MonitoringSettingsRepository,
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
private val tunnelManager: TunnelManager,
|
||||
) : ContainerHost<SettingUiState, Nothing>, ViewModel() {
|
||||
|
||||
override val container =
|
||||
@@ -33,13 +37,15 @@ class SettingsViewModel(
|
||||
tunnelsRepository.globalTunnelFlow,
|
||||
tunnelsRepository.userTunnelsFlow,
|
||||
monitoringRepository.flow,
|
||||
) { settings, tunnel, tunnels, monitoring ->
|
||||
tunnelManager.activeTunnels.map { it.isNotEmpty() }.distinctUntilChanged(),
|
||||
) { settings, tunnel, tunnels, monitoring, tunnelActive ->
|
||||
state.copy(
|
||||
settings = settings,
|
||||
remoteKey = settings.remoteKey,
|
||||
isRemoteEnabled = settings.isRemoteControlEnabled,
|
||||
isPinLockEnabled = settings.isPinLockEnabled,
|
||||
isLoading = false,
|
||||
tunnelActive = tunnelActive,
|
||||
globalTunnelConfig = tunnel,
|
||||
monitoring = monitoring,
|
||||
tunnels = tunnels,
|
||||
|
||||
+77
-59
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
@@ -13,10 +14,12 @@ 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.SelectedTunnelsRepository
|
||||
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.state.GlobalAppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||
@@ -32,9 +35,13 @@ import io.ktor.client.statement.bodyAsText
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.time.Instant
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.amnezia.awg.config.BadConfigException
|
||||
import org.orbitmvi.orbit.ContainerHost
|
||||
import org.orbitmvi.orbit.viewmodel.container
|
||||
@@ -48,41 +55,64 @@ class SharedAppViewModel(
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val monitoringSettingsRepository: MonitoringSettingsRepository,
|
||||
private val selectedTunnelsRepository: SelectedTunnelsRepository,
|
||||
monitoringSettingsRepository: MonitoringSettingsRepository,
|
||||
private val rootShellUtils: RootShellUtils,
|
||||
private val httpClient: HttpClient,
|
||||
private val fileUtils: FileUtils,
|
||||
) : ContainerHost<SharedAppUiState, LocalSideEffect>, ViewModel() {
|
||||
) : ContainerHost<GlobalAppUiState, LocalSideEffect>, ViewModel() {
|
||||
|
||||
val globalSideEffect = globalEffectRepository.flow
|
||||
|
||||
val tunnelsUiState =
|
||||
combine(
|
||||
tunnelRepository.userTunnelsFlow,
|
||||
monitoringSettingsRepository.flow,
|
||||
tunnelManager.activeTunnels,
|
||||
selectedTunnelsRepository.flow,
|
||||
) { tunnels, monitoringSettings, activeTuns, selectedTuns ->
|
||||
TunnelsUiState(
|
||||
tunnels = tunnels,
|
||||
isPingEnabled = monitoringSettings.isPingEnabled,
|
||||
showPingStats = monitoringSettings.showDetailedPingStats,
|
||||
activeTunnels = activeTuns,
|
||||
selectedTunnels = selectedTuns,
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), TunnelsUiState())
|
||||
|
||||
override val container =
|
||||
container<SharedAppUiState, LocalSideEffect>(
|
||||
SharedAppUiState(),
|
||||
buildSettings = { repeatOnSubscribedStopTimeout = 5000L },
|
||||
container<GlobalAppUiState, LocalSideEffect>(
|
||||
GlobalAppUiState(),
|
||||
buildSettings = { repeatOnSubscribedStopTimeout = 5_000L },
|
||||
) {
|
||||
intent {
|
||||
combine(
|
||||
tunnelRepository.userTunnelsFlow,
|
||||
serviceManager.autoTunnelService.map { it != null },
|
||||
tunnelRepository.userTunnelsFlow
|
||||
.map { tuns -> tuns.associate { it.id to it.name } }
|
||||
.distinctUntilChanged(),
|
||||
serviceManager.autoTunnelService.map { it != null }.distinctUntilChanged(),
|
||||
settingsRepository.flow,
|
||||
tunnelManager.activeTunnels,
|
||||
monitoringSettingsRepository.flow,
|
||||
) { tunnels, autoTunnelActive, settings, activeTunnels, monitoring ->
|
||||
tunnelsUiState
|
||||
.map { Pair(it.isLoading, it.selectedTunnels.size) }
|
||||
.distinctUntilChanged(),
|
||||
appStateRepository.flow,
|
||||
) { tunNames, autoTunnelActive, settings, (loading, selectedTunCount), appState
|
||||
->
|
||||
state.copy(
|
||||
theme = settings.theme,
|
||||
appMode = settings.appMode,
|
||||
locale = settings.locale ?: LocaleUtil.OPTION_PHONE_LANGUAGE,
|
||||
tunnelNames = tunNames,
|
||||
alreadyDonated = settings.alreadyDonated,
|
||||
isLocationDisclosureShown = appState.isLocationDisclosureShown,
|
||||
isBatteryOptimizationShown = appState.isBatteryOptimizationDisableShown,
|
||||
shouldShowDonationSnackbar = appState.shouldShowDonationSnackbar,
|
||||
selectedTunnelCount = selectedTunCount,
|
||||
pinLockEnabled = settings.isPinLockEnabled,
|
||||
isAutoTunnelActive = autoTunnelActive,
|
||||
settings = settings,
|
||||
tunnels = tunnels,
|
||||
activeTunnels = activeTunnels,
|
||||
isPingEnabled = monitoring.isPingEnabled,
|
||||
showPingStats = monitoring.showDetailedPingStats,
|
||||
proxyEnabled =
|
||||
settings.appMode == AppMode.PROXY ||
|
||||
settings.appMode == AppMode.LOCK_DOWN,
|
||||
isAppLoaded = true,
|
||||
isAppLoaded = !loading,
|
||||
)
|
||||
}
|
||||
.collect { newState -> reduce { newState } }
|
||||
@@ -94,18 +124,6 @@ class SharedAppViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
intent {
|
||||
appStateRepository.flow.collect {
|
||||
reduce {
|
||||
state.copy(
|
||||
isLocationDisclosureShown = it.isLocationDisclosureShown,
|
||||
isBatteryOptimizationShown = it.isBatteryOptimizationDisableShown,
|
||||
shouldShowDonationSnackbar = it.shouldShowDonationSnackbar,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
intent {
|
||||
tunnelManager.messageEvents.collect { (_, message) ->
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message.toStringValue()))
|
||||
@@ -114,7 +132,7 @@ class SharedAppViewModel(
|
||||
}
|
||||
|
||||
fun startTunnel(tunnelConfig: TunnelConfig) = intent {
|
||||
if (state.settings.appMode == AppMode.VPN) {
|
||||
if (state.appMode == AppMode.VPN) {
|
||||
if (!serviceManager.hasVpnPermission())
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.RequestVpnPermission(AppMode.VPN, tunnelConfig)
|
||||
@@ -131,18 +149,16 @@ class SharedAppViewModel(
|
||||
appStateRepository.setLocationDisclosureShown(true)
|
||||
}
|
||||
|
||||
fun setTheme(theme: Theme) = intent {
|
||||
settingsRepository.upsert(state.settings.copy(theme = theme))
|
||||
}
|
||||
fun setTheme(theme: Theme) = intent { settingsRepository.updateTheme(theme) }
|
||||
|
||||
fun setLocale(locale: String) = intent {
|
||||
settingsRepository.upsert(state.settings.copy(locale = locale))
|
||||
settingsRepository.updateLocale(locale)
|
||||
postSideEffect(GlobalSideEffect.ConfigChanged)
|
||||
}
|
||||
|
||||
fun setPinLockEnabled(enabled: Boolean) = intent {
|
||||
if (!enabled) PinManager.clearPin()
|
||||
settingsRepository.upsert(state.settings.copy(isPinLockEnabled = enabled))
|
||||
settingsRepository.updatePinLockEnabled(enabled)
|
||||
}
|
||||
|
||||
fun stopTunnel(tunnelConfig: TunnelConfig) = intent {
|
||||
@@ -184,7 +200,7 @@ class SharedAppViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
settingsRepository.upsert(state.settings.copy(appMode = appMode))
|
||||
settingsRepository.updateAppMode(appMode)
|
||||
}
|
||||
|
||||
fun setShouldShowDonationSnackbar(to: Boolean) = intent {
|
||||
@@ -223,7 +239,7 @@ class SharedAppViewModel(
|
||||
try {
|
||||
val tunnelConfigs =
|
||||
configs.map { (config, name) -> TunnelConfig.tunnelConfFromQuick(config, name) }
|
||||
tunnelRepository.saveTunnelsUniquely(tunnelConfigs, state.tunnels)
|
||||
tunnelRepository.saveTunnelsUniquely(tunnelConfigs, state.tunnelNames.map { it.value })
|
||||
} catch (_: IOException) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.read_failed))
|
||||
@@ -274,56 +290,58 @@ class SharedAppViewModel(
|
||||
}
|
||||
|
||||
fun toggleSelectAllTunnels() = intent {
|
||||
if (state.selectedTunnels.size != state.tunnels.size) {
|
||||
return@intent reduce { state.copy(selectedTunnels = state.tunnels) }
|
||||
if (state.selectedTunnelCount != state.tunnelNames.size) {
|
||||
val tunnels = tunnelRepository.getAll()
|
||||
selectedTunnelsRepository.set(tunnels)
|
||||
return@intent
|
||||
}
|
||||
reduce { state.copy(selectedTunnels = emptyList()) }
|
||||
selectedTunnelsRepository.clear()
|
||||
}
|
||||
|
||||
fun clearSelectedTunnels() = intent { reduce { state.copy(selectedTunnels = emptyList()) } }
|
||||
fun clearSelectedTunnels() = intent { selectedTunnelsRepository.clear() }
|
||||
|
||||
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 })
|
||||
}
|
||||
)
|
||||
}
|
||||
val (selectedTuns, tunnels) = tunnelsUiState.value.run { Pair(selectedTunnels, tunnels) }
|
||||
val selected =
|
||||
selectedTuns.toMutableList().apply {
|
||||
val removed = removeIf { it.id == tunnelId }
|
||||
if (!removed) addAll(tunnels.filter { it.id == tunnelId })
|
||||
}
|
||||
selectedTunnelsRepository.set(selected)
|
||||
}
|
||||
|
||||
fun deleteSelectedTunnels() = intent {
|
||||
val activeTunIds = tunnelManager.activeTunnels.firstOrNull()?.map { it.key }
|
||||
if (state.selectedTunnels.any { activeTunIds?.contains(it.id) == true })
|
||||
val selectedTuns = tunnelsUiState.value.selectedTunnels
|
||||
if (selectedTuns.any { activeTunIds?.contains(it.id) == true })
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.delete_active_message)
|
||||
)
|
||||
)
|
||||
tunnelRepository.delete(state.selectedTunnels)
|
||||
tunnelRepository.delete(selectedTuns)
|
||||
clearSelectedTunnels()
|
||||
}
|
||||
|
||||
fun copySelectedTunnel() = intent {
|
||||
val selected = state.selectedTunnels.firstOrNull() ?: return@intent
|
||||
val selected = tunnelsUiState.value.selectedTunnels.firstOrNull() ?: return@intent
|
||||
val copy = TunnelConfig.tunnelConfFromQuick(selected.amQuick, selected.name)
|
||||
tunnelRepository.saveTunnelsUniquely(listOf(copy), state.tunnels)
|
||||
tunnelRepository.saveTunnelsUniquely(listOf(copy), state.tunnelNames.map { it.value })
|
||||
clearSelectedTunnels()
|
||||
}
|
||||
|
||||
fun exportSelectedTunnels(configType: ConfigType, uri: Uri?) = intent {
|
||||
val selectedTunnels = tunnelsUiState.value.selectedTunnels
|
||||
val (files, shareFileName) =
|
||||
when (configType) {
|
||||
ConfigType.AM ->
|
||||
Pair(
|
||||
createAmFiles(state.selectedTunnels),
|
||||
createAmFiles(selectedTunnels),
|
||||
"am-export_${Instant.now().epochSecond}.zip",
|
||||
)
|
||||
ConfigType.WG ->
|
||||
Pair(
|
||||
createWgFiles(state.selectedTunnels),
|
||||
createWgFiles(selectedTunnels),
|
||||
"wg-export_${Instant.now().epochSecond}.zip",
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user