mirror of
https://github.com/wgtunnel/android.git
synced 2026-06-02 00:29:08 +02:00
feat: global config overrides (#983)
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 23,
|
||||
"identityHash": "c94fe51e6c318edf8bda81ab854c85e5",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `app_mode` INTEGER NOT NULL DEFAULT 0, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `is_tunnel_globals_enabled` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWildcardsEnabled",
|
||||
"columnName": "is_wildcards_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isStopOnNoInternetEnabled",
|
||||
"columnName": "is_stop_on_no_internet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLanOnKillSwitchEnabled",
|
||||
"columnName": "is_lan_on_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "debounceDelaySeconds",
|
||||
"columnName": "debounce_delay_seconds",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "3"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
|
||||
"columnName": "is_disable_kill_switch_on_trusted_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnUnsecureEnabled",
|
||||
"columnName": "is_tunnel_on_unsecure_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wifiDetectionMethod",
|
||||
"columnName": "wifi_detection_method",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingMonitoringEnabled",
|
||||
"columnName": "is_ping_monitoring_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelPingIntervalSeconds",
|
||||
"columnName": "tunnel_ping_interval_sec",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "30"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelPingAttempts",
|
||||
"columnName": "tunnel_ping_attempts",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "3"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelPingTimeoutSeconds",
|
||||
"columnName": "tunnel_ping_timeout_sec",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "appMode",
|
||||
"columnName": "app_mode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dnsProtocol",
|
||||
"columnName": "dns_protocol",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dnsEndpoint",
|
||||
"columnName": "dns_endpoint",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelGlobalsEnabled",
|
||||
"columnName": "is_tunnel_globals_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `restart_on_ping_failure` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]')",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amQuick",
|
||||
"columnName": "am_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "is_Active",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "restartOnPingFailure",
|
||||
"columnName": "restart_on_ping_failure",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingTarget",
|
||||
"columnName": "ping_target",
|
||||
"affinity": "TEXT",
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isEthernetTunnel",
|
||||
"columnName": "is_ethernet_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isIpv4Preferred",
|
||||
"columnName": "is_ipv4_preferred",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "true"
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "position",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoTunnelApps",
|
||||
"columnName": "auto_tunnel_apps",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'[]'"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "proxy_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "socks5ProxyEnabled",
|
||||
"columnName": "socks5_proxy_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "socks5ProxyBindAddress",
|
||||
"columnName": "socks5_proxy_bind_address",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpProxyEnabled",
|
||||
"columnName": "http_proxy_enable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "httpProxyBindAddress",
|
||||
"columnName": "http_proxy_bind_address",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "proxyUsername",
|
||||
"columnName": "proxy_username",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "proxyPassword",
|
||||
"columnName": "proxy_password",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c94fe51e6c318edf8bda81ab854c85e5')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.TunnelGlobalsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.LogsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.SystemFeaturesScreen
|
||||
@@ -304,11 +305,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
composable<Route.Config> { backStackEntry ->
|
||||
val args = backStackEntry.toRoute<Route.Config>()
|
||||
val viewModel =
|
||||
backStackEntry.sharedViewModel<TunnelsViewModel>(
|
||||
navController
|
||||
)
|
||||
ConfigScreen(args.id, viewModel)
|
||||
ConfigScreen(args.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,6 +365,19 @@ class MainActivity : AppCompatActivity() {
|
||||
it.sharedViewModel<SettingsViewModel>(navController)
|
||||
DnsSettingsScreen(viewModel)
|
||||
}
|
||||
composable<Route.TunnelGlobals> { backStackEntry ->
|
||||
val args = backStackEntry.toRoute<Route.TunnelGlobals>()
|
||||
TunnelGlobalsScreen(args.id)
|
||||
}
|
||||
composable<Route.ConfigGlobal> { backStackEntry ->
|
||||
val args = backStackEntry.toRoute<Route.ConfigGlobal>()
|
||||
ConfigScreen(args.id)
|
||||
}
|
||||
composable<Route.SplitTunnelGlobal> { backStackEntry ->
|
||||
val args =
|
||||
backStackEntry.toRoute<Route.SplitTunnelGlobal>()
|
||||
SplitTunnelScreen(args.id)
|
||||
}
|
||||
composable<Route.ProxySettings> { ProxySettingsScreen() }
|
||||
composable<Route.Appearance> { AppearanceScreen() }
|
||||
composable<Route.Language> { LanguageScreen() }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.di.*
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
|
||||
@@ -57,6 +58,28 @@ constructor(
|
||||
val condition: (SideEffectState) -> Boolean,
|
||||
)
|
||||
|
||||
private val settings: StateFlow<GeneralSettings> =
|
||||
settingsRepository.flow
|
||||
.filterNotNull()
|
||||
.stateIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = GeneralSettings(),
|
||||
)
|
||||
|
||||
private val tunnels: StateFlow<List<TunnelConf>> =
|
||||
tunnelsRepository.flow.stateIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
private suspend fun getSettings(): GeneralSettings =
|
||||
settingsRepository.flow.filterNotNull().first { it != GeneralSettings() }
|
||||
|
||||
private suspend fun getTunnels(): List<TunnelConf> =
|
||||
tunnelsRepository.flow.first { it.isNotEmpty() }
|
||||
|
||||
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
|
||||
val currentBackend = AtomicReference(userspaceTunnel)
|
||||
val currentSettings = AtomicReference(GeneralSettings())
|
||||
@@ -216,7 +239,17 @@ constructor(
|
||||
// for VPN Mode, we need to stop active tunnels as we can only have one active at a time
|
||||
if (activeTunnels.value.isNotEmpty() && tunnelProviderFlow.value == userspaceTunnel)
|
||||
stopActiveTunnels()
|
||||
tunnelProviderFlow.value.startTunnel(tunnelConf)
|
||||
val runConfig =
|
||||
tunnelConf.run {
|
||||
if (getSettings().isTunnelGlobalsEnabled) {
|
||||
val globalTunnel =
|
||||
getTunnels().firstOrNull { it.tunName == TunnelConfig.GLOBAL_CONFIG_NAME }
|
||||
?: return@run this
|
||||
return@run copyWithGlobalValues(globalTunnel)
|
||||
}
|
||||
this
|
||||
}
|
||||
tunnelProviderFlow.value.startTunnel(runConfig)
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel(tunnelId: Int) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class, ProxySettings::class],
|
||||
version = 22,
|
||||
version = 23,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
@@ -36,6 +36,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
AutoMigration(from = 19, to = 20, spec = ProxyMigration::class),
|
||||
AutoMigration(from = 20, to = 21, spec = FixProxySettingsMigration::class),
|
||||
AutoMigration(from = 21, to = 22),
|
||||
AutoMigration(from = 22, to = 23),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
|
||||
@@ -71,4 +71,12 @@ interface TunnelConfigDao {
|
||||
|
||||
@Query("SELECT * FROM tunnelconfig ORDER BY position")
|
||||
fun getAllFlow(): Flow<List<TunnelConfig>>
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE name != :globalName")
|
||||
fun getAllTunnelsExceptGlobal(
|
||||
globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME
|
||||
): Flow<List<TunnelConfig>>
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE name = :globalName LIMIT 1")
|
||||
fun getGlobalTunnel(globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME): Flow<TunnelConfig?>
|
||||
}
|
||||
|
||||
@@ -53,4 +53,6 @@ data class Settings(
|
||||
@ColumnInfo(name = "dns_protocol", defaultValue = "0")
|
||||
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
|
||||
@ColumnInfo(name = "dns_endpoint") val dnsEndpoint: String? = null,
|
||||
@ColumnInfo(name = "is_tunnel_globals_enabled", defaultValue = "0")
|
||||
val isTunnelGlobalsEnabled: Boolean = false,
|
||||
)
|
||||
|
||||
@@ -32,5 +32,6 @@ data class TunnelConfig(
|
||||
|
||||
companion object {
|
||||
const val AM_QUICK_DEFAULT = ""
|
||||
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ fun Settings.toAppSettings(): GeneralSettings {
|
||||
appMode = appMode,
|
||||
dnsProtocol = dnsProtocol,
|
||||
dnsEndpoint = dnsEndpoint,
|
||||
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,6 +62,7 @@ fun GeneralSettings.toSettings(): Settings {
|
||||
appMode = appMode,
|
||||
dnsProtocol = dnsProtocol,
|
||||
dnsEndpoint = dnsEndpoint,
|
||||
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+11
@@ -7,6 +7,7 @@ import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -21,6 +22,16 @@ class RoomTunnelRepository(
|
||||
it.map(TunnelConfigMapper::toTunnelConf)
|
||||
}
|
||||
|
||||
override val userTunnelsFlow =
|
||||
tunnelConfigDao.getAllTunnelsExceptGlobal().flowOn(ioDispatcher).map {
|
||||
it.map(TunnelConfigMapper::toTunnelConf)
|
||||
}
|
||||
|
||||
override val globalTunnelFlow: Flow<TunnelConf?> =
|
||||
tunnelConfigDao.getGlobalTunnel().flowOn(ioDispatcher).map {
|
||||
it?.let(TunnelConfigMapper::toTunnelConf)
|
||||
}
|
||||
|
||||
override suspend fun getAll(): Tunnels {
|
||||
return withContext(ioDispatcher) {
|
||||
tunnelConfigDao.getAll().map(TunnelConfigMapper::toTunnelConf)
|
||||
|
||||
@@ -31,6 +31,7 @@ data class GeneralSettings(
|
||||
val appMode: AppMode = AppMode.VPN,
|
||||
val dnsProtocol: DnsProtocol = DnsProtocol.SYSTEM,
|
||||
val dnsEndpoint: String? = null,
|
||||
val isTunnelGlobalsEnabled: Boolean = false,
|
||||
) {
|
||||
fun toAutoTunnelStateString(): String {
|
||||
return """
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.model
|
||||
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig.Companion.GLOBAL_CONFIG_NAME
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.defaultName
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import org.amnezia.awg.config.InetEndpoint
|
||||
import org.amnezia.awg.config.InetNetwork
|
||||
import org.amnezia.awg.config.Interface
|
||||
import org.amnezia.awg.config.Peer
|
||||
import org.amnezia.awg.crypto.KeyPair
|
||||
|
||||
data class TunnelConf(
|
||||
val id: Int = 0,
|
||||
@@ -60,6 +66,127 @@ data class TunnelConf(
|
||||
return configFromWgQuick(wgQuick)
|
||||
}
|
||||
|
||||
fun copyWithGlobalValues(globalTunnel: TunnelConf): TunnelConf {
|
||||
val existingConfig = toAmConfig()
|
||||
val globalConfig = globalTunnel.toAmConfig()
|
||||
|
||||
val newInterfaceBuilder =
|
||||
Interface.Builder().apply {
|
||||
setKeyPair(existingConfig.`interface`.keyPair)
|
||||
setAddresses(existingConfig.`interface`.addresses)
|
||||
setDnsServers(existingConfig.`interface`.dnsServers)
|
||||
setDnsSearchDomains(existingConfig.`interface`.dnsSearchDomains)
|
||||
setExcludedApplications(existingConfig.`interface`.excludedApplications)
|
||||
setIncludedApplications(existingConfig.`interface`.includedApplications)
|
||||
existingConfig.`interface`.listenPort.ifPresent { setListenPort(it) }
|
||||
existingConfig.`interface`.mtu.ifPresent { setMtu(it) }
|
||||
existingConfig.`interface`.junkPacketCount.ifPresent { setJunkPacketCount(it) }
|
||||
existingConfig.`interface`.junkPacketMinSize.ifPresent { setJunkPacketMinSize(it) }
|
||||
existingConfig.`interface`.junkPacketMaxSize.ifPresent { setJunkPacketMaxSize(it) }
|
||||
existingConfig.`interface`.initPacketJunkSize.ifPresent {
|
||||
setInitPacketJunkSize(it)
|
||||
}
|
||||
existingConfig.`interface`.responsePacketJunkSize.ifPresent {
|
||||
setResponsePacketJunkSize(it)
|
||||
}
|
||||
existingConfig.`interface`.initPacketMagicHeader.ifPresent {
|
||||
setInitPacketMagicHeader(it)
|
||||
}
|
||||
existingConfig.`interface`.responsePacketMagicHeader.ifPresent {
|
||||
setResponsePacketMagicHeader(it)
|
||||
}
|
||||
existingConfig.`interface`.underloadPacketMagicHeader.ifPresent {
|
||||
setUnderloadPacketMagicHeader(it)
|
||||
}
|
||||
existingConfig.`interface`.transportPacketMagicHeader.ifPresent {
|
||||
setTransportPacketMagicHeader(it)
|
||||
}
|
||||
existingConfig.`interface`.i1.ifPresent { setI1(it) }
|
||||
existingConfig.`interface`.i2.ifPresent { setI2(it) }
|
||||
existingConfig.`interface`.i3.ifPresent { setI3(it) }
|
||||
existingConfig.`interface`.i4.ifPresent { setI4(it) }
|
||||
existingConfig.`interface`.i5.ifPresent { setI5(it) }
|
||||
existingConfig.`interface`.j1.ifPresent { setJ1(it) }
|
||||
existingConfig.`interface`.j2.ifPresent { setJ2(it) }
|
||||
existingConfig.`interface`.j3.ifPresent { setJ3(it) }
|
||||
existingConfig.`interface`.itime.ifPresent { setItime(it) }
|
||||
setPreUp(existingConfig.`interface`.preUp)
|
||||
setPostUp(existingConfig.`interface`.postUp)
|
||||
setPreDown(existingConfig.`interface`.preDown)
|
||||
setPostDown(existingConfig.`interface`.postDown)
|
||||
|
||||
globalConfig.`interface`.mtu.ifPresent { setMtu(it) }
|
||||
if (globalConfig.`interface`.dnsServers.isNotEmpty()) {
|
||||
setDnsServers(globalConfig.`interface`.dnsServers)
|
||||
}
|
||||
if (globalConfig.`interface`.dnsSearchDomains.isNotEmpty()) {
|
||||
setDnsSearchDomains(globalConfig.`interface`.dnsSearchDomains)
|
||||
}
|
||||
|
||||
if (globalConfig.`interface`.excludedApplications.isNotEmpty()) {
|
||||
setExcludedApplications(globalConfig.`interface`.excludedApplications)
|
||||
}
|
||||
if (!globalConfig.`interface`.includedApplications.isEmpty()) {
|
||||
setIncludedApplications(globalConfig.`interface`.includedApplications)
|
||||
}
|
||||
|
||||
if (globalConfig.`interface`.preUp.isNotEmpty()) {
|
||||
setPreUp(globalConfig.`interface`.preUp)
|
||||
}
|
||||
if (globalConfig.`interface`.postUp.isNotEmpty()) {
|
||||
setPostUp(globalConfig.`interface`.postUp)
|
||||
}
|
||||
if (globalConfig.`interface`.preDown.isNotEmpty()) {
|
||||
setPreDown(globalConfig.`interface`.preDown)
|
||||
}
|
||||
if (globalConfig.`interface`.postDown.isNotEmpty()) {
|
||||
setPostDown(globalConfig.`interface`.postDown)
|
||||
}
|
||||
|
||||
globalConfig.`interface`.junkPacketCount.ifPresent { setJunkPacketCount(it) }
|
||||
globalConfig.`interface`.junkPacketMinSize.ifPresent { setJunkPacketMinSize(it) }
|
||||
globalConfig.`interface`.junkPacketMaxSize.ifPresent { setJunkPacketMaxSize(it) }
|
||||
globalConfig.`interface`.initPacketJunkSize.ifPresent { setInitPacketJunkSize(it) }
|
||||
globalConfig.`interface`.responsePacketJunkSize.ifPresent {
|
||||
setResponsePacketJunkSize(it)
|
||||
}
|
||||
globalConfig.`interface`.initPacketMagicHeader.ifPresent {
|
||||
setInitPacketMagicHeader(it)
|
||||
}
|
||||
globalConfig.`interface`.responsePacketMagicHeader.ifPresent {
|
||||
setResponsePacketMagicHeader(it)
|
||||
}
|
||||
globalConfig.`interface`.underloadPacketMagicHeader.ifPresent {
|
||||
setUnderloadPacketMagicHeader(it)
|
||||
}
|
||||
globalConfig.`interface`.transportPacketMagicHeader.ifPresent {
|
||||
setTransportPacketMagicHeader(it)
|
||||
}
|
||||
globalConfig.`interface`.i1.ifPresent { setI1(it) }
|
||||
globalConfig.`interface`.i2.ifPresent { setI2(it) }
|
||||
globalConfig.`interface`.i3.ifPresent { setI3(it) }
|
||||
globalConfig.`interface`.i4.ifPresent { setI4(it) }
|
||||
globalConfig.`interface`.i5.ifPresent { setI5(it) }
|
||||
globalConfig.`interface`.j1.ifPresent { setJ1(it) }
|
||||
globalConfig.`interface`.j2.ifPresent { setJ2(it) }
|
||||
globalConfig.`interface`.j3.ifPresent { setJ3(it) }
|
||||
globalConfig.`interface`.itime.ifPresent { setItime(it) }
|
||||
}
|
||||
val newInterface = newInterfaceBuilder.build()
|
||||
|
||||
val newConfigBuilder =
|
||||
org.amnezia.awg.config.Config.Builder().apply {
|
||||
setInterface(newInterface)
|
||||
addPeers(existingConfig.peers)
|
||||
}
|
||||
val newAmConfig = newConfigBuilder.build()
|
||||
|
||||
return copy(
|
||||
wgQuick = newAmConfig.toWgQuickString(true),
|
||||
amQuick = newAmConfig.toAwgQuickString(true, false),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun configFromWgQuick(wgQuick: String): Config {
|
||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||
@@ -96,6 +223,37 @@ data class TunnelConf(
|
||||
)
|
||||
}
|
||||
|
||||
fun generateDefaultGlobalConfig(): TunnelConf {
|
||||
val keyPair = KeyPair()
|
||||
val config =
|
||||
org.amnezia.awg.config.Config.Builder()
|
||||
.apply {
|
||||
setInterface(
|
||||
Interface.Builder()
|
||||
.apply {
|
||||
setKeyPair(keyPair)
|
||||
parseAddresses("10.0.0.2/32")
|
||||
}
|
||||
.build()
|
||||
)
|
||||
addPeer(
|
||||
Peer.Builder()
|
||||
.apply {
|
||||
setPublicKey(keyPair.publicKey)
|
||||
addAllowedIps(listOf(InetNetwork.parse("0.0.0.0/0")))
|
||||
setEndpoint(InetEndpoint.parse("server.example.com:51820"))
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
||||
.build()
|
||||
return TunnelConf(
|
||||
tunName = GLOBAL_CONFIG_NAME,
|
||||
amQuick = config.toAwgQuickString(false, false),
|
||||
wgQuick = config.toWgQuickString(false),
|
||||
)
|
||||
}
|
||||
|
||||
private const val IPV6_ALL_NETWORKS = "::/0"
|
||||
private const val IPV4_ALL_NETWORKS = "0.0.0.0/0"
|
||||
val ALL_IPS = listOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS)
|
||||
|
||||
+4
@@ -7,6 +7,10 @@ import kotlinx.coroutines.flow.Flow
|
||||
interface TunnelRepository {
|
||||
val flow: Flow<List<TunnelConf>>
|
||||
|
||||
val userTunnelsFlow: Flow<List<TunnelConf>>
|
||||
|
||||
val globalTunnelFlow: Flow<TunnelConf?>
|
||||
|
||||
suspend fun getAll(): Tunnels
|
||||
|
||||
suspend fun save(tunnelConf: TunnelConf)
|
||||
|
||||
@@ -37,6 +37,12 @@ sealed class Route {
|
||||
|
||||
@Keep @Serializable data class SplitTunnel(val id: Int) : Route()
|
||||
|
||||
@Keep @Serializable data class ConfigGlobal(val id: Int?) : Route()
|
||||
|
||||
@Keep @Serializable data class TunnelGlobals(val id: Int) : Route()
|
||||
|
||||
@Keep @Serializable data class SplitTunnelGlobal(val id: Int) : Route()
|
||||
|
||||
@Keep @Serializable data class TunnelAutoTunnel(val id: Int) : Route()
|
||||
|
||||
@Keep @Serializable data object Sort : Route()
|
||||
|
||||
+3
-8
@@ -1,6 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.navigation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -8,7 +7,6 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.NavbarState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.LockedDownBannerHeight
|
||||
|
||||
@@ -18,11 +16,8 @@ fun DynamicTopAppBar(navBarState: NavbarState, modifier: Modifier = Modifier) {
|
||||
TopAppBar(
|
||||
modifier = modifier.padding(top = LockedDownBannerHeight),
|
||||
colors = TopAppBarDefaults.topAppBarColors().copy(Color.Transparent),
|
||||
title = {
|
||||
Box(modifier = Modifier.padding(start = 10.dp)) { navBarState.topTitle?.invoke() }
|
||||
},
|
||||
actions = {
|
||||
Box(modifier = Modifier.padding(end = 10.dp)) { navBarState.topTrailing?.invoke() }
|
||||
},
|
||||
navigationIcon = { navBarState.topLeading?.invoke() },
|
||||
title = { navBarState.topTitle?.invoke() },
|
||||
actions = { navBarState.topTrailing?.invoke() },
|
||||
)
|
||||
}
|
||||
|
||||
+158
-18
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.navigation.components
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.rounded.Sort
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.Text
|
||||
@@ -17,6 +18,7 @@ import androidx.navigation.toRoute
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ActionIconButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route.Config
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.NavbarState
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
@@ -47,6 +49,10 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
|
||||
Route.Config::class.simpleName -> backStackEntry?.toRoute<Route.Config>()
|
||||
Route.SplitTunnel::class.simpleName ->
|
||||
backStackEntry?.toRoute<Route.SplitTunnel>()
|
||||
Route.ConfigGlobal::class.simpleName ->
|
||||
backStackEntry?.toRoute<Route.ConfigGlobal>()
|
||||
Route.SplitTunnelGlobal::class.simpleName ->
|
||||
backStackEntry?.toRoute<Route.SplitTunnelGlobal>()
|
||||
Route.TunnelAutoTunnel::class.simpleName ->
|
||||
backStackEntry?.toRoute<Route.TunnelAutoTunnel>()
|
||||
Route.Sort::class.simpleName -> backStackEntry?.toRoute<Route.Sort>()
|
||||
@@ -68,6 +74,8 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
|
||||
backStackEntry?.toRoute<Route.LocationDisclosure>()
|
||||
Route.Donate::class.simpleName -> backStackEntry?.toRoute<Route.Donate>()
|
||||
Route.Addresses::class.simpleName -> backStackEntry?.toRoute<Route.Addresses>()
|
||||
Route.TunnelGlobals::class.simpleName ->
|
||||
backStackEntry?.toRoute<Route.TunnelGlobals>()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -83,11 +91,21 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
|
||||
when (route) {
|
||||
Route.AdvancedAutoTunnel ->
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = { Text(stringResource(R.string.advanced_settings)) },
|
||||
)
|
||||
Route.Appearance ->
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = { Text(stringResource(R.string.appearance)) },
|
||||
)
|
||||
@@ -100,39 +118,43 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
|
||||
{ Text(stringResource(R.string.auto_tunnel)) }
|
||||
},
|
||||
)
|
||||
is Route.Config -> {
|
||||
val tunnel = sharedState.tunnels.find { it.id == route.id }
|
||||
NavbarState(
|
||||
showBottomItems = true,
|
||||
topTitle = {
|
||||
val title = tunnel?.tunName ?: stringResource(R.string.new_tunnel)
|
||||
Text(title)
|
||||
},
|
||||
topTrailing = {
|
||||
ActionIconButton(Icons.Rounded.Save, R.string.save) {
|
||||
keyboardController?.hide()
|
||||
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Route.Display ->
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = { Text(stringResource(R.string.display_theme)) },
|
||||
)
|
||||
Route.Dns ->
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = { Text(stringResource(R.string.dns_settings)) },
|
||||
)
|
||||
Route.Language ->
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = { Text(stringResource(R.string.language)) },
|
||||
)
|
||||
Route.License ->
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = { Text(stringResource(R.string.licenses)) },
|
||||
)
|
||||
@@ -151,6 +173,11 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
|
||||
)
|
||||
Route.ProxySettings ->
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = { Text(stringResource(R.string.proxy_settings)) },
|
||||
topTrailing = {
|
||||
@@ -175,6 +202,11 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
|
||||
)
|
||||
Route.Sort ->
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = { Text(stringResource(R.string.sort)) },
|
||||
topTrailing = {
|
||||
@@ -188,9 +220,35 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
|
||||
}
|
||||
},
|
||||
)
|
||||
is Config -> {
|
||||
val tunnel = sharedState.tunnels.find { it.id == route.id }
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = {
|
||||
val title = tunnel?.tunName ?: stringResource(R.string.new_tunnel)
|
||||
Text(title)
|
||||
},
|
||||
topTrailing = {
|
||||
ActionIconButton(Icons.Rounded.Save, R.string.save) {
|
||||
keyboardController?.hide()
|
||||
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
is Route.SplitTunnel -> {
|
||||
val tunnel = sharedState.tunnels.find { it.id == route.id }
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
topTitle = { Text(tunnel?.tunName ?: "") },
|
||||
topTrailing = {
|
||||
ActionIconButton(Icons.Rounded.Save, R.string.save) {
|
||||
@@ -200,6 +258,39 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
|
||||
showBottomItems = true,
|
||||
)
|
||||
}
|
||||
is Route.SplitTunnelGlobal -> {
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
topTitle = { Text(stringResource(R.string.splt_tunneling)) },
|
||||
topTrailing = {
|
||||
ActionIconButton(Icons.Rounded.Save, R.string.save) {
|
||||
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
)
|
||||
}
|
||||
is Route.ConfigGlobal -> {
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = { Text(stringResource(R.string.configuration)) },
|
||||
topTrailing = {
|
||||
ActionIconButton(Icons.Rounded.Save, R.string.save) {
|
||||
keyboardController?.hide()
|
||||
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Route.Support ->
|
||||
NavbarState(
|
||||
topTitle = { Text(stringResource(R.string.support)) },
|
||||
@@ -207,21 +298,44 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
|
||||
)
|
||||
Route.SystemFeatures ->
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
topTitle = { Text(stringResource(R.string.android_integrations)) },
|
||||
showBottomItems = true,
|
||||
)
|
||||
is Route.TunnelAutoTunnel -> {
|
||||
val tunnel = sharedState.tunnels.find { it.id == route.id }
|
||||
NavbarState(showBottomItems = true, topTitle = { Text(tunnel?.tunName ?: "") })
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = { Text(tunnel?.tunName ?: "") },
|
||||
)
|
||||
}
|
||||
Route.TunnelMonitoring ->
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
topTitle = { Text(stringResource(R.string.tunnel_monitoring)) },
|
||||
showBottomItems = true,
|
||||
)
|
||||
is Route.TunnelOptions -> {
|
||||
val tunnel = sharedState.tunnels.find { it.id == route.id }
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
showBottomItems = true,
|
||||
topTitle = { Text(tunnel?.tunName ?: "") },
|
||||
topTrailing = {
|
||||
@@ -230,7 +344,7 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
|
||||
sharedViewModel.postSideEffect(LocalSideEffect.Modal.QR)
|
||||
}
|
||||
ActionIconButton(Icons.Rounded.Edit, R.string.edit_tunnel) {
|
||||
navigate(Route.Config(route.id))
|
||||
navigate(Config(route.id))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -285,21 +399,47 @@ fun NavHostController.currentBackStackEntryAsNavbarState(
|
||||
}
|
||||
Route.WifiDetectionMethod ->
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
topTitle = { Text(stringResource(R.string.wifi_detection_method)) },
|
||||
showBottomItems = true,
|
||||
)
|
||||
Route.Donate -> {
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
topTitle = { Text(stringResource(R.string.donate_title)) },
|
||||
showBottomItems = true,
|
||||
)
|
||||
}
|
||||
Route.Addresses -> {
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
topTitle = { Text(stringResource(R.string.addresses)) },
|
||||
showBottomItems = true,
|
||||
)
|
||||
}
|
||||
is Route.TunnelGlobals -> {
|
||||
NavbarState(
|
||||
topLeading = {
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.ArrowBack, R.string.back) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
topTitle = { Text(stringResource(R.string.tunnel_global_overrides)) },
|
||||
showBottomItems = true,
|
||||
)
|
||||
}
|
||||
Route.TunnelsGraph,
|
||||
Route.SettingsGraph,
|
||||
Route.AutoTunnelGraph,
|
||||
|
||||
+10
-1
@@ -84,7 +84,6 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
|
||||
Modifier.verticalScroll(rememberScrollState())
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 24.dp)
|
||||
.padding(horizontal = 12.dp)
|
||||
.then(
|
||||
if (!isTv) {
|
||||
Modifier.clickable(
|
||||
@@ -134,6 +133,16 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
|
||||
)
|
||||
}
|
||||
)
|
||||
add(
|
||||
tunnelGlobalsSettingItem(
|
||||
settingsState.settings.isTunnelGlobalsEnabled,
|
||||
onClick = viewModel::setTunnelGlobals,
|
||||
) {
|
||||
settingsState.globalTunnelConf?.let {
|
||||
navController.navigate(Route.TunnelGlobals(it.id))
|
||||
}
|
||||
}
|
||||
)
|
||||
if (showProxySettings)
|
||||
add(proxYSettingsItem { navController.navigate(Route.ProxySettings) })
|
||||
}
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import android.R.attr.enabled
|
||||
import android.R.attr.onClick
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
|
||||
|
||||
@Composable
|
||||
fun tunnelGlobalsSettingItem(
|
||||
enabled: Boolean,
|
||||
onClick: (Boolean) -> Unit,
|
||||
onItemClick: () -> Unit,
|
||||
): SelectionItem {
|
||||
return SelectionItem(
|
||||
leading = { Icon(ImageVector.vectorResource(R.drawable.globe), contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.tunnel_global_overrides),
|
||||
SelectionLabelType.TITLE,
|
||||
)
|
||||
},
|
||||
trailing = { ScaledSwitch(checked = enabled, onClick = onClick) },
|
||||
onClick = onItemClick,
|
||||
)
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.components.globalConfigItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components.splitTunnelingItem
|
||||
|
||||
@Composable
|
||||
fun TunnelGlobalsScreen(globalTunnelId: Int) {
|
||||
val navController = LocalNavController.current
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
|
||||
modifier =
|
||||
Modifier.verticalScroll(rememberScrollState()).fillMaxSize().padding(vertical = 24.dp),
|
||||
) {
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
globalConfigItem { navController.navigate(Route.ConfigGlobal(globalTunnelId)) },
|
||||
splitTunnelingItem(stringResource(R.string.splt_tunneling)) {
|
||||
navController.navigate(Route.SplitTunnelGlobal(id = globalTunnelId))
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
|
||||
|
||||
@Composable
|
||||
fun globalConfigItem(onClick: () -> Unit): SelectionItem {
|
||||
return SelectionItem(
|
||||
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(stringResource(R.string.configuration), SelectionLabelType.TITLE)
|
||||
},
|
||||
trailing = { ForwardButton { onClick() } },
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
-8
@@ -16,7 +16,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components.MobileDataTunnelItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components.PingRestartItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components.WifiTunnelItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components.ethernetTunnelItem
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelsViewModel
|
||||
@@ -42,13 +41,6 @@ fun TunnelAutoTunnelScreen(tunnelId: Int, viewModel: TunnelsViewModel) {
|
||||
SurfaceSelectionGroupButton(
|
||||
items =
|
||||
buildList {
|
||||
if (tunnelsState.isPingEnabled) {
|
||||
add(
|
||||
PingRestartItem(tunnelConf.restartOnPingFailure) {
|
||||
viewModel.setRestartOnPing(tunnelId, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
add(
|
||||
MobileDataTunnelItem(tunnelConf.isMobileDataTunnel) {
|
||||
viewModel.setMobileDataTunnel(tunnelId, it)
|
||||
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.autotunnel.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.NetworkPing
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||
|
||||
@Composable
|
||||
fun PingRestartItem(enabled: Boolean, onClick: (Boolean) -> Unit): SelectionItem {
|
||||
return SelectionItem(
|
||||
leading = { Icon(Icons.Outlined.NetworkPing, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.restart_on_ping),
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = { ScaledSwitch(checked = enabled, onClick = onClick) },
|
||||
onClick = { onClick(!enabled) },
|
||||
)
|
||||
}
|
||||
+40
-34
@@ -7,11 +7,12 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.SecureScreenFromRecording
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.AddPeerButton
|
||||
@@ -20,12 +21,11 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components.
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelsViewModel
|
||||
import org.orbitmvi.orbit.compose.collectSideEffect
|
||||
|
||||
@Composable
|
||||
fun ConfigScreen(tunnelId: Int? = null, viewModel: TunnelsViewModel = hiltViewModel()) {
|
||||
val sharedViewModel = LocalSharedVm.current
|
||||
fun ConfigScreen(tunnelId: Int? = null) {
|
||||
val viewModel = LocalSharedVm.current
|
||||
|
||||
val tunnelsState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -39,6 +39,7 @@ fun ConfigScreen(tunnelId: Int? = null, viewModel: TunnelsViewModel = hiltViewMo
|
||||
}
|
||||
|
||||
var tunnelName by remember { mutableStateOf(tunnelConf?.tunName ?: "") }
|
||||
val isGlobalConfig = rememberSaveable { tunnelName == TunnelConfig.GLOBAL_CONFIG_NAME }
|
||||
|
||||
val isTunnelNameTaken by
|
||||
remember(tunnelName, tunnelsState.tunnels) {
|
||||
@@ -47,7 +48,7 @@ fun ConfigScreen(tunnelId: Int? = null, viewModel: TunnelsViewModel = hiltViewMo
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.collectSideEffect { sideEffect ->
|
||||
viewModel.collectSideEffect { sideEffect ->
|
||||
if (sideEffect is LocalSideEffect.SaveChanges)
|
||||
viewModel.saveConfigProxy(tunnelId, configProxy, tunnelName)
|
||||
}
|
||||
@@ -64,6 +65,7 @@ fun ConfigScreen(tunnelId: Int? = null, viewModel: TunnelsViewModel = hiltViewMo
|
||||
.padding(horizontal = 12.dp),
|
||||
) {
|
||||
InterfaceSection(
|
||||
isGlobalConfig,
|
||||
configProxy = configProxy,
|
||||
tunnelName,
|
||||
isTunnelNameTaken,
|
||||
@@ -79,34 +81,38 @@ fun ConfigScreen(tunnelId: Int? = null, viewModel: TunnelsViewModel = hiltViewMo
|
||||
configProxy = configProxy.copy(`interface` = configProxy.`interface`.setSipMimic())
|
||||
},
|
||||
)
|
||||
PeersSection(
|
||||
configProxy,
|
||||
onRemove = {
|
||||
configProxy =
|
||||
configProxy.copy(
|
||||
peers = configProxy.peers.toMutableList().apply { removeAt(it) }
|
||||
)
|
||||
},
|
||||
onToggleLan = { index ->
|
||||
configProxy =
|
||||
configProxy.copy(
|
||||
peers =
|
||||
configProxy.peers.toMutableList().apply {
|
||||
val peer = get(index)
|
||||
val updated =
|
||||
if (peer.isLanExcluded()) peer.includeLan()
|
||||
else peer.excludeLan()
|
||||
set(index, updated)
|
||||
}
|
||||
)
|
||||
},
|
||||
onUpdatePeer = { peer, index ->
|
||||
configProxy =
|
||||
configProxy.copy(
|
||||
peers = configProxy.peers.toMutableList().apply { set(index, peer) }
|
||||
)
|
||||
},
|
||||
)
|
||||
AddPeerButton { configProxy = configProxy.copy(peers = configProxy.peers + PeerProxy()) }
|
||||
if (!isGlobalConfig)
|
||||
PeersSection(
|
||||
configProxy,
|
||||
onRemove = {
|
||||
configProxy =
|
||||
configProxy.copy(
|
||||
peers = configProxy.peers.toMutableList().apply { removeAt(it) }
|
||||
)
|
||||
},
|
||||
onToggleLan = { index ->
|
||||
configProxy =
|
||||
configProxy.copy(
|
||||
peers =
|
||||
configProxy.peers.toMutableList().apply {
|
||||
val peer = get(index)
|
||||
val updated =
|
||||
if (peer.isLanExcluded()) peer.includeLan()
|
||||
else peer.excludeLan()
|
||||
set(index, updated)
|
||||
}
|
||||
)
|
||||
},
|
||||
onUpdatePeer = { peer, index ->
|
||||
configProxy =
|
||||
configProxy.copy(
|
||||
peers = configProxy.peers.toMutableList().apply { set(index, peer) }
|
||||
)
|
||||
},
|
||||
)
|
||||
if (!isGlobalConfig)
|
||||
AddPeerButton {
|
||||
configProxy = configProxy.copy(peers = configProxy.peers + PeerProxy())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+91
-84
@@ -1,5 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.components
|
||||
|
||||
import android.R.attr.label
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
@@ -28,6 +29,7 @@ import java.util.*
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun InterfaceFields(
|
||||
isGlobalConfig: Boolean,
|
||||
interfaceState: InterfaceProxy,
|
||||
showScripts: Boolean,
|
||||
showAmneziaValues: Boolean,
|
||||
@@ -41,93 +43,98 @@ fun InterfaceFields(
|
||||
var showPrivateKey by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
ConfigurationTextBox(
|
||||
value = interfaceState.privateKey,
|
||||
hint =
|
||||
stringResource(R.string.hint_template, stringResource(R.string.base64_key))
|
||||
.lowercase(Locale.getDefault()),
|
||||
onValueChange = { onInterfaceChange(interfaceState.copy(privateKey = it)) },
|
||||
label = stringResource(R.string.private_key),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
visualTransformation =
|
||||
if (showPrivateKey) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
trailing = {
|
||||
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 4.dp) {
|
||||
Row(modifier = Modifier.padding(end = 4.dp)) {
|
||||
IconButton(onClick = { showPrivateKey = !showPrivateKey }) {
|
||||
Icon(
|
||||
Icons.Outlined.RemoveRedEye,
|
||||
stringResource(R.string.show_password),
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
enabled = true,
|
||||
onClick = {
|
||||
val keypair = KeyPair()
|
||||
onInterfaceChange(
|
||||
interfaceState.copy(
|
||||
privateKey = keypair.privateKey.toBase64(),
|
||||
publicKey = keypair.publicKey.toBase64(),
|
||||
)
|
||||
if (!isGlobalConfig)
|
||||
ConfigurationTextBox(
|
||||
value = interfaceState.privateKey,
|
||||
hint =
|
||||
stringResource(R.string.hint_template, stringResource(R.string.base64_key))
|
||||
.lowercase(Locale.getDefault()),
|
||||
onValueChange = { onInterfaceChange(interfaceState.copy(privateKey = it)) },
|
||||
label = stringResource(R.string.private_key),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
visualTransformation =
|
||||
if (showPrivateKey) VisualTransformation.None
|
||||
else PasswordVisualTransformation(),
|
||||
trailing = {
|
||||
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 4.dp) {
|
||||
Row(modifier = Modifier.padding(end = 4.dp)) {
|
||||
IconButton(onClick = { showPrivateKey = !showPrivateKey }) {
|
||||
Icon(
|
||||
Icons.Outlined.RemoveRedEye,
|
||||
stringResource(R.string.show_password),
|
||||
)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.Refresh,
|
||||
stringResource(R.string.rotate_keys),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
enabled = true,
|
||||
onClick = {
|
||||
val keypair = KeyPair()
|
||||
onInterfaceChange(
|
||||
interfaceState.copy(
|
||||
privateKey = keypair.privateKey.toBase64(),
|
||||
publicKey = keypair.publicKey.toBase64(),
|
||||
)
|
||||
)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.Refresh,
|
||||
stringResource(R.string.rotate_keys),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = true,
|
||||
singleLine = true,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = interfaceState.publicKey,
|
||||
hint =
|
||||
stringResource(R.string.hint_template, stringResource(R.string.base64_key))
|
||||
.lowercase(Locale.getDefault()),
|
||||
onValueChange = { onInterfaceChange(interfaceState.copy(publicKey = it)) },
|
||||
label = stringResource(R.string.public_key),
|
||||
enabled = false,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
trailing = {
|
||||
IconButton(onClick = { clipboardManager.copy(interfaceState.publicKey) }) {
|
||||
Icon(
|
||||
Icons.Rounded.ContentCopy,
|
||||
stringResource(R.string.copy_public_key),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = interfaceState.addresses,
|
||||
onValueChange = { onInterfaceChange(interfaceState.copy(addresses = it)) },
|
||||
label = stringResource(R.string.addresses),
|
||||
hint =
|
||||
stringResource(
|
||||
R.string.hint_template,
|
||||
stringResource(R.string.comma_separated).lowercase(locale),
|
||||
)
|
||||
.lowercase(Locale.getDefault()),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = interfaceState.listenPort,
|
||||
onValueChange = { onInterfaceChange(interfaceState.copy(listenPort = it)) },
|
||||
label = stringResource(R.string.listen_port),
|
||||
hint = stringResource(R.string.random),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
)
|
||||
},
|
||||
enabled = true,
|
||||
singleLine = true,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
if (!isGlobalConfig)
|
||||
ConfigurationTextBox(
|
||||
value = interfaceState.publicKey,
|
||||
hint =
|
||||
stringResource(R.string.hint_template, stringResource(R.string.base64_key))
|
||||
.lowercase(Locale.getDefault()),
|
||||
onValueChange = { onInterfaceChange(interfaceState.copy(publicKey = it)) },
|
||||
label = stringResource(R.string.public_key),
|
||||
enabled = false,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
trailing = {
|
||||
IconButton(onClick = { clipboardManager.copy(interfaceState.publicKey) }) {
|
||||
Icon(
|
||||
Icons.Rounded.ContentCopy,
|
||||
stringResource(R.string.copy_public_key),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
if (!isGlobalConfig)
|
||||
ConfigurationTextBox(
|
||||
value = interfaceState.addresses,
|
||||
onValueChange = { onInterfaceChange(interfaceState.copy(addresses = it)) },
|
||||
label = stringResource(R.string.addresses),
|
||||
hint =
|
||||
stringResource(
|
||||
R.string.hint_template,
|
||||
stringResource(R.string.comma_separated).lowercase(locale),
|
||||
)
|
||||
.lowercase(Locale.getDefault()),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
if (!isGlobalConfig)
|
||||
ConfigurationTextBox(
|
||||
value = interfaceState.listenPort,
|
||||
onValueChange = { onInterfaceChange(interfaceState.copy(listenPort = it)) },
|
||||
label = stringResource(R.string.listen_port),
|
||||
hint = stringResource(R.string.random),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
|
||||
+14
-10
@@ -20,6 +20,7 @@ import java.util.*
|
||||
|
||||
@Composable
|
||||
fun InterfaceSection(
|
||||
isGlobalConfig: Boolean,
|
||||
configProxy: ConfigProxy,
|
||||
tunnelName: String,
|
||||
isTunnelNameTaken: Boolean,
|
||||
@@ -32,6 +33,7 @@ fun InterfaceSection(
|
||||
var showAmneziaValues by rememberSaveable {
|
||||
mutableStateOf(configProxy.`interface`.isAmneziaEnabled())
|
||||
}
|
||||
|
||||
var showScripts by rememberSaveable { mutableStateOf(configProxy.hasScripts()) }
|
||||
var isDropDownExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
val isAmneziaCompatibilitySet =
|
||||
@@ -82,17 +84,19 @@ fun InterfaceSection(
|
||||
},
|
||||
)
|
||||
}
|
||||
ConfigurationTextBox(
|
||||
value = tunnelName,
|
||||
onValueChange = onTunnelNameChange,
|
||||
label = stringResource(R.string.name),
|
||||
isError = isTunnelNameTaken,
|
||||
hint =
|
||||
stringResource(R.string.hint_template, stringResource(R.string.tunnel_name))
|
||||
.lowercase(Locale.getDefault()),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
if (!isGlobalConfig)
|
||||
ConfigurationTextBox(
|
||||
value = tunnelName,
|
||||
onValueChange = onTunnelNameChange,
|
||||
label = stringResource(R.string.name),
|
||||
isError = isTunnelNameTaken,
|
||||
hint =
|
||||
stringResource(R.string.hint_template, stringResource(R.string.tunnel_name))
|
||||
.lowercase(Locale.getDefault()),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
InterfaceFields(
|
||||
isGlobalConfig,
|
||||
interfaceState = configProxy.`interface`,
|
||||
showScripts = showScripts,
|
||||
showAmneziaValues = showAmneziaValues,
|
||||
|
||||
+8
-1
@@ -10,13 +10,16 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components.*
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelsViewModel
|
||||
@@ -61,7 +64,11 @@ fun TunnelOptionsScreen(tunnelId: Int, viewModel: TunnelsViewModel) {
|
||||
buildList {
|
||||
add(primaryTunnelItem(tunnelConf) { viewModel.togglePrimaryTunnel(tunnelId) })
|
||||
add(autoTunnelingItem(tunnelConf, navController))
|
||||
add(splitTunnelingItem(tunnelConf, navController))
|
||||
add(
|
||||
splitTunnelingItem(stringResource(R.string.splt_tunneling)) {
|
||||
navController.navigate(Route.SplitTunnel(id = tunnelConf.id))
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
SectionDivider()
|
||||
|
||||
+7
-19
@@ -1,33 +1,21 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components
|
||||
|
||||
import android.R.attr.onClick
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.CallSplit
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavController
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
|
||||
|
||||
@Composable
|
||||
fun splitTunnelingItem(tunnelConf: TunnelConf, navController: NavController): SelectionItem {
|
||||
fun splitTunnelingItem(label: String, onClick: () -> Unit): SelectionItem {
|
||||
return SelectionItem(
|
||||
leading = { Icon(Icons.AutoMirrored.Outlined.CallSplit, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.splt_tunneling),
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton { navController.navigate(Route.SplitTunnel(id = tunnelConf.id)) }
|
||||
},
|
||||
onClick = { navController.navigate(Route.SplitTunnel(id = tunnelConf.id)) },
|
||||
title = { SelectionItemLabel(label, SelectionLabelType.TITLE) },
|
||||
trailing = { ForwardButton { onClick() } },
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
|
||||
data class NavbarState(
|
||||
val topTitle: (@Composable () -> Unit)? = null,
|
||||
val topTrailing: (@Composable () -> Unit)? = null,
|
||||
val topLeading: (@Composable () -> Unit)? = null,
|
||||
val showBottomItems: Boolean = false,
|
||||
val removeBottom: Boolean = false,
|
||||
val isAutoTunnelActive: Boolean = false,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.state
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
|
||||
data class SettingUiState(
|
||||
val settings: GeneralSettings = GeneralSettings(),
|
||||
@@ -10,4 +11,5 @@ data class SettingUiState(
|
||||
val isPinLockEnabled: Boolean = false,
|
||||
val showDetailedPingStats: Boolean = false,
|
||||
val stateInitialized: Boolean = false,
|
||||
val globalTunnelConf: TunnelConf? = null,
|
||||
)
|
||||
|
||||
+15
-1
@@ -4,9 +4,11 @@ import androidx.lifecycle.ViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.DnsProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
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.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.SettingUiState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -23,6 +25,7 @@ constructor(
|
||||
private val settingsRepository: GeneralSettingRepository,
|
||||
private val shortcutManager: ShortcutManager,
|
||||
private val appStateRepository: AppStateRepository,
|
||||
private val tunnelsRepository: TunnelRepository,
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
) : ContainerHost<SettingUiState, Nothing>, ViewModel() {
|
||||
|
||||
@@ -32,7 +35,11 @@ constructor(
|
||||
buildSettings = { repeatOnSubscribedStopTimeout = 5000L },
|
||||
) {
|
||||
intent {
|
||||
combine(settingsRepository.flow, appStateRepository.flow) { settings, appState ->
|
||||
combine(
|
||||
settingsRepository.flow,
|
||||
appStateRepository.flow,
|
||||
tunnelsRepository.globalTunnelFlow,
|
||||
) { settings, appState, tunnel ->
|
||||
SettingUiState(
|
||||
settings = settings,
|
||||
isLocalLoggingEnabled = appState.isLocalLogsEnabled,
|
||||
@@ -41,6 +48,7 @@ constructor(
|
||||
isPinLockEnabled = appState.isPinLockEnabled,
|
||||
showDetailedPingStats = appState.showDetailedPingStats,
|
||||
stateInitialized = true,
|
||||
globalTunnelConf = tunnel,
|
||||
)
|
||||
}
|
||||
.collect { reduce { it } }
|
||||
@@ -72,6 +80,12 @@ constructor(
|
||||
settingsRepository.save(state.settings.copy(isLanOnKillSwitchEnabled = to))
|
||||
}
|
||||
|
||||
fun setTunnelGlobals(to: Boolean) = intent {
|
||||
settingsRepository.save(state.settings.copy(isTunnelGlobalsEnabled = to))
|
||||
if (state.globalTunnelConf == null)
|
||||
tunnelsRepository.save(TunnelConf.generateDefaultGlobalConfig())
|
||||
}
|
||||
|
||||
fun setTunnelPingIntervalSeconds(to: Int) = intent {
|
||||
settingsRepository.save(state.settings.copy(tunnelPingIntervalSeconds = to))
|
||||
}
|
||||
|
||||
@@ -20,11 +20,13 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectReposit
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.SharedAppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.io.File
|
||||
@@ -36,9 +38,11 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.awg.config.BadConfigException
|
||||
import org.orbitmvi.orbit.ContainerHost
|
||||
import org.orbitmvi.orbit.viewmodel.container
|
||||
import rikka.shizuku.Shizuku
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
@HiltViewModel
|
||||
@@ -123,6 +127,46 @@ constructor(
|
||||
appStateRepository.setPinLockEnabled(enabled)
|
||||
}
|
||||
|
||||
fun saveConfigProxy(tunnelId: Int?, configProxy: ConfigProxy, tunnelName: String) = intent {
|
||||
if (state.tunnels.any { it.tunName == tunnelName && it.id != tunnelId })
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.tunnel_name_taken))
|
||||
)
|
||||
runCatching {
|
||||
val (wg, am) = configProxy.buildConfigs()
|
||||
val tunnelConf =
|
||||
if (tunnelId == null) {
|
||||
TunnelConf.tunnelConfFromQuick(am.toAwgQuickString(true, false), tunnelName)
|
||||
} else {
|
||||
val latestTunnel = state.tunnels.find { it.id == tunnelId }
|
||||
latestTunnel?.copy(
|
||||
tunName = tunnelName,
|
||||
amQuick = am.toAwgQuickString(true, false),
|
||||
wgQuick = wg.toWgQuickString(true),
|
||||
)
|
||||
}
|
||||
if (tunnelConf != null) {
|
||||
tunnelRepository.save(tunnelConf)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.config_changes_saved)
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it)
|
||||
val message =
|
||||
when (it) {
|
||||
is BadConfigException -> it.asStringValue()
|
||||
is com.wireguard.config.BadConfigException -> it.asStringValue()
|
||||
else -> StringValue.StringResource(R.string.unknown_error)
|
||||
}
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
}
|
||||
}
|
||||
|
||||
fun stopTunnel(tunnelConf: TunnelConf) = intent { tunnelManager.stopTunnel(tunnelConf.id) }
|
||||
|
||||
fun setAppMode(appMode: AppMode) = intent {
|
||||
|
||||
+1
-43
@@ -11,7 +11,6 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepos
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.ConfigProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
@@ -27,7 +26,6 @@ import kotlinx.coroutines.flow.combine
|
||||
import org.amnezia.awg.config.BadConfigException
|
||||
import org.orbitmvi.orbit.ContainerHost
|
||||
import org.orbitmvi.orbit.viewmodel.container
|
||||
import timber.log.Timber
|
||||
|
||||
@HiltViewModel
|
||||
class TunnelsViewModel
|
||||
@@ -47,7 +45,7 @@ constructor(
|
||||
buildSettings = { repeatOnSubscribedStopTimeout = 5000L },
|
||||
) {
|
||||
combine(
|
||||
tunnelRepository.flow,
|
||||
tunnelRepository.userTunnelsFlow,
|
||||
generalSettingRepository.flow,
|
||||
appStateRepository.flow,
|
||||
tunnelManager.activeTunnels,
|
||||
@@ -69,46 +67,6 @@ constructor(
|
||||
globalEffectRepository.post(globalSideEffect)
|
||||
}
|
||||
|
||||
fun saveConfigProxy(tunnelId: Int?, configProxy: ConfigProxy, tunnelName: String) = intent {
|
||||
if (state.tunnels.any { it.tunName == tunnelName && it.id != tunnelId })
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.tunnel_name_taken))
|
||||
)
|
||||
runCatching {
|
||||
val (wg, am) = configProxy.buildConfigs()
|
||||
val tunnelConf =
|
||||
if (tunnelId == null) {
|
||||
TunnelConf.tunnelConfFromQuick(am.toAwgQuickString(true, false), tunnelName)
|
||||
} else {
|
||||
val latestTunnel = state.tunnels.find { it.id == tunnelId }
|
||||
latestTunnel?.copy(
|
||||
tunName = tunnelName,
|
||||
amQuick = am.toAwgQuickString(true, false),
|
||||
wgQuick = wg.toWgQuickString(true),
|
||||
)
|
||||
}
|
||||
if (tunnelConf != null) {
|
||||
tunnelRepository.save(tunnelConf)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.config_changes_saved)
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it)
|
||||
val message =
|
||||
when (it) {
|
||||
is BadConfigException -> it.asStringValue()
|
||||
is com.wireguard.config.BadConfigException -> it.asStringValue()
|
||||
else -> StringValue.StringResource(R.string.unknown_error)
|
||||
}
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSortChanges(tunnels: List<TunnelConf>) = intent {
|
||||
tunnelRepository.saveAll(tunnels.mapIndexed { index, conf -> conf.copy(position = index) })
|
||||
postSideEffect(
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="M480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-7 -0.5,-14.5T799,453q-5,29 -27,48t-52,19h-80q-33,0 -56.5,-23.5T560,440v-40L400,400v-80q0,-33 23.5,-56.5T480,240h40q0,-23 12.5,-40.5T563,171q-20,-5 -40.5,-8t-42.5,-3q-134,0 -227,93t-93,227h200q66,0 113,47t47,113v40L400,680v110q20,5 39.5,7.5T480,800Z"
|
||||
android:fillColor="#e3e3e3"/>
|
||||
</vector>
|
||||
@@ -391,4 +391,7 @@
|
||||
the Play Store version of this app. Please browse the project\'s webpages to find where to donate.
|
||||
</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
<string name="tunnel_global_overrides">Global tunnel overrides</string>
|
||||
<string name="back">Back</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
accompanist = "0.37.3"
|
||||
activityCompose = "1.11.0"
|
||||
amneziawgAndroid = "2.1.9"
|
||||
amneziawgAndroid = "2.1.10"
|
||||
androidx-junit = "1.3.0"
|
||||
icmp4a = "1.0.0"
|
||||
orbitCompose = "10.0.0"
|
||||
@@ -45,7 +45,6 @@ storage = "1.6.0"
|
||||
ktfmt = "0.24.0"
|
||||
licensee = "1.13.0"
|
||||
|
||||
|
||||
[libraries]
|
||||
|
||||
# accompanist
|
||||
|
||||
Reference in New Issue
Block a user