From bbc62a26e704578c7321c22367fe4652083edfc5 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Thu, 25 Dec 2025 00:06:53 -0500 Subject: [PATCH] refactor: ui state optimizations --- .../wireguardautotunnel/MainActivity.kt | 6 +- .../WireGuardAutoTunnel.kt | 2 + .../data/dao/GeneralSettingsDao.kt | 13 ++ .../data/repository/RoomSettingsRepository.kt | 27 +++- .../wireguardautotunnel/di/AppModule.kt | 18 ++- .../repository/GeneralSettingRepository.kt | 10 ++ .../repository/SelectedTunnelsRepository.kt | 27 ++++ .../currentBackStackEntryAsNavbarState.kt | 19 ++- .../ui/screens/autotunnel/AutoTunnelScreen.kt | 6 +- .../ui/screens/settings/SettingsScreen.kt | 21 +-- .../ui/screens/tunnels/TunnelsScreen.kt | 6 +- .../screens/tunnels/components/TunnelList.kt | 30 ++-- .../tunnels/settings/TunnelSettingsScreen.kt | 15 +- .../ui/screens/tunnels/sort/SortScreen.kt | 10 +- .../tunnels/splittunnel/SplitTunnelScreen.kt | 4 +- ...haredAppUiState.kt => GlobalAppUiState.kt} | 21 ++- .../ui/state/SettingUiState.kt | 1 + .../ui/state/TunnelsUiState.kt | 5 + .../util/extensions/RepositoryExtensions.kt | 8 +- .../viewmodel/SettingsViewModel.kt | 8 +- .../viewmodel/SharedAppViewModel.kt | 136 ++++++++++-------- 21 files changed, 252 insertions(+), 141 deletions(-) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/SelectedTunnelsRepository.kt rename app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/{SharedAppUiState.kt => GlobalAppUiState.kt} (50%) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt index b722426b..441d848a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt @@ -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), diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt index 46f5580b..7239c595 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/GeneralSettingsDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/GeneralSettingsDao.kt index 64c3c659..f4f799c6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/GeneralSettingsDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/dao/GeneralSettingsDao.kt @@ -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 + + @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) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt index 6114fa7c..fdeb8138 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt @@ -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) } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/AppModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/AppModule.kt index 5d1f0b66..af624beb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/AppModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/AppModule.kt @@ -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(named(Scope.APPLICATION)) { CoroutineScope(SupervisorJob() + get(named(Dispatcher.DEFAULT))) @@ -59,11 +64,16 @@ val appModule = module { ) } - single { 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 { + DynamicShortcutManager(androidContext(), get(named(Dispatcher.IO))) + } + scopedOf(::GlobalEffectRepository) + scopedOf(::SelectedTunnelsRepository) + } - singleOf(::GlobalEffectRepository) + single { NetworkUtils(get(named(Dispatcher.IO))) } viewModelOf(::AutoTunnelViewModel) viewModelOf(::ConfigViewModel) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/GeneralSettingRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/GeneralSettingRepository.kt index 1879e2ad..3abff95a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/GeneralSettingRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/GeneralSettingRepository.kt @@ -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 suspend fun getGeneralSettings(): GeneralSettings + + suspend fun updateTheme(theme: Theme) + + suspend fun updateLocale(locale: String) + + suspend fun updatePinLockEnabled(enabled: Boolean) + + suspend fun updateAppMode(appMode: AppMode) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/SelectedTunnelsRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/SelectedTunnelsRepository.kt new file mode 100644 index 00000000..d65f63aa --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/repository/SelectedTunnelsRepository.kt @@ -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>(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) { + _selectedTunnelsFlow.update { tunnelConfigs } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt index 773a6918..642f3e2d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt @@ -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( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/AutoTunnelScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/AutoTunnelScreen.kt index 422a09e5..47874594 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/AutoTunnelScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/AutoTunnelScreen.kt @@ -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( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index cdd578e0..f225940f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -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, ) }, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/TunnelsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/TunnelsScreen.kt index f64688e8..3559b8b4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/TunnelsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/TunnelsScreen.kt @@ -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) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/components/TunnelList.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/components/TunnelList.kt index c7b1a0da..c2fd5349 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/components/TunnelList.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/components/TunnelList.kt @@ -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, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/TunnelSettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/TunnelSettingsScreen.kt index 5a556fb8..111166a6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/TunnelSettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/settings/TunnelSettingsScreen.kt @@ -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) }, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/sort/SortScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/sort/SortScreen.kt index 27cf929c..22983c34 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/sort/SortScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/sort/SortScreen.kt @@ -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(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, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/splittunnel/SplitTunnelScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/splittunnel/SplitTunnelScreen.kt index 38d5ed57..591e80b1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/splittunnel/SplitTunnelScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/splittunnel/SplitTunnelScreen.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/SharedAppUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/GlobalAppUiState.kt similarity index 50% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/SharedAppUiState.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/GlobalAppUiState.kt index dc2f8e68..42f70825 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/SharedAppUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/GlobalAppUiState.kt @@ -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 = emptyList(), - val selectedTunnels: List = emptyList(), - val activeTunnels: Map = 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 = emptyMap(), + val selectedTunnelCount: Int = 0, + val alreadyDonated: Boolean = false, + val isPinVerified: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/SettingUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/SettingUiState.kt index 3c76f743..b63c71d4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/SettingUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/SettingUiState.kt @@ -15,4 +15,5 @@ data class SettingUiState( val globalTunnelConfig: TunnelConfig? = null, val tunnels: List = emptyList(), val monitoring: MonitoringSettings = MonitoringSettings(), + val tunnelActive: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/TunnelsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/TunnelsUiState.kt index b1a42513..1b6ead92 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/TunnelsUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/TunnelsUiState.kt @@ -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 = emptyList(), + val activeTunnels: Map = emptyMap(), + val selectedTunnels: List = emptyList(), + val isPingEnabled: Boolean = false, + val showPingStats: Boolean = false, val isLoading: Boolean = true, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/RepositoryExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/RepositoryExtensions.kt index 161b4a5d..09c02cfa 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/RepositoryExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/RepositoryExtensions.kt @@ -5,17 +5,17 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository suspend fun TunnelRepository.saveTunnelsUniquely( tunnels: List, - existing: List, + existingNames: List, ) { - val uniqueTunnels = generateUniquelyNamedConfigs(tunnels, existing) + val uniqueTunnels = generateUniquelyNamedConfigs(tunnels, existingNames) saveAll(uniqueTunnels) } private fun generateUniquelyNamedConfigs( incoming: List, - existing: List, + existingNames: List, ): List { - val usedNames = existing.map { it.name }.toMutableSet() + val usedNames = existingNames.toMutableSet() val result = mutableListOf() val regex = Regex("(.+)\\s*\\((\\d+)\\)$") diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SettingsViewModel.kt index b479fbc8..17d7460b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SettingsViewModel.kt @@ -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, 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, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SharedAppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SharedAppViewModel.kt index 7be11da7..b5aab390 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SharedAppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SharedAppViewModel.kt @@ -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, ViewModel() { +) : ContainerHost, 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(), - buildSettings = { repeatOnSubscribedStopTimeout = 5000L }, + container( + 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", ) }