mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 196d855579 | |||
| 230cd0adb8 | |||
| 33b51823ab | |||
| f333319576 | |||
| e6ad1531c9 | |||
| 030082df34 | |||
| a825a2f2a4 | |||
| aa1a344bb2 | |||
| 3aa03c1896 | |||
| 21e56cda80 | |||
| b5196fbf01 | |||
| e46fe93ae0 | |||
| 872ff83a12 | |||
| 5563292a87 | |||
| 8ba760a5ff | |||
| d431c2d39f | |||
| 33437ab237 | |||
| 4a432d2bb7 | |||
| 3df972d031 | |||
| 8b828cca55 | |||
| a223289949 | |||
| c8b65fb7fa | |||
| feec7f0ffc | |||
| b63c6a9b73 | |||
| 46975607c4 | |||
| 0c7bcb5453 | |||
| 599bf9c9e0 |
@@ -12,7 +12,6 @@ on:
|
||||
default: debug
|
||||
options:
|
||||
- debug
|
||||
- prerelease
|
||||
- nightly
|
||||
- release
|
||||
flavor:
|
||||
@@ -105,9 +104,6 @@ jobs:
|
||||
"release")
|
||||
./gradlew :app:assemble${flavor^}Release --info
|
||||
;;
|
||||
"prerelease")
|
||||
./gradlew :app:assemble${flavor^}Prerelease --info
|
||||
;;
|
||||
"nightly")
|
||||
./gradlew :app:assemble${flavor^}Nightly --info
|
||||
;;
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Check for new commits
|
||||
id: check
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
|
||||
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
run: mkdir ${{ github.workspace }}/temp
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: android_artifacts_*
|
||||
path: ${{ github.workspace }}/temp
|
||||
|
||||
@@ -25,7 +25,6 @@ on:
|
||||
description: "GitHub release type"
|
||||
options:
|
||||
- none
|
||||
- prerelease
|
||||
- release
|
||||
default: release
|
||||
required: true
|
||||
@@ -60,7 +59,7 @@ jobs:
|
||||
flavor: fdroid
|
||||
|
||||
build-standalone:
|
||||
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' || inputs.release_type == 'prerelease' || inputs.flavor == 'standalone' }}
|
||||
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' || inputs.release_type == 'debug' || inputs.flavor == 'standalone' }}
|
||||
uses: ./.github/workflows/build.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
@@ -109,7 +108,7 @@ jobs:
|
||||
run: mkdir ${{ github.workspace }}/temp
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: android_artifacts_*
|
||||
path: ${{ github.workspace }}/temp
|
||||
@@ -124,11 +123,6 @@ jobs:
|
||||
echo "$RELEASE_NOTES" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: On prerelease release notes
|
||||
if: ${{ github.event_name != 'push' && inputs.release_type == 'prerelease' }}
|
||||
run: |
|
||||
echo "RELEASE_NOTES=Testing version of app for specific feature." >> $GITHUB_ENV
|
||||
|
||||
- name: Get checksum
|
||||
id: checksum
|
||||
run: |
|
||||
@@ -162,8 +156,8 @@ jobs:
|
||||
tag_name: ${{ github.event_name == 'push' && github.ref_name || github.event.inputs.tag_name }}
|
||||
name: ${{ github.event_name == 'push' && github.ref_name || github.event.inputs.tag_name }}
|
||||
draft: false
|
||||
prerelease: ${{ github.event_name != 'push' && inputs.release_type == 'prerelease' }}
|
||||
make_latest: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
|
||||
prerelease: false
|
||||
make_latest: true
|
||||
files: |
|
||||
${{ github.workspace }}/temp/**/*.apk
|
||||
env:
|
||||
@@ -178,7 +172,7 @@ jobs:
|
||||
- name: Dispatch update for fdroid repo
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: wgtunnel/fdroid
|
||||
event-type: fdroid-update
|
||||
|
||||
|
||||
+18
-18
@@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
@@ -20,6 +22,8 @@ android {
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||
|
||||
defaultConfig {
|
||||
applicationId = Constants.APP_ID
|
||||
minSdk = Constants.MIN_SDK
|
||||
@@ -27,15 +31,10 @@ android {
|
||||
versionCode = computeVersionCode()
|
||||
versionName = computeVersionName()
|
||||
|
||||
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||
|
||||
sourceSets { getByName("debug").assets.srcDirs(files("$projectDir/schemas")) }
|
||||
|
||||
buildConfigField(
|
||||
"String[]",
|
||||
"LANGUAGES",
|
||||
"new String[]{ ${languageList().joinToString(separator = ", ") { "\"$it\"" }} }",
|
||||
)
|
||||
val languagesArray = buildLanguagesArray(languageList())
|
||||
buildConfigField("String[]", "LANGUAGES", "new String[]{ $languagesArray }")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables { useSupportLibrary = true }
|
||||
@@ -73,22 +72,15 @@ android {
|
||||
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "WG Tunnel - Debug")
|
||||
resValue("string", "app_name", "WG Tunnel Debug")
|
||||
isDebuggable = true
|
||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"")
|
||||
}
|
||||
|
||||
create(Constants.PRERELEASE) {
|
||||
initWith(buildTypes.getByName(Constants.RELEASE))
|
||||
applicationIdSuffix = ".prerelease"
|
||||
resValue("string", "app_name", "WG Tunnel - Pre")
|
||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.pre\"")
|
||||
}
|
||||
|
||||
create(Constants.NIGHTLY) {
|
||||
initWith(buildTypes.getByName(Constants.RELEASE))
|
||||
applicationIdSuffix = ".nightly"
|
||||
resValue("string", "app_name", "WG Tunnel - Nightly")
|
||||
resValue("string", "app_name", "WG Tunnel Nightly")
|
||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
|
||||
}
|
||||
}
|
||||
@@ -114,7 +106,9 @@ android {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
|
||||
|
||||
kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_17 } }
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
@@ -123,7 +117,7 @@ android {
|
||||
|
||||
licensee {
|
||||
Constants.allowedLicenses.forEach { allow(it) }
|
||||
allowUrl(Constants.XZING_LICENSE_URL)
|
||||
Constants.allowedLicenseUrls.forEach { allowUrl(it) }
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
@@ -219,6 +213,12 @@ dependencies {
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
implementation(libs.slf4j.android)
|
||||
|
||||
// shizuku
|
||||
implementation(libs.shizuku.api)
|
||||
implementation(libs.shizuku.provider)
|
||||
|
||||
implementation(libs.reorderable)
|
||||
}
|
||||
|
||||
tasks.register<Copy>("copyLicenseeJsonToAssets") {
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 18,
|
||||
"identityHash": "505728bad740c12bab998a066b569333",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT false, `split_tunnel_apps` TEXT NOT NULL DEFAULT '', `wifi_detection_method` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAmneziaEnabled",
|
||||
"columnName": "is_amnezia_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWildcardsEnabled",
|
||||
"columnName": "is_wildcards_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isStopOnNoInternetEnabled",
|
||||
"columnName": "is_stop_on_no_internet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isVpnKillSwitchEnabled",
|
||||
"columnName": "is_vpn_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelKillSwitchEnabled",
|
||||
"columnName": "is_kernel_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLanOnKillSwitchEnabled",
|
||||
"columnName": "is_lan_on_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"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": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnUnsecureEnabled",
|
||||
"columnName": "is_tunnel_on_unsecure_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "splitTunnelApps",
|
||||
"columnName": "split_tunnel_apps",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wifiDetectionMethod",
|
||||
"columnName": "wifi_detection_method",
|
||||
"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, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` 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)",
|
||||
"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": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingInterval",
|
||||
"columnName": "ping_interval",
|
||||
"affinity": "INTEGER",
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingCooldown",
|
||||
"columnName": "ping_cooldown",
|
||||
"affinity": "INTEGER",
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingIp",
|
||||
"columnName": "ping_ip",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, '505728bad740c12bab998a066b569333')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#648DB3</color>
|
||||
</resources>
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
<!--foreground service permissions-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<!--start service on boot permission-->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -27,7 +25,6 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -54,6 +51,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.Loca
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.TunnelAutoTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.sort.SortScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
|
||||
@@ -74,7 +72,6 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlin.system.exitProcess
|
||||
import org.amnezia.awg.backend.GoBackend.VpnService
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@@ -87,6 +84,8 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private var lastLocationPermissionState: Boolean? = null
|
||||
|
||||
val REQUEST_CODE = 123
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge(
|
||||
@@ -108,7 +107,6 @@ class MainActivity : AppCompatActivity() {
|
||||
val isTv = isRunningOnTv()
|
||||
val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val appViewState by viewModel.appViewState.collectAsStateWithLifecycle()
|
||||
val tunnelError by viewModel.tunnelManager.errorEvents.collectAsStateWithLifecycle(null)
|
||||
|
||||
val navController = rememberNavController()
|
||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||
@@ -151,15 +149,6 @@ class MainActivity : AppCompatActivity() {
|
||||
viewModel.handleEvent(AppEvent.SetBatteryOptimizeDisableShown)
|
||||
}
|
||||
|
||||
LaunchedEffect(tunnelError) {
|
||||
if (tunnelError == null) return@LaunchedEffect
|
||||
val message = tunnelError!!.second.toStringRes()
|
||||
val context = this@MainActivity
|
||||
snackbar.showSnackbar(
|
||||
context.getString(R.string.tunnel_error_template, context.getString(message))
|
||||
)
|
||||
}
|
||||
|
||||
with(appViewState) {
|
||||
LaunchedEffect(isConfigChanged) {
|
||||
if (isConfigChanged) {
|
||||
@@ -266,7 +255,7 @@ class MainActivity : AppCompatActivity() {
|
||||
SettingsAdvancedScreen(appUiState, viewModel)
|
||||
}
|
||||
composable<Route.LocationDisclosure> {
|
||||
LocationDisclosureScreen(appUiState, viewModel)
|
||||
LocationDisclosureScreen(viewModel)
|
||||
}
|
||||
composable<Route.AutoTunnel> {
|
||||
AutoTunnelScreen(appUiState, viewModel)
|
||||
@@ -320,6 +309,7 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
}
|
||||
composable<Route.Sort> { SortScreen(appUiState, viewModel) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,19 +321,12 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
checkPermissionAndNotify()
|
||||
WireGuardAutoTunnel.setUiActive(true)
|
||||
networkMonitor.checkPermissionsAndUpdateState()
|
||||
}
|
||||
|
||||
private fun checkPermissionAndNotify() {
|
||||
val hasLocation =
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
if (lastLocationPermissionState != hasLocation) {
|
||||
Timber.d("Location permission changed to: $hasLocation")
|
||||
if (hasLocation) {
|
||||
networkMonitor.sendLocationPermissionsGrantedBroadcast()
|
||||
}
|
||||
lastLocationPermissionState = hasLocation
|
||||
}
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
WireGuardAutoTunnel.setUiActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@ import android.app.Application
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.work.Configuration
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
@@ -23,6 +20,10 @@ import dagger.hilt.android.HiltAndroidApp
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
@@ -50,7 +51,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
StrictMode.setThreadPolicy(
|
||||
@@ -90,30 +90,20 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
applicationScope.launch {
|
||||
tunnelManager.setBackendState(BackendState.INACTIVE, emptyList())
|
||||
}
|
||||
applicationScope.cancel()
|
||||
tunnelManager.setBackendState(BackendState.INACTIVE, emptyList())
|
||||
super.onTerminate()
|
||||
}
|
||||
|
||||
class AppLifecycleObserver : DefaultLifecycleObserver {
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
Timber.d("Application entered foreground")
|
||||
foreground = true
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
Timber.d("Application entered background")
|
||||
foreground = false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var foreground = false
|
||||
|
||||
fun isForeground(): Boolean {
|
||||
return foreground
|
||||
private val _uiActive = MutableStateFlow(false)
|
||||
|
||||
val uiActive: StateFlow<Boolean>
|
||||
get() = _uiActive
|
||||
|
||||
fun setUiActive(active: Boolean) {
|
||||
_uiActive.update { active }
|
||||
}
|
||||
|
||||
@Volatile private var lastActiveTunnels: List<Int> = emptyList()
|
||||
|
||||
+5
@@ -43,8 +43,13 @@ interface NotificationManager {
|
||||
fun show(notificationId: Int, notification: Notification)
|
||||
|
||||
companion object {
|
||||
const val AUTO_TUNNEL_LOCATION_PERMISSION_ID = 123
|
||||
const val AUTO_TUNNEL_LOCATION_SERVICES_ID = 124
|
||||
// For auto tunnel foreground notification
|
||||
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
|
||||
// for tunnel foreground notification
|
||||
const val VPN_NOTIFICATION_ID = 100
|
||||
const val TUNNEL_STATUS_NOTIFICATION_ID = 101
|
||||
const val EXTRA_ID = "id"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,8 @@ constructor(
|
||||
service.stop()
|
||||
try {
|
||||
context.unbindService(autoTunnelServiceConnection)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to unbind AutoTunnelService")
|
||||
} finally {
|
||||
_tunnelService.value = null
|
||||
}
|
||||
@@ -120,6 +122,8 @@ constructor(
|
||||
service.stop()
|
||||
try {
|
||||
context.unbindService(tunnelServiceConnection)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to stop TunnelForegroundService")
|
||||
} finally {
|
||||
_tunnelService.value = null
|
||||
}
|
||||
|
||||
+4
-18
@@ -8,7 +8,6 @@ import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
@@ -24,22 +23,10 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -229,9 +216,8 @@ class TunnelForegroundService : LifecycleService() {
|
||||
}
|
||||
|
||||
private suspend fun startNetworkMonitorJob() {
|
||||
networkMonitor.networkStatusFlow.flowOn(ioDispatcher).collectLatest { status ->
|
||||
val isAvailable = status !is NetworkStatus.Disconnected
|
||||
isNetworkConnected.value = isAvailable
|
||||
networkMonitor.connectivityStateFlow.flowOn(ioDispatcher).collectLatest { status ->
|
||||
isNetworkConnected.value = status.hasConnectivity()
|
||||
Timber.d("Network available: $status")
|
||||
}
|
||||
}
|
||||
|
||||
+85
-65
@@ -3,12 +3,11 @@ package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
@@ -26,24 +25,12 @@ import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.zipWithPrevious
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -65,8 +52,6 @@ class AutoTunnelService : LifecycleService() {
|
||||
|
||||
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
private var killSwitchJob: Job? = null
|
||||
|
||||
class LocalBinder(val service: AutoTunnelService) : Binder()
|
||||
@@ -91,19 +76,14 @@ class AutoTunnelService : LifecycleService() {
|
||||
}
|
||||
|
||||
fun start() {
|
||||
kotlin
|
||||
.runCatching {
|
||||
launchWatcherNotification()
|
||||
initWakeLock()
|
||||
startAutoTunnelJob()
|
||||
startAutoTunnelStateJob()
|
||||
killSwitchJob = startKillSwitchJob()
|
||||
}
|
||||
.onFailure { Timber.e(it) }
|
||||
launchWatcherNotification()
|
||||
startAutoTunnelJob()
|
||||
startAutoTunnelStateJob()
|
||||
killSwitchJob = startKillSwitchJob()
|
||||
startNotificationJob()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
wakeLock?.let { if (it.isHeld) it.release() }
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
@@ -151,39 +131,6 @@ class AutoTunnelService : LifecycleService() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun initWakeLock() {
|
||||
wakeLock =
|
||||
(getSystemService(POWER_SERVICE) as PowerManager).run {
|
||||
val tag = this.javaClass.name
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||
try {
|
||||
Timber.i("Initiating wakelock with 10 min timeout")
|
||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
|
||||
return with(autoTunnelStateFlow.value.networkState) {
|
||||
val wifiName =
|
||||
when (networkStatus) {
|
||||
is NetworkStatus.Connected -> {
|
||||
networkStatus.wifiSsid
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
copy(
|
||||
isWifiConnected = networkStatus.wifiConnected,
|
||||
isMobileDataConnected = networkStatus.cellularConnected,
|
||||
isEthernetConnected = networkStatus.ethernetConnected,
|
||||
wifiName = wifiName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun startAutoTunnelStateJob() =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
@@ -197,9 +144,9 @@ class AutoTunnelService : LifecycleService() {
|
||||
old.isKernelEnabled == new.isKernelEnabled
|
||||
} // Only emit when isKernelEnabled changes
|
||||
.flatMapLatest {
|
||||
networkMonitor.networkStatusFlow.flowOn(ioDispatcher).map {
|
||||
buildNetworkState(it)
|
||||
}
|
||||
networkMonitor.connectivityStateFlow
|
||||
.flowOn(ioDispatcher)
|
||||
.map(NetworkState::from)
|
||||
}
|
||||
.distinctUntilChanged(),
|
||||
) { double, networkState ->
|
||||
@@ -236,6 +183,79 @@ class AutoTunnelService : LifecycleService() {
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
// watch for changes to location permission and notify user it will impact auto-tunneling
|
||||
// TODO can add deeplinks later back to the app for fixing
|
||||
// TODO or a recheck button for location permission so we dont have to poll it
|
||||
private fun startNotificationJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
var locationServicesShown = false
|
||||
var locationPermissionsShown = false
|
||||
autoTunnelStateFlow.zipWithPrevious().collect { (previous, current) ->
|
||||
when (current.settings.wifiDetectionMethod) {
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT,
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.LEGACY -> {
|
||||
with(current.networkState) {
|
||||
if (
|
||||
locationPermissionGranted == false &&
|
||||
(previous?.networkState?.locationPermissionGranted == true ||
|
||||
!locationServicesShown)
|
||||
) {
|
||||
locationServicesShown = true
|
||||
Timber.i("Detected location permission lost")
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.warning),
|
||||
description =
|
||||
getString(R.string.location_permissions_missing),
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_PERMISSION_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
if (
|
||||
locationServicesEnabled == false &&
|
||||
(previous?.networkState?.locationServicesEnabled == true ||
|
||||
!locationPermissionsShown)
|
||||
) {
|
||||
locationPermissionsShown = true
|
||||
Timber.i("Detected location services lost")
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.warning),
|
||||
description =
|
||||
getString(R.string.location_services_not_detected),
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_SERVICES_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
if (
|
||||
locationServicesEnabled == true &&
|
||||
previous?.networkState?.locationServicesEnabled == false
|
||||
) {
|
||||
notificationManager.remove(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_SERVICES_ID
|
||||
)
|
||||
}
|
||||
if (
|
||||
locationPermissionGranted == true &&
|
||||
previous?.networkState?.locationPermissionGranted == false
|
||||
) {
|
||||
notificationManager.remove(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_PERMISSION_ID
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startKillSwitchJob() =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
autoTunnelStateFlow.collect {
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
@@ -35,6 +36,8 @@ constructor(
|
||||
}
|
||||
|
||||
override suspend fun startBackend(tunnel: TunnelConf) {
|
||||
// name too long for kernel mode
|
||||
if (!tunnel.isNameKernelCompatible) throw BackendError.TunnelNameTooLong
|
||||
try {
|
||||
updateTunnelStatus(tunnel, TunnelStatus.Starting)
|
||||
backend.setState(tunnel, Tunnel.State.UP, tunnel.toWgConfig())
|
||||
|
||||
+50
-14
@@ -1,5 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Kernel
|
||||
@@ -10,20 +14,13 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class TunnelManager
|
||||
@Inject
|
||||
constructor(
|
||||
@@ -32,6 +29,7 @@ constructor(
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
private val notificationManager: NotificationManager,
|
||||
) : TunnelProvider {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -64,8 +62,46 @@ constructor(
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
override val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>>
|
||||
get() = tunnelProviderFlow.value.errorEvents
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>> =
|
||||
combine(
|
||||
tunnelProviderFlow.flatMapLatest { it.errorEvents },
|
||||
WireGuardAutoTunnel.uiActive,
|
||||
) { errorEvent, isEnabled ->
|
||||
if (isEnabled) errorEvent else null
|
||||
}
|
||||
.filterNotNull()
|
||||
.shareIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
replay = 0,
|
||||
)
|
||||
|
||||
// observe tunnel errors and launch notifications if ui is inactive
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
tunnelProviderFlow
|
||||
.flatMapLatest { it.errorEvents }
|
||||
.collect { (tunnelConf, error) ->
|
||||
if (!WireGuardAutoTunnel.uiActive.value) {
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title = StringValue.DynamicString(tunnelConf.name),
|
||||
description =
|
||||
StringValue.StringResource(
|
||||
R.string.tunnel_error_template,
|
||||
error.toStringRes(),
|
||||
),
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.TUNNEL_STATUS_NOTIFICATION_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason> =
|
||||
tunnelProviderFlow.value.bouncingTunnelIds
|
||||
@@ -106,7 +142,7 @@ constructor(
|
||||
return tunnelProviderFlow.value.getStatistics(tunnelConf)
|
||||
}
|
||||
|
||||
fun restorePreviousState() =
|
||||
fun restorePreviousState(): Job =
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
val settings = appDataRepository.settings.get()
|
||||
if (settings.isRestoreOnBootEnabled) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 17,
|
||||
version = 18,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
@@ -32,6 +32,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
AutoMigration(from = 14, to = 15),
|
||||
AutoMigration(from = 15, to = 16),
|
||||
AutoMigration(from = 16, to = 17, spec = WifiDetectionMigration::class),
|
||||
AutoMigration(from = 17, to = 18),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
|
||||
@@ -46,5 +46,6 @@ interface TunnelConfigDao {
|
||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
|
||||
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
||||
|
||||
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||
@Query("SELECT * FROM tunnelconfig ORDER BY position")
|
||||
fun getAllFlow(): Flow<List<TunnelConfig>>
|
||||
}
|
||||
|
||||
@@ -24,9 +24,10 @@ data class TunnelConfig(
|
||||
@ColumnInfo(name = "ping_cooldown", defaultValue = "null") val pingCooldown: Long? = null,
|
||||
@ColumnInfo(name = "ping_ip", defaultValue = "null") var pingIp: String? = null,
|
||||
@ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false")
|
||||
var isEthernetTunnel: Boolean = false,
|
||||
val isEthernetTunnel: Boolean = false,
|
||||
@ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true")
|
||||
var isIpv4Preferred: Boolean = true,
|
||||
val isIpv4Preferred: Boolean = true,
|
||||
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
+2
-2
@@ -5,11 +5,11 @@ import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
|
||||
import kotlin.collections.firstOrNull
|
||||
|
||||
object GitHubReleaseMapper {
|
||||
fun toAppUpdate(gitHubRelease: GitHubRelease): AppUpdate {
|
||||
fun toAppUpdate(gitHubRelease: GitHubRelease, newVersion: String): AppUpdate {
|
||||
with(gitHubRelease) {
|
||||
val apkAsset = assets.firstOrNull { it.name.endsWith(".apk") }
|
||||
return AppUpdate(
|
||||
version = tagName.removePrefix("v"),
|
||||
version = newVersion,
|
||||
title = name ?: "Update $tagName",
|
||||
releaseNotes = body ?: "No release notes provided",
|
||||
apkUrl = apkAsset?.browserDownloadUrl,
|
||||
|
||||
+2
@@ -21,6 +21,7 @@ object TunnelConfigMapper {
|
||||
pingIp,
|
||||
isEthernetTunnel,
|
||||
isIpv4Preferred,
|
||||
position,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -42,6 +43,7 @@ object TunnelConfigMapper {
|
||||
pingIp,
|
||||
isEthernetTunnel,
|
||||
isIpv4Preferred,
|
||||
position,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+14
-7
@@ -7,6 +7,7 @@ import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.UpdateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
@@ -31,24 +32,30 @@ class GitHubUpdateRepository(
|
||||
override suspend fun checkForUpdate(currentVersion: String): Result<AppUpdate?> =
|
||||
withContext(ioDispatcher) {
|
||||
Timber.i("Checking for update")
|
||||
val isNightly = BuildConfig.VERSION_NAME.contains("nightly")
|
||||
val release =
|
||||
if (BuildConfig.VERSION_NAME.contains("nightly")) {
|
||||
gitHubApi.getNightlyRelease(githubOwner, githubRepo)
|
||||
if (isNightly) {
|
||||
gitHubApi.getNightlyRelease(githubOwner, githubRepo).onFailure(Timber::e)
|
||||
} else {
|
||||
gitHubApi.getLatestRelease(githubOwner, githubRepo)
|
||||
gitHubApi.getLatestRelease(githubOwner, githubRepo).onFailure(Timber::e)
|
||||
}
|
||||
release.map { release ->
|
||||
val apkAsset =
|
||||
release.assets.find { asset ->
|
||||
asset.name.startsWith("wgtunnel-full-v") && asset.name.endsWith(".apk")
|
||||
asset.name.startsWith("wgtunnel-${Constants.STANDALONE_FLAVOR}-v") &&
|
||||
asset.name.endsWith(".apk")
|
||||
}
|
||||
val newVersion =
|
||||
apkAsset?.name?.removePrefix("wgtunnel-full-v")?.removeSuffix(".apk")
|
||||
?: return@map null
|
||||
apkAsset
|
||||
?.name
|
||||
?.removePrefix("wgtunnel-${Constants.STANDALONE_FLAVOR}-v")
|
||||
?.removeSuffix(".apk") ?: return@map null
|
||||
|
||||
Timber.i("Latest version: $newVersion, current version: $currentVersion")
|
||||
if (isNightly && newVersion != currentVersion)
|
||||
return@map GitHubReleaseMapper.toAppUpdate(release, newVersion)
|
||||
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
|
||||
GitHubReleaseMapper.toAppUpdate(release)
|
||||
GitHubReleaseMapper.toAppUpdate(release, newVersion)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
@@ -21,7 +22,9 @@ import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.amnezia.awg.backend.Backend
|
||||
import org.amnezia.awg.backend.GoBackend
|
||||
import org.amnezia.awg.backend.RootTunnelActionHandler
|
||||
@@ -97,6 +100,7 @@ class TunnelModule {
|
||||
appDataRepository: AppDataRepository,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
notificationManager: NotificationManager,
|
||||
): TunnelManager {
|
||||
return TunnelManager(
|
||||
kernelTunnel,
|
||||
@@ -104,6 +108,7 @@ class TunnelModule {
|
||||
appDataRepository,
|
||||
applicationScope,
|
||||
ioDispatcher,
|
||||
notificationManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -112,11 +117,22 @@ class TunnelModule {
|
||||
fun provideNetworkMonitor(
|
||||
@ApplicationContext context: Context,
|
||||
settingsRepository: AppSettingRepository,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
@AppShell appShell: RootShell,
|
||||
): NetworkMonitor {
|
||||
val method = runBlocking { settingsRepository.get().wifiDetectionMethod }
|
||||
return AndroidNetworkMonitor(
|
||||
context,
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.fromValue(method.value),
|
||||
object : AndroidNetworkMonitor.ConfigurationListener {
|
||||
override val detectionMethod: Flow<AndroidNetworkMonitor.WifiDetectionMethod>
|
||||
get() =
|
||||
settingsRepository.flow
|
||||
.distinctUntilChangedBy { it.wifiDetectionMethod }
|
||||
.map { it.wifiDetectionMethod }
|
||||
|
||||
override val rootShell: RootShell
|
||||
get() = appShell
|
||||
},
|
||||
applicationScope,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,23 +11,23 @@ sealed class BackendError : Exception() {
|
||||
|
||||
data object KernelModuleName : BackendError()
|
||||
|
||||
data object InvalidConfig : BackendError()
|
||||
|
||||
data object NotAuthorized : BackendError()
|
||||
|
||||
data object ServiceNotRunning : BackendError()
|
||||
|
||||
data object Unknown : BackendError()
|
||||
|
||||
data object TunnelNameTooLong : BackendError()
|
||||
|
||||
fun toStringRes() =
|
||||
when (this) {
|
||||
Config -> R.string.config_error
|
||||
DNS -> R.string.dns_resolve_error
|
||||
InvalidConfig -> R.string.invalid_config_error
|
||||
KernelModuleName -> R.string.kernel_name_error
|
||||
NotAuthorized,
|
||||
Unauthorized -> R.string.auth_error
|
||||
ServiceNotRunning -> R.string.service_running_error
|
||||
Unknown -> R.string.unknown_error
|
||||
TunnelNameTooLong -> R.string.error_tunnel_name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,12 @@ data class TunnelConf(
|
||||
val pingIp: String? = null,
|
||||
val isEthernetTunnel: Boolean = false,
|
||||
val isIpv4Preferred: Boolean = true,
|
||||
val position: Int = 0,
|
||||
@Transient private var stateChangeCallback: ((Any) -> Unit)? = null,
|
||||
) : Tunnel, org.amnezia.awg.backend.Tunnel {
|
||||
|
||||
val isNameKernelCompatible: Boolean = (name.length <= 15)
|
||||
|
||||
fun setStateChangeCallback(callback: (Any) -> Unit) {
|
||||
stateChangeCallback = callback
|
||||
}
|
||||
@@ -94,6 +97,7 @@ data class TunnelConf(
|
||||
pingIp,
|
||||
isEthernetTunnel,
|
||||
isIpv4Preferred,
|
||||
position,
|
||||
)
|
||||
.apply { stateChangeCallback = this@TunnelConf.stateChangeCallback }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,38 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.state
|
||||
|
||||
import com.zaneschepke.networkmonitor.ConnectivityState
|
||||
import com.zaneschepke.networkmonitor.util.WifiSecurityType
|
||||
|
||||
data class NetworkState(
|
||||
val isWifiConnected: Boolean = false,
|
||||
val isMobileDataConnected: Boolean = false,
|
||||
val isEthernetConnected: Boolean = false,
|
||||
val wifiName: String? = null,
|
||||
val isWifiSecure: Boolean? = null,
|
||||
val locationServicesEnabled: Boolean? = null,
|
||||
val locationPermissionGranted: Boolean? = null,
|
||||
) {
|
||||
fun hasNoCapabilities(): Boolean {
|
||||
return !isWifiConnected && !isMobileDataConnected && !isEthernetConnected
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(connectivityState: ConnectivityState): NetworkState {
|
||||
return NetworkState(
|
||||
isWifiSecure =
|
||||
when (connectivityState.wifiState.securityType) {
|
||||
WifiSecurityType.OPEN,
|
||||
WifiSecurityType.UNKNOWN -> false
|
||||
null -> null
|
||||
else -> true
|
||||
},
|
||||
isWifiConnected = connectivityState.wifiState.connected,
|
||||
isMobileDataConnected = connectivityState.cellularConnected,
|
||||
isEthernetConnected = connectivityState.ethernetConnected,
|
||||
wifiName = connectivityState.wifiState.ssid,
|
||||
locationPermissionGranted = connectivityState.wifiState.locationPermissionsGranted,
|
||||
locationServicesEnabled = connectivityState.wifiState.locationServicesEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,6 @@ sealed class Route {
|
||||
@Serializable data class TunnelAutoTunnel(val id: Int) : Route()
|
||||
|
||||
@Serializable data object Logs : Route()
|
||||
|
||||
@Serializable data object Sort : Route()
|
||||
}
|
||||
|
||||
+6
-33
@@ -3,68 +3,41 @@ package com.zaneschepke.wireguardautotunnel.ui.common
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.indication
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ExpandingRowListItem(
|
||||
leading: @Composable () -> Unit,
|
||||
text: String,
|
||||
onHold: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onDoubleClick: () -> Unit,
|
||||
trailing: @Composable () -> Unit,
|
||||
isSelected: Boolean,
|
||||
expanded: (@Composable () -> Unit)?,
|
||||
expanded: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.animateContentSize()
|
||||
modifier
|
||||
.animateContentSize()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
else Color.Transparent
|
||||
)
|
||||
.then(
|
||||
if (!isTv) {
|
||||
Modifier.combinedClickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = ripple(),
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onHold()
|
||||
},
|
||||
onDoubleClick = onDoubleClick,
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).height(48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
@@ -84,7 +57,7 @@ fun ExpandingRowListItem(
|
||||
}
|
||||
trailing()
|
||||
}
|
||||
expanded?.invoke()
|
||||
expanded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.banner
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||
|
||||
@Composable
|
||||
fun WarningBanner(
|
||||
title: String,
|
||||
visible: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
trailing: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
AnimatedVisibility(visible = visible, enter = expandVertically(), exit = shrinkVertically()) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = modifier.fillMaxWidth().clip(RoundedCornerShape(8.dp)),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).padding(start = 2.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.Start),
|
||||
modifier = Modifier.weight(4f, false).fillMaxWidth(),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Warning,
|
||||
stringResource(R.string.warning),
|
||||
Modifier.size(18.dp),
|
||||
tint = Straw,
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement =
|
||||
Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
|
||||
modifier = Modifier.fillMaxWidth().weight(1f).padding(start = 6.dp),
|
||||
) {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
trailing?.let {
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
) {
|
||||
it()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-23
@@ -2,34 +2,23 @@ package com.zaneschepke.wireguardautotunnel.ui.common.button
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||
|
||||
@androidx.compose.runtime.Composable
|
||||
fun IconSurfaceButton(
|
||||
title: String,
|
||||
onClick: () -> Unit,
|
||||
selected: Boolean,
|
||||
leadingIcon: ImageVector? = null,
|
||||
leading: (@Composable () -> Unit)? = null,
|
||||
description: String? = null,
|
||||
) {
|
||||
val border: BorderStroke? =
|
||||
@@ -64,15 +53,7 @@ fun IconSurfaceButton(
|
||||
modifier =
|
||||
Modifier.padding(vertical = if (description == null) 10.dp else 0.dp),
|
||||
) {
|
||||
leadingIcon?.let {
|
||||
Icon(
|
||||
leadingIcon,
|
||||
leadingIcon.name,
|
||||
Modifier.size(iconSize),
|
||||
if (selected) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
leading?.invoke()
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(title, style = MaterialTheme.typography.titleMedium)
|
||||
description?.let {
|
||||
|
||||
+5
-3
@@ -1,13 +1,15 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
|
||||
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
data class SelectionItem(
|
||||
val leadingIcon: ImageVector? = null,
|
||||
val leading: (@Composable () -> Unit)? = null,
|
||||
val trailing: (@Composable () -> Unit)? = null,
|
||||
val title: (@Composable () -> Unit),
|
||||
val description: (@Composable () -> Unit)? = null,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
val height: Int = 64,
|
||||
val modifier: Modifier = Modifier.height(64.dp),
|
||||
)
|
||||
|
||||
+8
-15
@@ -5,19 +5,18 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||
|
||||
@Composable
|
||||
fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
||||
fun SurfaceSelectionGroupButton(items: List<SelectionItem>, modifier: Modifier = Modifier) {
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
@@ -25,9 +24,10 @@ fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.then(item.onClick?.let { Modifier.clickable { it() } } ?: Modifier),
|
||||
.then(item.onClick?.let { modifier.clickable { it() } } ?: modifier),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -37,21 +37,14 @@ fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(4f, false).fillMaxWidth(),
|
||||
) {
|
||||
item.leadingIcon?.let { icon ->
|
||||
Icon(
|
||||
icon,
|
||||
icon.name,
|
||||
modifier = Modifier.size(iconSize),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
item.leading?.invoke()
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement =
|
||||
Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(start = if (item.leadingIcon != null) 16.dp else 0.dp)
|
||||
.padding(start = if (item.leading != null) 16.dp else 0.dp)
|
||||
.weight(1f)
|
||||
.padding(
|
||||
vertical = if (item.description == null) 16.dp else 6.dp
|
||||
|
||||
+34
-47
@@ -10,7 +10,6 @@ import androidx.compose.material.icons.rounded.QuestionMark
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -21,7 +20,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.BottomNavItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.isCurrentRoute
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
@@ -32,7 +30,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot
|
||||
@Composable
|
||||
fun BottomNavbar(appUiState: AppUiState) {
|
||||
val navController = LocalNavController.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
|
||||
val items =
|
||||
@@ -49,8 +46,9 @@ fun BottomNavbar(appUiState: AppUiState) {
|
||||
icon = Icons.Rounded.Bolt,
|
||||
onClick = {
|
||||
val route =
|
||||
if (appUiState.appState.isLocationDisclosureShown) Route.AutoTunnel
|
||||
else Route.LocationDisclosure
|
||||
if (appUiState.appState.isLocationDisclosureShown) {
|
||||
Route.AutoTunnel
|
||||
} else Route.LocationDisclosure
|
||||
navController.goFromRoot(route)
|
||||
},
|
||||
active = appUiState.isAutoTunnelActive,
|
||||
@@ -68,53 +66,42 @@ fun BottomNavbar(appUiState: AppUiState) {
|
||||
onClick = { navController.goFromRoot(Route.Support) },
|
||||
),
|
||||
)
|
||||
// Define ripple configuration based on platform
|
||||
val rippleConfiguration =
|
||||
if (isTv) {
|
||||
RippleConfiguration()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Apply ripple configuration only if needed
|
||||
CompositionLocalProvider(LocalRippleConfiguration provides rippleConfiguration) {
|
||||
NavigationBar(containerColor = MaterialTheme.colorScheme.surface) {
|
||||
items.forEach { item ->
|
||||
val isSelected = navBackStackEntry.isCurrentRoute(item.route::class)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
NavigationBar(containerColor = MaterialTheme.colorScheme.surface) {
|
||||
items.forEach { item ->
|
||||
val isSelected = navBackStackEntry.isCurrentRoute(item.route::class)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
if (item.active) {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
Badge(
|
||||
modifier =
|
||||
Modifier.offset(x = 8.dp, y = (-8).dp).size(6.dp),
|
||||
containerColor = SilverTree,
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(imageVector = item.icon, contentDescription = item.name)
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
if (item.active) {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
Badge(
|
||||
modifier = Modifier.offset(x = 8.dp, y = (-8).dp).size(6.dp),
|
||||
containerColor = SilverTree,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
) {
|
||||
Icon(imageVector = item.icon, contentDescription = item.name)
|
||||
}
|
||||
},
|
||||
onClick = { navController.goFromRoot(item.route) },
|
||||
selected = isSelected,
|
||||
enabled = true,
|
||||
label = null,
|
||||
alwaysShowLabel = false,
|
||||
colors =
|
||||
NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onBackground,
|
||||
indicatorColor = Color.Transparent,
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(imageVector = item.icon, contentDescription = item.name)
|
||||
}
|
||||
},
|
||||
onClick = item.onClick,
|
||||
selected = isSelected,
|
||||
enabled = true,
|
||||
label = null,
|
||||
alwaysShowLabel = false,
|
||||
colors =
|
||||
NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onBackground,
|
||||
indicatorColor = Color.Transparent,
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+47
-59
@@ -4,6 +4,7 @@ import android.os.Build
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.Sort
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -11,6 +12,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -23,11 +25,10 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.isCurrentRoute
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.NavBarState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Brick
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
|
||||
|
||||
@Composable
|
||||
fun currentNavBackStackEntryAsNavBarState(
|
||||
@@ -60,35 +61,40 @@ fun currentNavBackStackEntryAsNavBarState(
|
||||
|
||||
Row {
|
||||
if (selectedCount == 0) {
|
||||
val showSort = remember(uiState.tunnels) { uiState.tunnels.size > 1 }
|
||||
if (showSort)
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.Sort, R.string.sort) {
|
||||
navController.navigate(Route.Sort)
|
||||
}
|
||||
ActionIconButton(Icons.Rounded.Add, R.string.add_tunnel) {
|
||||
viewModel.handleEvent(
|
||||
AppEvent.SetBottomSheet(AppViewState.BottomSheet.IMPORT_TUNNELS)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ActionIconButton(Icons.Rounded.SelectAll, R.string.select_all) {
|
||||
viewModel.handleEvent(AppEvent.ToggleSelectAllTunnels)
|
||||
}
|
||||
// due to permissions, and SAF issues on TV, not support less than Android 10 on
|
||||
// Android TV for file exports
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ActionIconButton(Icons.Rounded.Download, R.string.download) {
|
||||
viewModel.handleEvent(
|
||||
AppEvent.SetBottomSheet(AppViewState.BottomSheet.EXPORT_TUNNELS)
|
||||
)
|
||||
}
|
||||
return@Row
|
||||
}
|
||||
ActionIconButton(Icons.Rounded.SelectAll, R.string.select_all) {
|
||||
viewModel.handleEvent(AppEvent.ToggleSelectAllTunnels)
|
||||
}
|
||||
// due to permissions, and SAF issues on TV, not support less than Android 10 on
|
||||
// Android TV for file exports
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ActionIconButton(Icons.Rounded.Download, R.string.download) {
|
||||
viewModel.handleEvent(
|
||||
AppEvent.SetBottomSheet(AppViewState.BottomSheet.EXPORT_TUNNELS)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedCount == 1) {
|
||||
ActionIconButton(Icons.Rounded.CopyAll, R.string.copy) {
|
||||
viewModel.handleEvent(AppEvent.CopySelectedTunnel)
|
||||
}
|
||||
if (selectedCount == 1) {
|
||||
ActionIconButton(Icons.Rounded.CopyAll, R.string.copy) {
|
||||
viewModel.handleEvent(AppEvent.CopySelectedTunnel)
|
||||
}
|
||||
}
|
||||
|
||||
if (showDelete) {
|
||||
ActionIconButton(Icons.Rounded.Delete, R.string.delete_tunnel) {
|
||||
viewModel.handleEvent(AppEvent.SetShowModal(AppViewState.ModalType.DELETE))
|
||||
}
|
||||
if (showDelete) {
|
||||
ActionIconButton(Icons.Rounded.Delete, R.string.delete_tunnel) {
|
||||
viewModel.handleEvent(AppEvent.SetShowModal(AppViewState.ModalType.DELETE))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,8 +110,6 @@ fun currentNavBackStackEntryAsNavBarState(
|
||||
when {
|
||||
backStackEntry.isCurrentRoute(Route.Main::class) -> {
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.tunnels)) },
|
||||
topTrailing = { TunnelActionBar() },
|
||||
route = Route.Main,
|
||||
@@ -113,36 +117,15 @@ fun currentNavBackStackEntryAsNavBarState(
|
||||
}
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.AutoTunnel::class) -> {
|
||||
val (icon, label, tint) =
|
||||
if (uiState.appSettings.isAutoTunnelEnabled) {
|
||||
Triple(Icons.Rounded.Stop, R.string.stop_auto, Brick)
|
||||
} else {
|
||||
Triple(Icons.Rounded.PlayArrow, R.string.start_auto, SilverTree)
|
||||
}
|
||||
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.auto_tunnel)) },
|
||||
topTrailing = {
|
||||
IconButton(
|
||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnel) }
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
stringResource(label),
|
||||
tint = tint,
|
||||
modifier = Modifier.size(iconSize),
|
||||
)
|
||||
}
|
||||
},
|
||||
route = Route.AutoTunnel,
|
||||
)
|
||||
}
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Logs::class) -> {
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = false,
|
||||
topTitle = { Text(stringResource(R.string.logs)) },
|
||||
topTrailing = {
|
||||
@@ -158,60 +141,65 @@ fun currentNavBackStackEntryAsNavBarState(
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Settings::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.settings)) },
|
||||
route = Route.Settings,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Appearance::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.appearance)) },
|
||||
route = Route.Appearance,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Language::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.language)) },
|
||||
route = Route.Language,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Display::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.display_theme)) },
|
||||
route = Route.Display,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.WifiDetectionMethod::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.wifi_detection_method)) },
|
||||
route = Route.WifiDetectionMethod,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.KillSwitch::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.kill_switch)) },
|
||||
route = Route.KillSwitch,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Support::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.support)) },
|
||||
route = Route.Support,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Sort::class) -> {
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.sort)) },
|
||||
topTrailing = {
|
||||
Row {
|
||||
ActionIconButton(Icons.Rounded.SortByAlpha, R.string.sort) {
|
||||
viewModel.handleUiEvent(UiEvent.SortTunnels)
|
||||
}
|
||||
ActionIconButton(Icons.Rounded.Save, R.string.save) {
|
||||
viewModel.handleEvent(AppEvent.InvokeScreenAction)
|
||||
}
|
||||
}
|
||||
},
|
||||
route = Route.Sort,
|
||||
)
|
||||
}
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.License::class) -> {
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
|
||||
+109
-59
@@ -1,93 +1,93 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
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.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CheckCircle
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.banner.WarningBanner
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.NetworkTunnelingItems
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.WifiTunnelingItems
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isLocationServicesEnabled
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchLocationServicesSettings
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun AutoTunnelScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
val context = LocalContext.current
|
||||
|
||||
var currentText by remember { mutableStateOf("") }
|
||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||
var showLocationDialog by remember { mutableStateOf(false) }
|
||||
|
||||
fun checkFineLocationGranted() {
|
||||
isBackgroundLocationGranted = fineLocationState.status.isGranted
|
||||
}
|
||||
|
||||
fun isWifiNameReadable(): Boolean {
|
||||
return when {
|
||||
!isBackgroundLocationGranted || !fineLocationState.status.isGranted -> {
|
||||
showLocationDialog = true
|
||||
false
|
||||
val showLocationServicesWarning by
|
||||
remember(
|
||||
uiState.connectivityState?.wifiState,
|
||||
uiState.appSettings.trustedNetworkSSIDs,
|
||||
uiState.appSettings.wifiDetectionMethod,
|
||||
) {
|
||||
derivedStateOf {
|
||||
uiState.connectivityState?.wifiState?.locationServicesEnabled == false &&
|
||||
uiState.appSettings.wifiDetectionMethod.needsLocationPermissions() &&
|
||||
uiState.appSettings.trustedNetworkSSIDs.isNotEmpty()
|
||||
}
|
||||
!context.isLocationServicesEnabled() -> {
|
||||
showLocationServicesAlertDialog = true
|
||||
false
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) checkFineLocationGranted()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if (isTv && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||
checkFineLocationGranted()
|
||||
} else {
|
||||
val backgroundLocationState =
|
||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
val showLocationPermissionsWarning by
|
||||
remember(
|
||||
uiState.connectivityState?.wifiState,
|
||||
uiState.appSettings.trustedNetworkSSIDs,
|
||||
uiState.appSettings.wifiDetectionMethod,
|
||||
) {
|
||||
derivedStateOf {
|
||||
uiState.connectivityState?.wifiState?.locationPermissionsGranted == false &&
|
||||
uiState.appSettings.wifiDetectionMethod.needsLocationPermissions() &&
|
||||
uiState.appSettings.trustedNetworkSSIDs.isNotEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.appSettings.trustedNetworkSSIDs) { currentText = "" }
|
||||
|
||||
LocationServicesDialog(
|
||||
showLocationServicesAlertDialog,
|
||||
onDismiss = { showLocationServicesAlertDialog = false },
|
||||
onAttest = { showLocationServicesAlertDialog = false },
|
||||
)
|
||||
|
||||
BackgroundLocationDialog(
|
||||
showLocationDialog,
|
||||
onDismiss = { showLocationDialog = false },
|
||||
onAttest = { showLocationDialog = false },
|
||||
)
|
||||
if (showLocationDialog) {
|
||||
InfoDialog(
|
||||
onAttest = {
|
||||
context.launchAppSettings()
|
||||
showLocationDialog = false
|
||||
},
|
||||
onDismiss = { showLocationDialog = false },
|
||||
title = { Text(stringResource(R.string.location_permissions)) },
|
||||
body = { Text(stringResource(R.string.location_justification)) },
|
||||
confirmText = { Text(stringResource(R.string.open_settings)) },
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -98,16 +98,66 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
.padding(vertical = 24.dp)
|
||||
.padding(horizontal = 12.dp),
|
||||
) {
|
||||
WarningBanner(
|
||||
stringResource(R.string.location_services_not_detected),
|
||||
showLocationServicesWarning,
|
||||
trailing = {
|
||||
TextButton({ context.launchLocationServicesSettings() }) {
|
||||
Text(
|
||||
stringResource(R.string.fix),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
WarningBanner(
|
||||
stringResource(R.string.location_permissions_missing),
|
||||
showLocationPermissionsWarning,
|
||||
trailing = {
|
||||
TextButton({ showLocationDialog = true }) {
|
||||
Text(
|
||||
stringResource(R.string.fix),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
val (title, buttonText, icon) =
|
||||
remember(uiState.isAutoTunnelActive) {
|
||||
when (uiState.isAutoTunnelActive) {
|
||||
true ->
|
||||
Triple(
|
||||
context.getString(R.string.auto_tunnel_running),
|
||||
context.getString(R.string.stop),
|
||||
Icons.Outlined.CheckCircle,
|
||||
)
|
||||
false ->
|
||||
Triple(
|
||||
context.getString(R.string.auto_tunnel_not_running),
|
||||
context.getString(R.string.start),
|
||||
Icons.Outlined.Info,
|
||||
)
|
||||
}
|
||||
}
|
||||
SurfaceSelectionGroupButton(
|
||||
items =
|
||||
WifiTunnelingItems(
|
||||
uiState,
|
||||
viewModel,
|
||||
currentText,
|
||||
{ currentText = it },
|
||||
{ isWifiNameReadable() },
|
||||
listOf(
|
||||
SelectionItem(
|
||||
leading = { Icon(icon, null) },
|
||||
title = { Text(title) },
|
||||
trailing = {
|
||||
Button({ viewModel.handleEvent(AppEvent.ToggleAutoTunnel) }) {
|
||||
Text(buttonText, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
SurfaceSelectionGroupButton(
|
||||
items = WifiTunnelingItems(uiState, viewModel, currentText) { currentText = it }
|
||||
)
|
||||
SectionDivider()
|
||||
SurfaceSelectionGroupButton(items = NetworkTunnelingItems(uiState, viewModel))
|
||||
SectionDivider()
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.compo
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.PauseCircle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -23,7 +24,7 @@ fun DebounceDelaySelector(currentDelay: Int, onEvent: (AppEvent) -> Unit) {
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.PauseCircle,
|
||||
leading = { Icon(Icons.Outlined.PauseCircle, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.debounce_delay),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -13,7 +14,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
|
||||
@Composable
|
||||
fun AdvancedSettingsItem(onClick: () -> Unit): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Settings,
|
||||
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.advanced_settings),
|
||||
|
||||
+8
-7
@@ -4,6 +4,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.PublicOff
|
||||
import androidx.compose.material.icons.outlined.SettingsEthernet
|
||||
import androidx.compose.material.icons.outlined.SignalCellular4Bar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -21,7 +22,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<SelectionItem> {
|
||||
return listOf(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.SignalCellular4Bar,
|
||||
leading = { Icon(Icons.Outlined.SignalCellular4Bar, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.tunnel_mobile_data),
|
||||
@@ -40,8 +41,8 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
|
||||
},
|
||||
description = {
|
||||
val cellularActive =
|
||||
remember(uiState.networkStatus) {
|
||||
uiState.networkStatus?.cellularConnected ?: false
|
||||
remember(uiState.connectivityState) {
|
||||
uiState.connectivityState?.cellularConnected ?: false
|
||||
}
|
||||
Text(
|
||||
text =
|
||||
@@ -58,7 +59,7 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
|
||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnCellular) },
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.SettingsEthernet,
|
||||
leading = { Icon(Icons.Outlined.SettingsEthernet, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.tunnel_on_ethernet),
|
||||
@@ -77,8 +78,8 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
|
||||
},
|
||||
description = {
|
||||
val ethernetActive =
|
||||
remember(uiState.networkStatus) {
|
||||
uiState.networkStatus?.ethernetConnected ?: false
|
||||
remember(uiState.connectivityState) {
|
||||
uiState.connectivityState?.ethernetConnected ?: false
|
||||
}
|
||||
Text(
|
||||
text =
|
||||
@@ -95,7 +96,7 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
|
||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnEthernet) },
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.PublicOff,
|
||||
leading = { Icon(Icons.Outlined.PublicOff, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.stop_on_no_internet),
|
||||
|
||||
+10
-18
@@ -17,8 +17,6 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
|
||||
@@ -40,7 +38,6 @@ fun WifiTunnelingItems(
|
||||
viewModel: AppViewModel,
|
||||
currentText: String,
|
||||
onTextChange: (String) -> Unit,
|
||||
isWifiNameReadable: () -> Boolean,
|
||||
): List<SelectionItem> {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
@@ -49,7 +46,7 @@ fun WifiTunnelingItems(
|
||||
val baseItems =
|
||||
listOf(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Wifi,
|
||||
leading = { Icon(Icons.Outlined.Wifi, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.tunnel_on_wifi),
|
||||
@@ -68,11 +65,12 @@ fun WifiTunnelingItems(
|
||||
},
|
||||
description = {
|
||||
val wifiInfo by
|
||||
remember(uiState.networkStatus) {
|
||||
remember(uiState.connectivityState) {
|
||||
derivedStateOf {
|
||||
(uiState.networkStatus as? NetworkStatus.Connected)
|
||||
?.takeIf { it.wifiConnected }
|
||||
.let { Pair(it?.wifiSsid, it?.securityType) }
|
||||
uiState.connectivityState
|
||||
?.wifiState
|
||||
?.takeIf { it.connected }
|
||||
.let { Pair(it?.ssid, it?.securityType) }
|
||||
}
|
||||
}
|
||||
val (wifiName, securityType) = wifiInfo
|
||||
@@ -111,7 +109,7 @@ fun WifiTunnelingItems(
|
||||
baseItems +
|
||||
listOf(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.WifiFind,
|
||||
leading = { Icon(Icons.Outlined.WifiFind, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.wifi_detection_method),
|
||||
@@ -139,7 +137,7 @@ fun WifiTunnelingItems(
|
||||
onClick = { navController.navigate(Route.WifiDetectionMethod) },
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Filter1,
|
||||
leading = { Icon(Icons.Outlined.Filter1, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.use_wildcards),
|
||||
@@ -201,13 +199,7 @@ fun WifiTunnelingItems(
|
||||
onDelete = { viewModel.handleEvent(AppEvent.DeleteTrustedSSID(it)) },
|
||||
currentText = currentText,
|
||||
onSave = { ssid ->
|
||||
if (
|
||||
uiState.appSettings.wifiDetectionMethod ==
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.ROOT ||
|
||||
isWifiNameReadable()
|
||||
) {
|
||||
viewModel.handleEvent(AppEvent.SaveTrustedSSID(ssid))
|
||||
}
|
||||
viewModel.handleEvent(AppEvent.SaveTrustedSSID(ssid))
|
||||
},
|
||||
onValueChange = onTextChange,
|
||||
supporting = {
|
||||
@@ -217,7 +209,7 @@ fun WifiTunnelingItems(
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.VpnKeyOff,
|
||||
leading = { Icon(Icons.Outlined.VpnKeyOff, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.kill_switch_off),
|
||||
-2
@@ -28,8 +28,6 @@ fun WifiDetectionMethodScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
enumValues<AndroidNetworkMonitor.WifiDetectionMethod>().forEach {
|
||||
val title = it.asString(context)
|
||||
val description = it.asDescriptionString(context)
|
||||
// TODO skip shizuku for now
|
||||
if (it == AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU) return@forEach
|
||||
IconSurfaceButton(
|
||||
title = title,
|
||||
onClick = { viewModel.handleEvent(AppEvent.SetDetectionMethod(it)) },
|
||||
|
||||
+7
-14
@@ -9,24 +9,17 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.AppSettingsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.LocationDisclosureHeader
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.SkipItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.appSettingsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.skipItem
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
|
||||
@Composable
|
||||
fun LocationDisclosureScreen(appUiState: AppUiState, viewModel: AppViewModel) {
|
||||
val navController = LocalNavController.current
|
||||
fun LocationDisclosureScreen(viewModel: AppViewModel) {
|
||||
|
||||
LaunchedEffect(Unit, appUiState) {
|
||||
if (appUiState.appState.isLocationDisclosureShown)
|
||||
navController.goFromRoot(Route.AutoTunnel)
|
||||
}
|
||||
LaunchedEffect(Unit) { viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) }
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -34,7 +27,7 @@ fun LocationDisclosureScreen(appUiState: AppUiState, viewModel: AppViewModel) {
|
||||
modifier = Modifier.fillMaxSize().padding(top = 18.dp).padding(horizontal = 24.dp),
|
||||
) {
|
||||
LocationDisclosureHeader()
|
||||
SurfaceSelectionGroupButton(items = listOf(AppSettingsItem(viewModel)))
|
||||
SurfaceSelectionGroupButton(items = listOf(SkipItem(viewModel)))
|
||||
SurfaceSelectionGroupButton(items = listOf(appSettingsItem()))
|
||||
SurfaceSelectionGroupButton(items = listOf(skipItem()))
|
||||
}
|
||||
}
|
||||
|
||||
+6
-16
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.com
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.LocationOn
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -11,31 +12,20 @@ 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.util.extensions.launchAppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
|
||||
@Composable
|
||||
fun AppSettingsItem(viewModel: AppViewModel): SelectionItem {
|
||||
fun appSettingsItem(): SelectionItem {
|
||||
val context = LocalContext.current
|
||||
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.LocationOn,
|
||||
leading = { Icon(Icons.Outlined.LocationOn, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.launch_app_settings),
|
||||
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton {
|
||||
context.launchAppSettings().also {
|
||||
viewModel.handleEvent(AppEvent.SetLocationDisclosureShown)
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
context.launchAppSettings().also {
|
||||
viewModel.handleEvent(AppEvent.SetLocationDisclosureShown)
|
||||
}
|
||||
},
|
||||
trailing = { ForwardButton { context.launchAppSettings() } },
|
||||
onClick = { context.launchAppSettings() },
|
||||
)
|
||||
}
|
||||
|
||||
+7
-5
@@ -5,13 +5,15 @@ 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.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot
|
||||
|
||||
@Composable
|
||||
fun SkipItem(viewModel: AppViewModel): SelectionItem {
|
||||
fun skipItem(): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
title = {
|
||||
Text(
|
||||
@@ -19,7 +21,7 @@ fun SkipItem(viewModel: AppViewModel): SelectionItem {
|
||||
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = { ForwardButton { viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) } },
|
||||
onClick = { viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) },
|
||||
trailing = { ForwardButton { navController.goFromRoot(Route.AutoTunnel) } },
|
||||
onClick = { navController.goFromRoot(Route.AutoTunnel) },
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -18,9 +18,9 @@ import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.EthernetTunnelItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.MobileDataTunnelItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.WifiTunnelItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.ethernetTunnelItem
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
|
||||
@Composable
|
||||
@@ -46,7 +46,7 @@ fun TunnelAutoTunnelScreen(
|
||||
items =
|
||||
buildList {
|
||||
add(MobileDataTunnelItem(tunnelConf, viewModel))
|
||||
add(EthernetTunnelItem(tunnelConf, viewModel))
|
||||
add(ethernetTunnelItem(tunnelConf, viewModel))
|
||||
add(
|
||||
WifiTunnelItem(tunnelConf, appSettings, viewModel, currentText) {
|
||||
currentText = it
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.component
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.PhoneAndroid
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun MobileDataTunnelItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.PhoneAndroid,
|
||||
leading = { Icon(Icons.Outlined.PhoneAndroid, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.mobile_tunnel),
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.component
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.SettingsEthernet
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -14,9 +15,9 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
|
||||
@Composable
|
||||
fun EthernetTunnelItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
fun ethernetTunnelItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.SettingsEthernet,
|
||||
leading = { Icon(Icons.Outlined.SettingsEthernet, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.ethernet_tunnel),
|
||||
+4
-14
@@ -5,19 +5,9 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.FolderZip
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material.icons.outlined.FolderZip
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -121,7 +111,7 @@ fun ExportTunnelsBottomSheet(viewModel: AppViewModel) {
|
||||
private fun ExportOptionRow(label: String, onClick: () -> Unit) {
|
||||
Row(modifier = Modifier.fillMaxWidth().clickable(onClick = onClick).padding(10.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.FolderZip,
|
||||
imageVector = Icons.Outlined.FolderZip,
|
||||
contentDescription = label,
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
|
||||
+27
-26
@@ -1,7 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -25,8 +27,6 @@ import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
import java.text.Collator
|
||||
import java.util.*
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -40,17 +40,8 @@ fun TunnelList(
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val collator = Collator.getInstance(Locale.getDefault())
|
||||
val sortedTunnels =
|
||||
remember(appUiState.tunnels) {
|
||||
appUiState.tunnels.sortedWith(
|
||||
compareBy(
|
||||
// primary tunnel first
|
||||
{ !it.isPrimaryTunnel },
|
||||
{ collator.compare(it.tunName, "") },
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -59,7 +50,7 @@ fun TunnelList(
|
||||
modifier
|
||||
.pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput }
|
||||
.overscroll(rememberOverscrollEffect()),
|
||||
state = rememberLazyListState(0, appUiState.tunnels.count()),
|
||||
state = lazyListState,
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = false,
|
||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
@@ -67,7 +58,7 @@ fun TunnelList(
|
||||
if (appUiState.tunnels.isEmpty()) {
|
||||
item { GettingStartedLabel(onClick = { context.openWebUrl(it) }) }
|
||||
}
|
||||
items(sortedTunnels, key = { it.id }) { tunnel ->
|
||||
items(appUiState.tunnels, key = { it.id }) { tunnel ->
|
||||
val tunnelState =
|
||||
remember(appUiState.activeTunnels) {
|
||||
appUiState.activeTunnels.getValueById(tunnel.id) ?: TunnelState()
|
||||
@@ -75,26 +66,36 @@ fun TunnelList(
|
||||
val selected = remember(selectedTunnels) { selectedTunnels.any { it.id == tunnel.id } }
|
||||
TunnelRowItem(
|
||||
state = tunnelState,
|
||||
expanded = appUiState.appState.expandedTunnelIds.contains(tunnel.id),
|
||||
isSelected = selected,
|
||||
tunnel = tunnel,
|
||||
tunnelState = tunnelState,
|
||||
onClick = {
|
||||
if (selectedTunnels.isNotEmpty() && !isTv) {
|
||||
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
|
||||
} else {
|
||||
navController.navigate(Route.TunnelOptions(tunnel.id))
|
||||
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
|
||||
}
|
||||
},
|
||||
onDoubleClick = {
|
||||
viewModel.handleEvent(AppEvent.ToggleTunnelStatsExpanded(tunnel.id))
|
||||
onTvClick = {
|
||||
navController.navigate(Route.TunnelOptions(tunnel.id))
|
||||
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
|
||||
},
|
||||
onToggleSelectedTunnel = {
|
||||
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(it))
|
||||
},
|
||||
onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) },
|
||||
isTv = isTv,
|
||||
modifier =
|
||||
if (!isTv)
|
||||
Modifier.combinedClickable(
|
||||
onClick = {
|
||||
if (selectedTunnels.isNotEmpty()) {
|
||||
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
|
||||
} else {
|
||||
navController.navigate(Route.TunnelOptions(tunnel.id))
|
||||
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
|
||||
},
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
)
|
||||
else Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+47
-23
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Circle
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.icons.rounded.SettingsEthernet
|
||||
import androidx.compose.material.icons.rounded.Smartphone
|
||||
@@ -19,9 +18,12 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
|
||||
@@ -32,31 +34,62 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
|
||||
fun TunnelRowItem(
|
||||
state: TunnelState,
|
||||
isSelected: Boolean,
|
||||
expanded: Boolean,
|
||||
tunnel: TunnelConf,
|
||||
tunnelState: TunnelState,
|
||||
onClick: () -> Unit,
|
||||
onDoubleClick: () -> Unit,
|
||||
onTvClick: () -> Unit,
|
||||
onToggleSelectedTunnel: (TunnelConf) -> Unit,
|
||||
onSwitchClick: (Boolean) -> Unit,
|
||||
isTv: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val leadingIconColor =
|
||||
remember(state) {
|
||||
if (state.status.isUp()) tunnelState.statistics.asColor() else Color.Gray
|
||||
}
|
||||
|
||||
val (leadingIcon, size) =
|
||||
val (leadingIcon, size, typeDescription) =
|
||||
remember(tunnel) {
|
||||
when {
|
||||
tunnel.isPrimaryTunnel -> Pair(Icons.Rounded.Star, 16.dp)
|
||||
tunnel.isMobileDataTunnel -> Pair(Icons.Rounded.Smartphone, 16.dp)
|
||||
tunnel.isEthernetTunnel -> Pair(Icons.Rounded.SettingsEthernet, 16.dp)
|
||||
else -> Pair(Icons.Rounded.Circle, 14.dp)
|
||||
tunnel.isPrimaryTunnel ->
|
||||
Triple(Icons.Rounded.Star, 16.dp, context.getString(R.string.primary_tunnel))
|
||||
tunnel.isMobileDataTunnel ->
|
||||
Triple(
|
||||
Icons.Rounded.Smartphone,
|
||||
16.dp,
|
||||
context.getString(R.string.mobile_data_tunnel),
|
||||
)
|
||||
tunnel.isEthernetTunnel ->
|
||||
Triple(
|
||||
Icons.Rounded.SettingsEthernet,
|
||||
16.dp,
|
||||
context.getString(R.string.ethernet_tunnel),
|
||||
)
|
||||
else -> Triple(Icons.Rounded.Circle, 14.dp, context.getString(R.string.tunnel))
|
||||
}
|
||||
}
|
||||
|
||||
// Status description based on tunnel state
|
||||
val statusDescription =
|
||||
remember(state) {
|
||||
if (state.status.isUpOrStarting()) {
|
||||
context.getString(R.string.active)
|
||||
} else {
|
||||
context.getString(R.string.inactive)
|
||||
}
|
||||
}
|
||||
|
||||
// Combined content description for accessibility
|
||||
val combinedContentDescription =
|
||||
stringResource(
|
||||
R.string.tunnel_item_description,
|
||||
tunnel.tunName,
|
||||
typeDescription,
|
||||
statusDescription,
|
||||
)
|
||||
|
||||
ExpandingRowListItem(
|
||||
modifier = modifier.semantics(mergeDescendants = true) { combinedContentDescription },
|
||||
leading = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -64,27 +97,24 @@ fun TunnelRowItem(
|
||||
) {
|
||||
if (isTv) {
|
||||
Checkbox(
|
||||
isSelected,
|
||||
checked = isSelected,
|
||||
onCheckedChange = { onToggleSelectedTunnel(tunnel) },
|
||||
modifier = Modifier.minimumInteractiveComponentSize().size(12.dp),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
leadingIcon,
|
||||
stringResource(R.string.status),
|
||||
contentDescription = null,
|
||||
tint = leadingIconColor,
|
||||
modifier = Modifier.size(size),
|
||||
)
|
||||
}
|
||||
},
|
||||
text = tunnel.tunName,
|
||||
onHold = { if (!isTv) onToggleSelectedTunnel(tunnel) },
|
||||
onClick = { if (!isTv) onClick() },
|
||||
onDoubleClick = { if (!isTv) onDoubleClick() },
|
||||
expanded = {
|
||||
if (expanded) {
|
||||
if (tunnelState.status != TunnelStatus.Down) {
|
||||
TunnelStatisticsRow(tunnelState.statistics, tunnel)
|
||||
} else null
|
||||
}
|
||||
},
|
||||
trailing = {
|
||||
Row(
|
||||
@@ -92,13 +122,7 @@ fun TunnelRowItem(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
||||
) {
|
||||
if (isTv) {
|
||||
IconButton(onClick = onDoubleClick) {
|
||||
Icon(
|
||||
Icons.Rounded.KeyboardArrowDown,
|
||||
contentDescription = stringResource(R.string.info),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onClick) {
|
||||
IconButton(onClick = onTvClick) {
|
||||
Icon(
|
||||
Icons.Rounded.Settings,
|
||||
contentDescription = stringResource(R.string.settings),
|
||||
|
||||
+13
-14
@@ -1,10 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -97,15 +94,17 @@ fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConf: TunnelConf) {
|
||||
)
|
||||
}
|
||||
if (endpoint != null) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
|
||||
) {
|
||||
Text(
|
||||
"endpoint: $endpoint",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
AnimatedVisibility(visible = true) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.endpoint).lowercase() + ": $endpoint",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.sort
|
||||
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.overscroll
|
||||
import androidx.compose.foundation.rememberOverscrollEffect
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.filled.DragHandle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isSortedBy
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
|
||||
import sh.calvin.reorderable.DragGestureDetector
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@Composable
|
||||
fun SortScreen(appUiState: AppUiState, viewModel: AppViewModel) {
|
||||
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
|
||||
var sortAscending by remember { mutableStateOf<Boolean?>(null) }
|
||||
var sortedTunnels by remember { mutableStateOf(appUiState.tunnels.sortedBy { it.position }) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.uiEvent.collect { uiEvent ->
|
||||
when (uiEvent) {
|
||||
UiEvent.SortTunnels -> {
|
||||
sortAscending =
|
||||
when (sortAscending) {
|
||||
null -> !sortedTunnels.isSortedBy { it.name }
|
||||
true -> false
|
||||
false -> null
|
||||
}
|
||||
sortedTunnels =
|
||||
when (sortAscending) {
|
||||
true -> sortedTunnels.sortedBy { it.name }
|
||||
false -> sortedTunnels.sortedByDescending { it.name }
|
||||
null -> sortedTunnels.sortedBy { it.position }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.handleEvent(
|
||||
AppEvent.SetScreenAction {
|
||||
viewModel.handleEvent(
|
||||
AppEvent.SaveAllConfigs(
|
||||
sortedTunnels.mapIndexed { index, conf -> conf.copy(position = index) }
|
||||
)
|
||||
)
|
||||
viewModel.handleEvent(AppEvent.PopBackStack(true))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
val reorderableLazyListState =
|
||||
rememberReorderableLazyListState(
|
||||
lazyListState,
|
||||
scrollThresholdPadding = WindowInsets.systemBars.asPaddingValues(),
|
||||
) { from, to ->
|
||||
sortedTunnels =
|
||||
sortedTunnels.toMutableList().apply { add(to.index, removeAt(from.index)) }
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
|
||||
modifier =
|
||||
Modifier.pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput }
|
||||
.overscroll(rememberOverscrollEffect())
|
||||
.padding(horizontal = 16.dp, vertical = 24.dp),
|
||||
state = lazyListState,
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = false,
|
||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
) {
|
||||
itemsIndexed(sortedTunnels, key = { _, tunnel -> tunnel.id }) { index, tunnel ->
|
||||
ReorderableItem(reorderableLazyListState, tunnel.id) { isDragging ->
|
||||
ExpandingRowListItem(
|
||||
leading = {},
|
||||
text = tunnel.name,
|
||||
trailing = {
|
||||
if (!isTv)
|
||||
Icon(
|
||||
Icons.Default.DragHandle,
|
||||
stringResource(
|
||||
com.zaneschepke.wireguardautotunnel.R.string.drag_handle
|
||||
),
|
||||
)
|
||||
else
|
||||
Row {
|
||||
IconButton(
|
||||
onClick = {
|
||||
sortedTunnels =
|
||||
sortedTunnels.toMutableList().apply {
|
||||
add(index - 1, removeAt(index))
|
||||
}
|
||||
},
|
||||
enabled = index != 0,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ArrowUpward,
|
||||
stringResource(
|
||||
com.zaneschepke.wireguardautotunnel.R.string.move_up
|
||||
),
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
sortedTunnels =
|
||||
sortedTunnels.toMutableList().apply {
|
||||
add(index + 1, removeAt(index))
|
||||
}
|
||||
},
|
||||
enabled = index != sortedTunnels.count() - 1,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ArrowDownward,
|
||||
stringResource(R.string.move_down),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
isSelected = isDragging,
|
||||
expanded = {},
|
||||
modifier =
|
||||
Modifier.draggableHandle(
|
||||
onDragStarted = {
|
||||
hapticFeedback.performHapticFeedback(
|
||||
HapticFeedbackType.GestureThresholdActivate
|
||||
)
|
||||
},
|
||||
onDragStopped = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd)
|
||||
},
|
||||
dragGestureDetector = DragGestureDetector.LongPress,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -68,7 +68,7 @@ fun TunnelOptionsScreen(
|
||||
listOf(
|
||||
PrimaryTunnelItem(tunnelConf, viewModel),
|
||||
AutoTunnelingItem(tunnelConf),
|
||||
ServerIpv4Item(tunnelConf, viewModel),
|
||||
serverIpv4Item(tunnelConf, viewModel),
|
||||
SplitTunnelingItem(tunnelConf),
|
||||
)
|
||||
)
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.compon
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Bolt
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -17,7 +18,7 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
fun AutoTunnelingItem(tunnelConf: TunnelConf): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Bolt,
|
||||
leading = { Icon(Icons.Outlined.Bolt, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.auto_tunneling),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.compon
|
||||
|
||||
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
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun PingRestartItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.NetworkPing,
|
||||
leading = { Icon(Icons.Outlined.NetworkPing, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.restart_on_ping),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.compon
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Star
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun PrimaryTunnelItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Star,
|
||||
leading = { Icon(Icons.Outlined.Star, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.primary_tunnel),
|
||||
|
||||
+6
-5
@@ -157,11 +157,12 @@ private fun ConfigTypeSelector(selectedOption: ConfigType, onOptionSelected: (Co
|
||||
}
|
||||
},
|
||||
colors =
|
||||
SegmentedButtonDefaults.colors()
|
||||
.copy(
|
||||
activeContainerColor = Color.White,
|
||||
inactiveContainerColor = Color.White,
|
||||
),
|
||||
SegmentedButtonDefaults.colors(
|
||||
activeContainerColor = Color.White,
|
||||
inactiveContainerColor = Color.White,
|
||||
activeContentColor = Color.Black,
|
||||
inactiveContentColor = Color.Black,
|
||||
),
|
||||
onCheckedChange = { onOptionSelected(entry) },
|
||||
checked = isActive,
|
||||
) {
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.compon
|
||||
|
||||
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
|
||||
@@ -17,7 +18,7 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
fun SplitTunnelingItem(tunnelConf: TunnelConf): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.AutoMirrored.Outlined.CallSplit,
|
||||
leading = { Icon(Icons.AutoMirrored.Outlined.CallSplit, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.splt_tunneling),
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.compon
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Dns
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -14,9 +15,9 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
|
||||
@Composable
|
||||
fun ServerIpv4Item(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
fun serverIpv4Item(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Dns,
|
||||
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.server_ipv4),
|
||||
+6
-6
@@ -20,15 +20,15 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelec
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AlwaysOnVpnItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AppShortcutsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AppearanceItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.KernelModeItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.KillSwitchItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocalLoggingItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.PinLockItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ReadLogsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.RestartAtBootItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.alwaysOnVpnItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.appearanceItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.killSwitchItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
|
||||
@@ -64,8 +64,8 @@ fun SettingsScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
items =
|
||||
buildList {
|
||||
add(AppShortcutsItem(uiState, viewModel))
|
||||
if (!isTv) add(AlwaysOnVpnItem(uiState, viewModel))
|
||||
add(KillSwitchItem())
|
||||
if (!isTv) add(alwaysOnVpnItem(uiState, viewModel))
|
||||
add(killSwitchItem())
|
||||
add(RestartAtBootItem(uiState, viewModel))
|
||||
}
|
||||
)
|
||||
@@ -73,7 +73,7 @@ fun SettingsScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
SurfaceSelectionGroupButton(
|
||||
items =
|
||||
buildList {
|
||||
add(AppearanceItem())
|
||||
add(appearanceItem())
|
||||
add(LocalLoggingItem(uiState, viewModel))
|
||||
if (uiState.appState.isLocalLogsEnabled) add(ReadLogsItem())
|
||||
add(PinLockItem(uiState, viewModel))
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.SmartToy
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -23,7 +24,7 @@ fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionIt
|
||||
val clipboardManager = rememberClipboardHelper()
|
||||
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Filled.SmartToy,
|
||||
leading = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = uiState.appState.isRemoteControlEnabled,
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.compo
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Contrast
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
fun DisplayThemeItem(): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Contrast,
|
||||
leading = { Icon(Icons.Outlined.Contrast, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.display_theme),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.compo
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Translate
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
fun LanguageItem(): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Translate,
|
||||
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.language),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.compo
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Notifications
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSet
|
||||
fun NotificationsItem(): SelectionItem {
|
||||
val context = LocalContext.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Notifications,
|
||||
leading = { Icon(Icons.Outlined.Notifications, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.notifications),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AppShortcut
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun AppShortcutsItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Filled.AppShortcut,
|
||||
leading = { Icon(Icons.Filled.AppShortcut, contentDescription = null) },
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = uiState.appSettings.isShortcutsEnabled,
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
|
||||
|
||||
@Composable
|
||||
fun BackgroundLocationDialog(show: Boolean, onDismiss: () -> Unit, onAttest: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
if (show) {
|
||||
val alwaysOnDescription = buildAnnotatedString {
|
||||
append(stringResource(R.string.background_location_message))
|
||||
append(" ")
|
||||
pushStringAnnotation(tag = "appSettings", annotation = "")
|
||||
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
|
||||
append(stringResource(id = R.string.app_settings))
|
||||
}
|
||||
pop()
|
||||
append(" ")
|
||||
append(stringResource(R.string.background_location_message2))
|
||||
append(".")
|
||||
}
|
||||
InfoDialog(
|
||||
onDismiss = { onDismiss() },
|
||||
onAttest = { onDismiss() },
|
||||
title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) },
|
||||
body = {
|
||||
ClickableText(
|
||||
text = alwaysOnDescription,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
),
|
||||
) {
|
||||
alwaysOnDescription
|
||||
.getStringAnnotations(tag = "appSettings", it, it)
|
||||
.firstOrNull()
|
||||
?.let { context.launchAppSettings() }
|
||||
}
|
||||
},
|
||||
confirmText = { Text(text = stringResource(R.string.okay)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Code
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun KernelModeItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Code,
|
||||
leading = { Icon(Icons.Outlined.Code, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.kernel),
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.VpnKeyOff
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -13,10 +14,10 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
|
||||
@Composable
|
||||
fun KillSwitchItem(): SelectionItem {
|
||||
fun killSwitchItem(): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.VpnKeyOff,
|
||||
leading = { Icon(Icons.Outlined.VpnKeyOff, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.kill_switch_options),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ViewHeadline
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun LocalLoggingItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.ViewHeadline,
|
||||
leading = { Icon(Icons.Outlined.ViewHeadline, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(stringResource(R.string.local_logging), SelectionLabelType.TITLE)
|
||||
},
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Pin
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -32,7 +33,7 @@ fun PinLockItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
}
|
||||
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Pin,
|
||||
leading = { Icon(Icons.Outlined.Pin, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.enable_app_lock),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ViewTimeline
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
fun ReadLogsItem(): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Filled.ViewTimeline,
|
||||
leading = { Icon(Icons.Filled.ViewTimeline, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(stringResource(R.string.read_logs), SelectionLabelType.TITLE)
|
||||
},
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Restore
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun RestartAtBootItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Restore,
|
||||
leading = { Icon(Icons.Outlined.Restore, contentDescription = null) },
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = uiState.appSettings.isRestoreOnBootEnabled,
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.VpnLock
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -14,9 +15,9 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
|
||||
@Composable
|
||||
fun AlwaysOnVpnItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
fun alwaysOnVpnItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.VpnLock,
|
||||
leading = { Icon(Icons.Outlined.VpnLock, contentDescription = null) },
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
enabled =
|
||||
+3
-2
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -13,10 +14,10 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
|
||||
@Composable
|
||||
fun AppearanceItem(): SelectionItem {
|
||||
fun appearanceItem(): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.AutoMirrored.Outlined.ViewQuilt,
|
||||
leading = { Icon(Icons.AutoMirrored.Outlined.ViewQuilt, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.appearance),
|
||||
+3
-1
@@ -11,7 +11,9 @@ import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components.LanTrafficItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components.VpnKillSwitchItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components.nativeKillSwitchItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@@ -26,7 +28,7 @@ fun KillSwitchScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
modifier = Modifier.fillMaxSize().padding(vertical = 24.dp).padding(horizontal = 12.dp),
|
||||
) {
|
||||
if (!isTv) {
|
||||
SurfaceSelectionGroupButton(items = listOf(NativeKillSwitchItem()))
|
||||
SurfaceSelectionGroupButton(items = listOf(nativeKillSwitchItem()))
|
||||
SectionDivider()
|
||||
}
|
||||
SurfaceSelectionGroupButton(
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Lan
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun LanTrafficItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Lan,
|
||||
leading = { Icon(Icons.Outlined.Lan, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.allow_lan_traffic),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.compo
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.VpnKey
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -14,7 +15,7 @@ import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
@Composable
|
||||
fun VpnKillSwitchItem(uiState: AppUiState, toggleVpnSwitch: () -> Unit): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.VpnKey,
|
||||
leading = { Icon(Icons.Outlined.VpnKey, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.vpn_kill_switch),
|
||||
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -13,10 +14,10 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
|
||||
|
||||
@Composable
|
||||
fun NativeKillSwitchItem(): SelectionItem {
|
||||
fun nativeKillSwitchItem(): SelectionItem {
|
||||
val context = LocalContext.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.AdminPanelSettings,
|
||||
leading = { Icon(Icons.Outlined.AdminPanelSettings, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.native_kill_switch),
|
||||
+41
-2
@@ -12,6 +12,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -72,8 +78,41 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(uiState.appUpdate?.version ?: "")
|
||||
Text(uiState.appUpdate?.releaseNotes ?: "")
|
||||
val annotatedString = buildAnnotatedString {
|
||||
append("${uiState.appUpdate?.version ?: ""}\n")
|
||||
// Add clickable text for second line
|
||||
withLink(
|
||||
link =
|
||||
LinkAnnotation.Clickable(
|
||||
tag = stringResource(id = R.string.release_notes),
|
||||
linkInteractionListener = {
|
||||
val version =
|
||||
if (BuildConfig.VERSION_NAME.contains("nightly")) {
|
||||
"nightly"
|
||||
} else {
|
||||
uiState.appUpdate
|
||||
?.version
|
||||
?.removePrefix("v")
|
||||
?.trim() ?: ""
|
||||
}
|
||||
val url = "${Constants.BASE_RELEASE_URL}$version".trim()
|
||||
context.openWebUrl(url)
|
||||
},
|
||||
styles =
|
||||
TextLinkStyles(
|
||||
style =
|
||||
SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
)
|
||||
),
|
||||
)
|
||||
) {
|
||||
append(stringResource(R.string.release_notes))
|
||||
}
|
||||
}
|
||||
|
||||
Text(text = annotatedString)
|
||||
if (uiState.isLoading) {
|
||||
LinearProgressIndicator(
|
||||
progress = { uiState.downloadProgress },
|
||||
|
||||
+29
-7
@@ -1,9 +1,12 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.Mail
|
||||
import androidx.compose.material.icons.outlined.Favorite
|
||||
import androidx.compose.material.icons.outlined.Mail
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
@@ -14,6 +17,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
@@ -26,7 +30,13 @@ fun ContactSupportOptions(context: android.content.Context) {
|
||||
addAll(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
leadingIcon = ImageVector.vectorResource(R.drawable.matrix),
|
||||
leading = {
|
||||
Icon(
|
||||
ImageVector.vectorResource(R.drawable.matrix),
|
||||
contentDescription = null,
|
||||
Modifier.size(iconSize),
|
||||
)
|
||||
},
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.join_matrix),
|
||||
@@ -41,7 +51,13 @@ fun ContactSupportOptions(context: android.content.Context) {
|
||||
onClick = { context.openWebUrl(context.getString(R.string.matrix_url)) },
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = ImageVector.vectorResource(R.drawable.telegram),
|
||||
leading = {
|
||||
Icon(
|
||||
ImageVector.vectorResource(R.drawable.telegram),
|
||||
contentDescription = null,
|
||||
Modifier.size(iconSize),
|
||||
)
|
||||
},
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.join_telegram),
|
||||
@@ -58,7 +74,13 @@ fun ContactSupportOptions(context: android.content.Context) {
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = ImageVector.vectorResource(R.drawable.github),
|
||||
leading = {
|
||||
Icon(
|
||||
ImageVector.vectorResource(R.drawable.github),
|
||||
contentDescription = null,
|
||||
Modifier.size(iconSize),
|
||||
)
|
||||
},
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.open_issue),
|
||||
@@ -73,7 +95,7 @@ fun ContactSupportOptions(context: android.content.Context) {
|
||||
onClick = { context.openWebUrl(context.getString(R.string.github_url)) },
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Filled.Mail,
|
||||
leading = { Icon(Icons.Outlined.Mail, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.email_description),
|
||||
@@ -88,7 +110,7 @@ fun ContactSupportOptions(context: android.content.Context) {
|
||||
if (BuildConfig.FLAVOR != Constants.GOOGLE_PLAY_FLAVOR) {
|
||||
add(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Filled.Favorite,
|
||||
leading = { Icon(Icons.Outlined.Favorite, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.donate),
|
||||
|
||||
+7
-3
@@ -4,6 +4,10 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Balance
|
||||
import androidx.compose.material.icons.filled.Book
|
||||
import androidx.compose.material.icons.filled.Policy
|
||||
import androidx.compose.material.icons.outlined.Balance
|
||||
import androidx.compose.material.icons.outlined.Book
|
||||
import androidx.compose.material.icons.outlined.Policy
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
@@ -24,7 +28,7 @@ fun GeneralSupportOptions(context: android.content.Context) {
|
||||
buildList {
|
||||
add(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Filled.Book,
|
||||
leading = { Icon(Icons.Outlined.Book, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.docs_description),
|
||||
@@ -41,7 +45,7 @@ fun GeneralSupportOptions(context: android.content.Context) {
|
||||
)
|
||||
add(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Filled.Policy,
|
||||
leading = { Icon(Icons.Outlined.Policy, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.privacy_policy),
|
||||
@@ -60,7 +64,7 @@ fun GeneralSupportOptions(context: android.content.Context) {
|
||||
)
|
||||
add(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Filled.Balance,
|
||||
leading = { Icon(Icons.Outlined.Balance, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.licenses),
|
||||
|
||||
+3
-1
@@ -3,7 +3,9 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CloudDownload
|
||||
import androidx.compose.material.icons.outlined.CloudDownload
|
||||
import androidx.compose.material.icons.rounded.CloudDownload
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
@@ -18,7 +20,7 @@ fun UpdateSection(onUpdateCheck: () -> Unit = {}) {
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Filled.CloudDownload,
|
||||
leading = { Icon(Icons.Outlined.CloudDownload, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.check_for_update),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.state
|
||||
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.networkmonitor.ConnectivityState
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.GeneralStateMapper
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
||||
@@ -16,5 +16,5 @@ data class AppUiState(
|
||||
val isAutoTunnelActive: Boolean = false,
|
||||
val appConfigurationChange: Boolean = false,
|
||||
val isAppLoaded: Boolean = false,
|
||||
val networkStatus: NetworkStatus? = null,
|
||||
val connectivityState: ConnectivityState? = null,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ val OffWhite = Color(0xFFF2F2F4)
|
||||
val CoolGray = Color(0xFF8D9D9F)
|
||||
val LightGrey = Color(0xFFECEDEF)
|
||||
val Aqua = Color(0xFF76BEBD)
|
||||
val Plantation = Color(0xFF264A49)
|
||||
val Plantation = Color(0xFF2E3538)
|
||||
val Shark = Color(0xFF21272A)
|
||||
val BalticSea = Color(0xFF1C1B1F)
|
||||
|
||||
|
||||
@@ -3,18 +3,17 @@ package com.zaneschepke.wireguardautotunnel.ui.theme
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.material.ripple.RippleAlpha
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
|
||||
private val DarkColorScheme =
|
||||
darkColorScheme(
|
||||
@@ -49,9 +48,11 @@ enum class Theme {
|
||||
DYNAMIC,
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composable () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
var isDark = isSystemInDarkTheme()
|
||||
val autoTheme = if (isDark) DarkColorScheme else LightColorScheme
|
||||
val colorScheme =
|
||||
@@ -105,5 +106,22 @@ fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composabl
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
|
||||
// Make hover/ripple more obvious on TV
|
||||
val rippleConfig =
|
||||
if (isTv) {
|
||||
RippleConfiguration(
|
||||
color = colorScheme.outline.copy(alpha = 0.12f),
|
||||
rippleAlpha =
|
||||
RippleAlpha(
|
||||
pressedAlpha = 0.7f,
|
||||
focusedAlpha = 0.6f,
|
||||
draggedAlpha = 0.9f,
|
||||
hoveredAlpha = 0.3f,
|
||||
),
|
||||
)
|
||||
} else null
|
||||
|
||||
CompositionLocalProvider(LocalRippleConfiguration provides rippleConfig) {
|
||||
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,4 +38,6 @@ object Constants {
|
||||
const val GOOGLE_PLAY_FLAVOR = "google"
|
||||
const val STANDALONE_FLAVOR = "standalone"
|
||||
const val RELEASE = "release"
|
||||
|
||||
const val BASE_RELEASE_URL = "https://github.com/wgtunnel/wgtunnel/releases/tag/"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,18 @@ sealed class StringValue {
|
||||
return when (this) {
|
||||
is Empty -> ""
|
||||
is DynamicString -> value
|
||||
is StringResource -> context?.getString(resId, *args).orEmpty()
|
||||
is StringResource -> {
|
||||
val stringArgs =
|
||||
args
|
||||
.map { arg ->
|
||||
when (arg) {
|
||||
is Int -> context?.getString(arg) ?: arg.toString()
|
||||
else -> arg
|
||||
}
|
||||
}
|
||||
.toTypedArray()
|
||||
context?.getString(resId, *stringArgs).orEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-8
@@ -176,14 +176,19 @@ fun Context.launchSettings(): Result<Unit> {
|
||||
}
|
||||
|
||||
fun Context.launchAppSettings() {
|
||||
kotlin.runCatching {
|
||||
val intent =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
kotlin
|
||||
.runCatching {
|
||||
val intent =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
.onFailure {
|
||||
val fallback = Intent(Settings.ACTION_SETTINGS)
|
||||
startActivity(fallback)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.requestTunnelTileServiceStateUpdate() {
|
||||
|
||||
+7
-53
@@ -1,64 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.ObsoleteCoroutinesApi
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.channels.ticker
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.selects.whileSelect
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Chunks based on a time or size threshold.
|
||||
*
|
||||
* Borrowed from this
|
||||
* [Stack Overflow question](https://stackoverflow.com/questions/51022533/kotlin-chunk-sequence-based-on-size-and-time).
|
||||
*/
|
||||
@OptIn(ObsoleteCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
||||
fun <T> ReceiveChannel<T>.chunked(scope: CoroutineScope, size: Int, time: Duration) =
|
||||
scope.produce<List<T>> {
|
||||
while (true) { // this loop goes over each chunk
|
||||
val chunk = ConcurrentLinkedQueue<T>() // current chunk
|
||||
val ticker = ticker(time.toMillis()) // time-limit for this chunk
|
||||
try {
|
||||
whileSelect {
|
||||
ticker.onReceive {
|
||||
false // done with chunk when timer ticks, takes priority over received
|
||||
// elements
|
||||
}
|
||||
this@chunked.onReceive {
|
||||
chunk += it
|
||||
chunk.size < size // continue whileSelect if chunk is not full
|
||||
}
|
||||
}
|
||||
} catch (e: ClosedReceiveChannelException) {
|
||||
Timber.e(e)
|
||||
return@produce
|
||||
} finally {
|
||||
ticker.cancel()
|
||||
if (chunk.isNotEmpty()) {
|
||||
send(chunk.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun <K, V> Flow<Map<K, V>>.distinctByKeys(): Flow<Map<K, V>> {
|
||||
return distinctUntilChanged { old, new -> old.keys == new.keys }
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
fun <T> CoroutineScope.asChannel(flow: Flow<T>): ReceiveChannel<T> = produce {
|
||||
flow.collect { value -> channel.send(value) }
|
||||
fun <T> Flow<T>.zipWithPrevious(): Flow<Pair<T?, T>> = flow {
|
||||
var previous: T? = null
|
||||
collect { current ->
|
||||
emit(previous to current)
|
||||
previous = current
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <R> StateFlow<AppUiState>.withFirstState(block: suspend (AppUiState) -> R): R {
|
||||
|
||||
@@ -24,3 +24,7 @@ typealias Packages = List<PackageInfo>
|
||||
fun <T> MutableList<T>.addAllUnique(elements: Collection<T>, comparator: (T, T) -> Boolean) {
|
||||
addAll(elements.filterNot { new -> this.any { existing -> comparator(existing, new) } })
|
||||
}
|
||||
|
||||
fun <T, R : Comparable<R>> List<T>.isSortedBy(selector: (T) -> R): Boolean {
|
||||
return zipWithNext().all { (a, b) -> selector(a) <= selector(b) }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.ViewModel
|
||||
@@ -9,8 +10,8 @@ import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.ConnectivityState
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
@@ -33,6 +34,7 @@ import com.zaneschepke.wireguardautotunnel.util.*
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.addAllUnique
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.withFirstState
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
@@ -46,6 +48,7 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.amnezia.awg.config.BadConfigException
|
||||
import org.amnezia.awg.config.Config
|
||||
import rikka.shizuku.Shizuku
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
@@ -62,7 +65,7 @@ constructor(
|
||||
private val logReader: LogReader,
|
||||
private val fileUtils: FileUtils,
|
||||
private val shortcutManager: ShortcutManager,
|
||||
networkMonitor: NetworkMonitor,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
) : ViewModel() {
|
||||
|
||||
private var logsJob: Job? = null
|
||||
@@ -75,27 +78,30 @@ constructor(
|
||||
private val _screenCallback = MutableStateFlow<(() -> Unit)?>(null)
|
||||
|
||||
private val _appViewState = MutableStateFlow(AppViewState())
|
||||
val appViewState = _appViewState.asStateFlow()
|
||||
val appViewState: StateFlow<AppViewState> = _appViewState.asStateFlow()
|
||||
|
||||
private val _uiEvent = MutableSharedFlow<UiEvent>()
|
||||
val uiEvent: SharedFlow<UiEvent> = _uiEvent.asSharedFlow()
|
||||
|
||||
private val _logs = MutableStateFlow<List<LogMessage>>(emptyList())
|
||||
val logs: StateFlow<List<LogMessage>> = _logs.asStateFlow()
|
||||
private val maxLogSize = Constants.MAX_LOG_SIZE
|
||||
|
||||
val uiState =
|
||||
val uiState: StateFlow<AppUiState> =
|
||||
combine(
|
||||
appDataRepository.settings.flow,
|
||||
appDataRepository.tunnels.flow,
|
||||
appDataRepository.appState.flow,
|
||||
tunnelManager.activeTunnels,
|
||||
serviceManager.autoTunnelService.map { it != null },
|
||||
networkMonitor.networkStatusFlow,
|
||||
networkMonitor.connectivityStateFlow,
|
||||
) { array ->
|
||||
val settings = array[0] as AppSettings
|
||||
val tunnels = array[1] as List<TunnelConf>
|
||||
val appState = array[2] as AppState
|
||||
val activeTunnels = array[3] as Map<TunnelConf, TunnelState>
|
||||
val autoTunnel = array[4] as Boolean
|
||||
val network = array[5] as NetworkStatus
|
||||
val network = array[5] as ConnectivityState
|
||||
|
||||
AppUiState(
|
||||
appSettings = settings,
|
||||
@@ -104,7 +110,7 @@ constructor(
|
||||
appState = appState,
|
||||
isAutoTunnelActive = autoTunnel,
|
||||
isAppLoaded = true,
|
||||
networkStatus = network,
|
||||
connectivityState = network,
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
@@ -120,11 +126,15 @@ constructor(
|
||||
handleKillSwitchChange(state.appSettings)
|
||||
initServicesFromSavedState(state)
|
||||
if (state.appState.isLocalLogsEnabled) logsJob = startCollectingLogs()
|
||||
handleTunnelErrors()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvent(event: AppEvent) =
|
||||
fun handleUiEvent(event: UiEvent): Job =
|
||||
viewModelScope.launch(mainDispatcher) { _uiEvent.emit(event) }
|
||||
|
||||
fun handleEvent(event: AppEvent): Job =
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
uiState.withFirstState { state ->
|
||||
when (event) {
|
||||
@@ -213,26 +223,46 @@ constructor(
|
||||
|
||||
is AppEvent.SetDetectionMethod ->
|
||||
handleSetDetectionMethod(event.detectionMethod, state.appSettings)
|
||||
is AppEvent.SaveAllConfigs -> saveAllTunnels(event.tunnels)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveAllTunnels(tunnels: List<TunnelConf>) {
|
||||
appDataRepository.tunnels.saveAll(tunnels)
|
||||
}
|
||||
|
||||
private suspend fun handleSetDetectionMethod(
|
||||
detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
|
||||
appSettings: AppSettings,
|
||||
) {
|
||||
if (detectionMethod == appSettings.wifiDetectionMethod) return
|
||||
when (detectionMethod) {
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.ROOT -> {
|
||||
val allowed = requestRoot()
|
||||
if (!allowed) return
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.ROOT -> if (!requestRoot()) return
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU -> {
|
||||
Shizuku.addRequestPermissionResultListener(
|
||||
Shizuku.OnRequestPermissionResultListener { requestCode: Int, grantResult: Int
|
||||
->
|
||||
if (grantResult != PERMISSION_GRANTED)
|
||||
return@OnRequestPermissionResultListener
|
||||
viewModelScope.launch {
|
||||
saveSettings(appSettings.copy(wifiDetectionMethod = detectionMethod))
|
||||
}
|
||||
}
|
||||
)
|
||||
try {
|
||||
if (Shizuku.checkSelfPermission() != PERMISSION_GRANTED)
|
||||
return Shizuku.requestPermission(123)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return handleShowMessage(
|
||||
StringValue.StringResource(R.string.shizuku_not_detected)
|
||||
)
|
||||
}
|
||||
}
|
||||
// TODO check if shizuku available
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU -> Unit
|
||||
else -> Unit
|
||||
}
|
||||
saveSettings(appSettings.copy(wifiDetectionMethod = detectionMethod))
|
||||
handleShowMessage(StringValue.StringResource(R.string.app_restart_required))
|
||||
}
|
||||
|
||||
private fun handleToggleSelectAllTunnels(tunnels: List<TunnelConf>) =
|
||||
@@ -285,9 +315,17 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
private fun handleTunnelErrors() =
|
||||
viewModelScope.launch { tunnelManager.errorEvents.collect { errorEvent -> } }
|
||||
viewModelScope.launch {
|
||||
tunnelManager.errorEvents.collect { errorEvent ->
|
||||
handleShowMessage(
|
||||
StringValue.StringResource(
|
||||
R.string.tunnel_error_template,
|
||||
errorEvent.second.toStringRes(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleAppReadyCheck(tunnels: List<TunnelConf>) {
|
||||
if (tunnels.size == appDataRepository.tunnels.count()) {
|
||||
|
||||
@@ -125,4 +125,6 @@ sealed class AppEvent {
|
||||
data class SetShowModal(val modalType: AppViewState.ModalType) : AppEvent()
|
||||
|
||||
data object ToggleSelectAllTunnels : AppEvent()
|
||||
|
||||
data class SaveAllConfigs(val tunnels: List<TunnelConf>) : AppEvent()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel.event
|
||||
|
||||
sealed class UiEvent {
|
||||
data object SortTunnels : UiEvent()
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -163,6 +163,24 @@
|
||||
<string name="learn_more">Zjistit více</string>
|
||||
<string name="stop">zastavit</string>
|
||||
<string name="server_ipv4">Překlad názvu hostitele IPv4</string>
|
||||
<string name="always_on_message2">ujistěte se, že je pro všechny ostatní aplikace vypnutá funkce trvalé připojení VPN, a zkuste to znovu</string>
|
||||
<string name="use_wildcards">Použít zástupné znaky(wildcards) pro názvy</string>
|
||||
<string name="multiple">Několik</string>
|
||||
<string name="enter_config_url">Zadejte adresu URL konfigurace</string>
|
||||
<string name="search">Hledat</string>
|
||||
<string name="join_matrix">Připojte se k Matrix komunitě</string>
|
||||
<string name="join_telegram">Připojte se k Telegram komunitě</string>
|
||||
<string name="post_down">Po deaktivaci</string>
|
||||
<string name="invalid_config_error">chyba_neplatné_konfigurace</string>
|
||||
<string name="error_download_failed">Nepodařilo se stáhnout konfiguraci</string>
|
||||
<string name="select">Vybrat</string>
|
||||
<string name="save">Uložit</string>
|
||||
<string name="dns_resolve_error">Chyba překladu dns</string>
|
||||
<string name="bio_update_required">Vyžadována aktualizace biometrického zabezpečení</string>
|
||||
<string name="export_logs">Exportovat uložené protokoly</string>
|
||||
<string name="add_tunnel">Přidat tunel</string>
|
||||
<string name="delete_logs">Smazat a vyčistit protokoly</string>
|
||||
<string name="dropdown">Rozbalovací nabídka</string>
|
||||
<string name="select_all">Vybrat vše</string>
|
||||
<string name="share">Sdílet</string>
|
||||
<string name="trusted_ssid_value_description">Odeslat SSID</string>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<string name="name">Nombre</string>
|
||||
<string name="always_on_vpn_support">Permitir VPN siempre-activada</string>
|
||||
<string name="location_services_not_detected">Servicios de Ubicación No Detectados</string>
|
||||
<string name="auto_tunneling">Túnel-automático</string>
|
||||
<string name="auto_tunneling">Túnel automático</string>
|
||||
<string name="vpn_on">VPN on</string>
|
||||
<string name="vpn_off">VPN off</string>
|
||||
<string name="create_import">Crear desde cero</string>
|
||||
@@ -68,7 +68,7 @@
|
||||
<string name="error_ssid_exists">SSID existente</string>
|
||||
<string name="error_root_denied">Shell root denegado</string>
|
||||
<string name="error_no_file_explorer">Explorador de archivos no instalado</string>
|
||||
<string name="auto_tunnel_title">Servicio de túnel-automático</string>
|
||||
<string name="auto_tunnel_title">Servicio de túnel automático</string>
|
||||
<string name="delete_tunnel">Eliminar túnel</string>
|
||||
<string name="delete_tunnel_message">¿Estás seguro de que quieres eliminar este túnel?</string>
|
||||
<string name="yes">Sí</string>
|
||||
@@ -103,7 +103,7 @@
|
||||
<string name="always_on_message">Se ha denegado el permiso de conexión VPN. Por favor, compruebe el</string>
|
||||
<string name="always_on_message2">para asegurarse de que el VPN Siempre encendido esté desactivada para todas las demás aplicaciones e inténtelo de nuevo</string>
|
||||
<string name="response_packet_magic_header">Encabezado del paquete de respuesta</string>
|
||||
<string name="junk_packet_maximum_size">Tamaño máximo del paquete basura</string>
|
||||
<string name="junk_packet_maximum_size">Tamaño máximo del paquete innecesario</string>
|
||||
<string name="init_packet_junk_size">Tamaño basura del paquete de inicialización</string>
|
||||
<string name="unsure_how">Si no estás seguro de cómo proceder</string>
|
||||
<string name="see_the">Ver la</string>
|
||||
@@ -152,7 +152,7 @@
|
||||
<string name="light">Claro</string>
|
||||
<string name="dark">Oscuro</string>
|
||||
<string name="use_root_shell_for_wifi">Utilizar el shell root para obtener el nombre del wifi</string>
|
||||
<string name="tunnel_running">Túnel funcionando</string>
|
||||
<string name="tunnel_running">Túnel conectado</string>
|
||||
<string name="monitoring_state_changes">Monitorizando cambios de estado</string>
|
||||
<string name="dynamic">Dinámico</string>
|
||||
<string name="language">Idioma</string>
|
||||
@@ -175,7 +175,7 @@
|
||||
<string name="splt_tunneling">Túnel dividido</string>
|
||||
<string name="pre_up">Pre up</string>
|
||||
<string name="post_up">Post up</string>
|
||||
<string name="pre_down">Pre down</string>
|
||||
<string name="pre_down">Antes de la desactivación</string>
|
||||
<string name="post_down">Post down</string>
|
||||
<string name="quick_actions">Acciones rápidas</string>
|
||||
<string name="remove_amnezia_compatibility">Eliminar compatibilidad con Amnezia</string>
|
||||
@@ -185,4 +185,70 @@
|
||||
<string name="server_ipv4">Resolución de host IPv4</string>
|
||||
<string name="prefer_ipv4">Preferir conexión IPv4</string>
|
||||
<string name="multiple">Múltiple</string>
|
||||
<string name="add_from_url">Añadir desde URL</string>
|
||||
<string name="save">Guardar</string>
|
||||
<string name="select">Seleccionar</string>
|
||||
<string name="join_telegram">Únete a la comunidad de Telegram</string>
|
||||
<string name="join_matrix">Únete a la comunidad de Matrix</string>
|
||||
<string name="share">Compartir</string>
|
||||
<string name="update_download_failed">Fallo al descargar actualización.</string>
|
||||
<string name="licenses">Licencias</string>
|
||||
<string name="checking_for_update">Comprobando actualización</string>
|
||||
<string name="select_all">Seleccionar todos</string>
|
||||
<string name="permission_required">Permiso requerido</string>
|
||||
<string name="active">Activo</string>
|
||||
<string name="copy">Copiar</string>
|
||||
<string name="bio_not_supported">Datos biométricos no admitidos</string>
|
||||
<string name="export_tunnels_wireguard">Exportar túneles como WireGuard</string>
|
||||
<string name="export_tunnels_amnezia">Exportar túneles como Amnezia</string>
|
||||
<string name="wifi_name_template">Activo: %1$s</string>
|
||||
<string name="add_tunnel">Añadir túnel</string>
|
||||
<string name="export_logs">Exportar registros almacenados</string>
|
||||
<string name="inactive">Inactivo</string>
|
||||
<string name="search">Buscar</string>
|
||||
<string name="bio_subtitle">Inicia sesión con tu credencial biométrica</string>
|
||||
<string name="kernel_name_error">error en el nombre del módulo del kernel</string>
|
||||
<string name="camera_permission_required">Permiso de cámara requerido</string>
|
||||
<string name="dns_resolve_error">error de resolución DNS</string>
|
||||
<string name="error_download_failed">Fallo al descargar configuración</string>
|
||||
<string name="enter_config_url">Introducir URL de configuración</string>
|
||||
<string name="bio_update_required">Actualización de seguridad biométrica requerida</string>
|
||||
<string name="delete_logs">Eliminar y limpiar registros</string>
|
||||
<string name="app_permission_title">Puente de Control WG Tunnel</string>
|
||||
<string name="dropdown">Menú desplegable</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="delete">Eliminar</string>
|
||||
<string name="export_failed">Exportación fallida</string>
|
||||
<string name="app_permission_description">Controlar túneles y funcionalidades de auto-túnel</string>
|
||||
<string name="tunnel_error_template">Túnel fallido con: %1$s</string>
|
||||
<string name="invalid_config_error">invalid_config_error</string>
|
||||
<string name="remote_key_template">Clave: %1$s</string>
|
||||
<string name="config_error">Error de configuración</string>
|
||||
<string name="auth_error">error no autorizado</string>
|
||||
<string name="service_running_error">Error de servicio no en ejecución</string>
|
||||
<string name="status">Estado</string>
|
||||
<string name="bio_auth_title">Autenticación biométrica</string>
|
||||
<string name="version_template">Versión: %1$s</string>
|
||||
<string name="check_for_update">Comprobar actualización</string>
|
||||
<string name="update_check_failed">Fallo al comprobar la actualización.</string>
|
||||
<string name="security_template">Seguridad: %1$s</string>
|
||||
<string name="flavor_template">Sabor: %1$s</string>
|
||||
<string name="bio_not_created">Datos biométricos no creados</string>
|
||||
<string name="tunnel_starting">Iniciando túnel</string>
|
||||
<string name="enable_remote_app_control">Activar control remoto de la app</string>
|
||||
<string name="export_success">Éxito al exportar</string>
|
||||
<string name="download">Descargar</string>
|
||||
<string name="latest_installed">Ya se está ejecutando la última versión.</string>
|
||||
<string name="update_available">¡Actualización disponible!</string>
|
||||
<string name="download_and_install">Descargar e instalar</string>
|
||||
<string name="install_updated_permission">Esta app necesita permiso para instalar actualizaciónes.</string>
|
||||
<string name="allow">Permitir</string>
|
||||
<string name="update_check_unsupported">Comprobación de actualización no permitoda en esta compilación.</string>
|
||||
<string name="darker">Más oscuro</string>
|
||||
<string name="amoled">AMOLED</string>
|
||||
<string name="show_qr">Mostrar QR</string>
|
||||
<string name="amnezia">Amnezia</string>
|
||||
<string name="wireguard">WireGuard</string>
|
||||
<string name="done">Hecho</string>
|
||||
<string name="nothing_here_yet">¡No hay nada aquí de momento!</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="error_file_extension">See pole .conf või .zip fail</string>
|
||||
<string name="vpn_channel_name">VPN-i teavituskanal</string>
|
||||
<string name="turn_off_tunnel">See toiming eeldab, et tunnel pole töös</string>
|
||||
<string name="prominent_background_location_message">See funktsionaalsus eeldab, et rakendusel on õigus asukoha ja WiFi SSID tuvastamiseks taustal, seda ka siis, kui rakendus on suletud. Lisateavet leiad Privaatsusreeglite lehelt, mille leiad Kasutajatoe vaatest.</string>
|
||||
<string name="no_tunnels">Ühtegi tunnelit pole veel lisatud!</string>
|
||||
<string name="tunnels">Tunnelid</string>
|
||||
<string name="tunnel_mobile_data">Loo tunnel mobiilse andmesidega</string>
|
||||
<string name="privacy_policy">Vaata privaatsusreegleid</string>
|
||||
<string name="okay">Sobib</string>
|
||||
<string name="tunnel_on_ethernet">Loo tunnel kohtvõrgus</string>
|
||||
<string name="add_tunnels_text">Lisa conf- või zip-failist</string>
|
||||
<string name="open_file">Ava fail</string>
|
||||
<string name="add_from_qr">Lisa QR-koodist</string>
|
||||
<string name="qr_scan">Skaneeri QR-koodi</string>
|
||||
<string name="tunnel_name">Tunneli nimi</string>
|
||||
<string name="exclude">Välista</string>
|
||||
<string name="include">Kaasa</string>
|
||||
<string name="config_changes_saved">Seadistuse muudatused on salvestatud.</string>
|
||||
<string name="public_key">Avalik võti</string>
|
||||
<string name="addresses">Aadressid</string>
|
||||
<string name="dns_servers">Nimeserverid</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="app_permission_description">Halda tunneleid ja tunnelite automaatse käivitamise seadistusi.</string>
|
||||
<string name="thank_you">Tänud, et kasutad rakendust WG Tunnel!</string>
|
||||
<string name="allowed_ips">Lubatud IP-aadressid</string>
|
||||
<string name="endpoint">Otspunkt</string>
|
||||
<string name="name">Nimi</string>
|
||||
<string name="always_on_vpn_support">Luba, et VPN on alati sisse lülitatud</string>
|
||||
<string name="location_services_not_detected">Asukohateenused pole tuvastatavad</string>
|
||||
<string name="auto_tunneling">Automaatne tunneldus</string>
|
||||
<string name="vpn_on">VPN on kasutusel</string>
|
||||
<string name="vpn_off">VPN pole kasutusel</string>
|
||||
<string name="create_import">Loo nullist</string>
|
||||
<string name="turn_on_tunnel">Toiming eeldab aktiivse tunneli olemasolu</string>
|
||||
<string name="licenses">Litsentsid</string>
|
||||
<string name="update_check_unsupported">Selle rakenduse versiooni puhul pole uuenduste kontrollimine toetatud.</string>
|
||||
<string name="darker">Tumedam kujundus</string>
|
||||
<string name="amoled">AMOLED</string>
|
||||
<string name="show_qr">Näita QR-koodi</string>
|
||||
<string name="amnezia">Amnezia</string>
|
||||
<string name="wireguard">WireGuard</string>
|
||||
<string name="done">Valmis</string>
|
||||
<string name="download">Laadi alla</string>
|
||||
<string name="check_for_update">Kontrolli rakenduse uuendusi</string>
|
||||
<string name="update_check_failed">Uuenduse kontrollimine ei õnnestunud.</string>
|
||||
<string name="checking_for_update">Kontrollin uuendusi</string>
|
||||
<string name="latest_installed">Sa juba kasutad viimast versiooni.</string>
|
||||
<string name="update_download_failed">Uuenduse allalaadimine ei õnnestunud.</string>
|
||||
<string name="update_available">Uus versioon on saadaval!</string>
|
||||
<string name="download_and_install">Laadi alla ja paigalda</string>
|
||||
<string name="permission_required">Õigused on vajalikud</string>
|
||||
<string name="install_updated_permission">See rakendus vajab uuenduse paigaldamiseks õigusi.</string>
|
||||
<string name="allow">Luba</string>
|
||||
<string name="nothing_here_yet">Siin pole veel midagi!</string>
|
||||
<string name="share">Jaga</string>
|
||||
<string name="select_all">Vali kõik</string>
|
||||
<string name="export_success">Eksportimine õnnestus</string>
|
||||
<string name="inactive">Pole aktiivne</string>
|
||||
<string name="active">Aktiivne</string>
|
||||
<string name="status">Olek</string>
|
||||
<string name="bio_auth_title">Biomeetriline autentimine</string>
|
||||
<string name="bio_subtitle">Logi sisse kasutades biomeetrilist autentimist</string>
|
||||
<string name="bio_not_supported">Biomeetriline tuvastamine pole toetatud</string>
|
||||
<string name="bio_not_created">Biomeetriline tuvastamine pole seadistatud</string>
|
||||
<string name="bio_update_required">Vajalik on biomeetrilise tuvastamise turvauuendus</string>
|
||||
<string name="tunnel_starting">Tunnel käivitub</string>
|
||||
<string name="enable_remote_app_control">Luba rakenduse kaugjuhtimine</string>
|
||||
<string name="add_from_url">Lisa võrguaadressilt</string>
|
||||
<string name="enter_config_url">Sisesta seadistuse võrguaadress</string>
|
||||
<string name="error_download_failed">Seadistuse allalaadimine ei õnnestunud</string>
|
||||
<string name="save">Salvesta</string>
|
||||
<string name="search">Otsi</string>
|
||||
<string name="select">Vali</string>
|
||||
<string name="join_telegram">Liitu kogukonnaga Telegramis</string>
|
||||
<string name="join_matrix">Liitu kogukonnaga Matrixis</string>
|
||||
<string name="add_tunnel">Lisa tunnel</string>
|
||||
<string name="export_logs">Ekspordi salvestatud logid</string>
|
||||
<string name="delete_logs">Kustuta ja eemalda logid</string>
|
||||
<string name="copy">Kopeeri</string>
|
||||
<string name="info">Teave</string>
|
||||
<string name="export_tunnels_amnezia">Ekspordi tunnelid Amnezia jaoks</string>
|
||||
<string name="export_tunnels_wireguard">Ekspordi tunnelid WireGuardi jaoks</string>
|
||||
<string name="advanced_settings">Täiendavad seadistused</string>
|
||||
<string name="show_scripts">Näita skripte</string>
|
||||
<string name="pre_up">Käivituseelne</string>
|
||||
<string name="post_up">Käivitusjärgne</string>
|
||||
<string name="pre_down">Sulgemiseelne</string>
|
||||
<string name="post_down">Sulgemijärgne</string>
|
||||
<string name="hide_scripts">Peida skriptid</string>
|
||||
<string name="enable_amnezia_compatibility">Lisa ühilduvus Amnezia teenustega</string>
|
||||
<string name="remove_amnezia_compatibility">Eemalda ühilduvus Amnezia teenustega</string>
|
||||
<string name="show_amnezia_properties">Näita Amnezia seadistusi</string>
|
||||
<string name="hide_amnezia_properties">Peida Amnezia seadistused</string>
|
||||
<string name="donate">Toeta projekti rahaliselt</string>
|
||||
<string name="local_logging">Kohalik logimine</string>
|
||||
<string name="enable_local_logging">Logi andmed nutiseadmes</string>
|
||||
<string name="add_from_clipboard">Lisa lõikelaualt</string>
|
||||
<string name="appearance">Välimus</string>
|
||||
<string name="notifications">Teavitused</string>
|
||||
<string name="automatic">Automaatne</string>
|
||||
<string name="light">Hele kujundus</string>
|
||||
<string name="dark">Tume kujundus</string>
|
||||
<string name="dynamic">Dünaamiline kujundus</string>
|
||||
<string name="language">Keel</string>
|
||||
<string name="display_theme">Kujundus</string>
|
||||
<string name="interface_">Võrguliides</string>
|
||||
<string name="private_key">Privaatvõti</string>
|
||||
<string name="copy_public_key">Kopeeri avalik võti</string>
|
||||
<string name="base64_key">base64-kodeeringus võti</string>
|
||||
<string name="comma_separated_list">komadega eraldatud loend</string>
|
||||
<string name="listen_port">Kuulatav port</string>
|
||||
<string name="random">(juhuslik)</string>
|
||||
<string name="optional">(valikuline)</string>
|
||||
<string name="preshared_key">Eeljagatud võti</string>
|
||||
<string name="seconds">sekundit</string>
|
||||
<string name="persistent_keepalive">Pidev elumärksõnum</string>
|
||||
<string name="cancel">Katkesta</string>
|
||||
<string name="error_authentication_failed">Autentimine ei õnnestunud</string>
|
||||
<string name="exclude_lan">Välista kohtvõrgud</string>
|
||||
<string name="include_lan">Kaasa kohtvõrgud</string>
|
||||
<string name="dns_resolve_error">nimelahenduse viga</string>
|
||||
<string name="peer">Partner</string>
|
||||
<string name="add_peer">Lisa partner</string>
|
||||
<string name="default_ping_ip">(valikuline, vaikimisi partneri otspunkt)</string>
|
||||
<string name="rotate_keys">Vaheta võtmeid</string>
|
||||
<string name="delete_tunnel">Kustuta tunnel</string>
|
||||
<string name="delete_tunnel_message">Kas sa oled kindel, et soovid selle tunneli kustutada?</string>
|
||||
<string name="yes">Jah</string>
|
||||
<string name="all">kõik</string>
|
||||
<string name="no_email_detected">E-posti rakendust ei õnnestu tuvastada</string>
|
||||
<string name="no_browser_detected">Veebibrauserit ei õnnestu tuvastada</string>
|
||||
<string name="open_issue">Alusta veateate koostamist</string>
|
||||
<string name="read_logs">Loe logisid</string>
|
||||
<string name="auto">(automaatne)</string>
|
||||
<string name="incorrect_pin">PIN-kood pole õige</string>
|
||||
<string name="pin_created">PIN-koodi loomine õnnestus</string>
|
||||
<string name="enter_pin">Sisesta oma PIN-kood</string>
|
||||
<string name="create_pin">Loo PIN-kood</string>
|
||||
<string name="edit_tunnel">Muuda tunnelit</string>
|
||||
<string name="version">Versioon</string>
|
||||
<string name="settings">Seadistused</string>
|
||||
<string name="support">Kasutajatugi</string>
|
||||
<string name="unknown_error">Tekkis tundmatu viga</string>
|
||||
<string name="tunnel_on_wifi">Loo tunnel ebausaldusväärses WiFi võrgus</string>
|
||||
<string name="email_subject">WG Tunneli kasutajatugi</string>
|
||||
<string name="email_chooser">Saada e-kiri…</string>
|
||||
<string name="docs_description">Loe dokumentatsiooni</string>
|
||||
<string name="email_description">Saada mulle e-kiri</string>
|
||||
<string name="use_kernel">Kasuta kernelimoodulit</string>
|
||||
<string name="error_ssid_exists">SSID on juba olemas</string>
|
||||
<string name="error_root_denied">Juurkasutaja õigustes kest on keelatud</string>
|
||||
<string name="error_no_file_explorer">Failihaldurit pole paigaldatud</string>
|
||||
<string name="set_primary_tunnel">Määra põhiliseks tunneliks</string>
|
||||
<string name="skip">Jäta vahele</string>
|
||||
<string name="export_failed">Eksportimine ei õnnestunud</string>
|
||||
<string name="tunnel_error_template">Viga tunneli töös: %1$s</string>
|
||||
<string name="wifi_name_template">Aktiivne: %1$s</string>
|
||||
<string name="remote_key_template">Võti: %1$s</string>
|
||||
<string name="version_template">Versioon: %1$s</string>
|
||||
<string name="security_template">Turvalisus: %1$s</string>
|
||||
<string name="flavor_template">Levitusviis: %1$s</string>
|
||||
<string name="config_error">seadistusviga</string>
|
||||
<string name="invalid_config_error">vigane_seadistus_viga</string>
|
||||
<string name="kernel_name_error">viga kerneli mooduli nimes</string>
|
||||
<string name="auth_error">viga autentimisel</string>
|
||||
<string name="service_running_error">viga, kus teenus ei toimi</string>
|
||||
<string name="unsure_how">kui sa ei tea, mida järgmiseks teha</string>
|
||||
<string name="see_the">Vaata</string>
|
||||
<string name="getting_started_guide">esimeste toimingute juhendit</string>
|
||||
<string name="restart_at_boot">Käivita alglaadimisel uuesti</string>
|
||||
<string name="vpn_denied_dialog_title">Õigused on puudu</string>
|
||||
<string name="set_custom_ping_internal">Pingi välp (sekundites)</string>
|
||||
<string name="optional_default">"valikuline, vaikimisi: "</string>
|
||||
<string name="never">mitte kunagi</string>
|
||||
<string name="sec">sek</string>
|
||||
<string name="handshake">kätlus</string>
|
||||
<string name="logs">Logid</string>
|
||||
<string name="kill_switch">Kiirpeatamine</string>
|
||||
<string name="trusted_wifi_names">Usaldusväärsete WiFi-võrkude nimed</string>
|
||||
<string name="add_wifi_name">Lisa WiFi võrgunimi</string>
|
||||
<string name="primary_tunnel">Põhiline tunnel</string>
|
||||
<string name="app_settings">rakenduse seadistused</string>
|
||||
<string name="background_location_message2">tagamaks, et need õigused on lubatud</string>
|
||||
<string name="root_accepted">Juurkasutaja kest on lubatud</string>
|
||||
<string name="set_custom_ping_ip">Sisesta muu ip-aadress</string>
|
||||
<string name="learn_more">Lisateave</string>
|
||||
<string name="monitoring_state_changes">Jälgin oleku muudatusi</string>
|
||||
<string name="tunnel_running">Tunnel töötab</string>
|
||||
<string name="tunnel_specific_settings">Tunnelikohased seadistused</string>
|
||||
<string name="kernel_not_supported">Kernel pole toetatud</string>
|
||||
<string name="start_auto">Käivita automaatne tunneldus</string>
|
||||
<string name="stop_auto">Peata automaatne tunneldus</string>
|
||||
<string name="quick_actions">Kiirtoimingud</string>
|
||||
<string name="tunnel_control">Tunneli juhtimine</string>
|
||||
<string name="auto_tunnel">Automaatne tunneldus</string>
|
||||
<string name="delete">Kustuta</string>
|
||||
<string name="camera_permission_required">Vajalik on õigus kasutada kaamerat</string>
|
||||
<string name="dropdown">Rippmenüü</string>
|
||||
<string name="splt_tunneling">Jagatud tunneldus</string>
|
||||
<string name="stop">peata</string>
|
||||
<string name="stop_on_no_internet">Peata internetiühenduse puudumisel</string>
|
||||
<string name="stop_on_internet_loss">Peata tunnel internetiühenduse kadumisel</string>
|
||||
<string name="enable_app_lock">Kasuta rakenduse lukustust</string>
|
||||
<string name="restart_on_ping">Pingimise mittetoimimisel käivita uuesti (beeta)</string>
|
||||
<string name="mobile_data_tunnel">Määra mobiilse andmeside tunneliks</string>
|
||||
<string name="use_tunnel_on_wifi_name">Kasuta tunnelit WiFi nime puhul</string>
|
||||
<string name="kernel">Tuum/Kernel</string>
|
||||
<string name="error_file_format">Vigane tunneli seadistuste vorming</string>
|
||||
<string name="mobile_tunnel">Mobiilside andmetunnel</string>
|
||||
<string name="launch_app_settings">Käivita rakenduse seadistused</string>
|
||||
<string name="use_wildcards">Kasuta nimedes metamärke</string>
|
||||
<string name="wildcards_active">Metamärgid on kasutusel</string>
|
||||
<string name="prefer_ipv4">Eelista IPv4 ühendust</string>
|
||||
<string name="server_ipv4">IPv4 hostinime nimelahendus</string>
|
||||
<string name="junk_packet_count">Rämpspakettide arv</string>
|
||||
<string name="junk_packet_minimum_size">Rämpspaketi miinimumsuurus</string>
|
||||
<string name="junk_packet_maximum_size">Rämpspaketi maksimumsuurus</string>
|
||||
<string name="init_packet_junk_size">Alustava paketi rämpsuosa suurus</string>
|
||||
<string name="response_packet_junk_size">Vastuspaketi rämpsuosa suurus</string>
|
||||
<string name="init_packet_magic_header">Alustava paketi päise kohandatud osa</string>
|
||||
<string name="response_packet_magic_header">Vastuspaketi päise kohandatud osa</string>
|
||||
<string name="transport_packet_magic_header">Transpordipaketi päise kohandatud osa</string>
|
||||
<string name="underload_packet_magic_header">Väikese koormusega paketi päise kohandatud osa</string>
|
||||
<string name="vpn_settings">VPN-i süsteemsed seadistused</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_permission_title">پل کنترل تونل وایرگارد</string>
|
||||
<string name="privacy_policy">مشاهده سیاست حفظ حریم خصوصی</string>
|
||||
<string name="vpn_channel_name">کانال اطلاع رسانی VPN</string>
|
||||
<string name="error_file_extension">فایل .conf یا .zip نیست.</string>
|
||||
<string name="turn_off_tunnel">اقدام نیاز به تونل زنی دارد</string>
|
||||
<string name="no_tunnels">هنوز تونلی اضافه نشده!</string>
|
||||
<string name="tunnels">تونلها</string>
|
||||
<string name="tunnel_mobile_data">تونل روی داده تلفن همراه</string>
|
||||
<string name="okay">باشه</string>
|
||||
<string name="prominent_background_location_title">افشای موقعیت مکانی پسزمینه</string>
|
||||
<string name="thank_you">از استفاده شما از تونل WG متشکریم!</string>
|
||||
<string name="trusted_ssid_value_description">ارسال SSID</string>
|
||||
<string name="add_tunnels_text">از فایل یا زیپ اضافه کنید</string>
|
||||
<string name="app_permission_description">تونلهای کنترل و ویژگیهای تونل خودکار.</string>
|
||||
<string name="tunnel_on_ethernet">تونل روی اترنت</string>
|
||||
<string name="prominent_background_location_message">این ویژگی برای فعال کردن نظارت بر SSID وایفای، حتی در زمان بسته بودن برنامه، به مجوز موقعیت مکانی در پسزمینه نیاز دارد. برای جزئیات بیشتر، لطفاً به سیاست حفظ حریم خصوصی که در صفحه پشتیبانی لینک شده است، مراجعه کنید.</string>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
</resources>
|
||||
@@ -115,4 +115,16 @@
|
||||
<string name="sec">sek</string>
|
||||
<string name="read_logs">Lue lokitiedot</string>
|
||||
<string name="mobile_tunnel">Mobiilidatatunneli</string>
|
||||
<string name="restart_at_boot">Käynnistä laitteen käynnistyksen yhteydessä</string>
|
||||
<string name="always_on_message2">varmistaaksesi, että Aina päällä oleva VPN on kytketty pois päältä kaikkien muiden sovellusten osalta ja yritä uudelleen</string>
|
||||
<string name="background_location_message">Tätä toimintoa varten tulee sijainnin käyttölupa olla aina sallittuna ja/tai tarkka sijainti käytössä. Katso</string>
|
||||
<string name="app_settings">sovelluksen asetukset</string>
|
||||
<string name="root_accepted">Root hyväksytty</string>
|
||||
<string name="set_custom_ping_ip">Määrittele pingin ip-osoite</string>
|
||||
<string name="set_custom_ping_internal">Pingin aikaväli (sek)</string>
|
||||
<string name="show_amnezia_properties">Näytä Amnezia-asetukset</string>
|
||||
<string name="launch_app_settings">Käynnistä sovelluksen asetukset</string>
|
||||
<string name="use_wildcards">Käytä jokerimerkkiä nimissä</string>
|
||||
<string name="wildcards_active">Jokerimerkit aktivoitu</string>
|
||||
<string name="tunnel_running">Tunneli käytössä</string>
|
||||
</resources>
|
||||
|
||||
@@ -187,4 +187,7 @@
|
||||
<string name="copy">Copier</string>
|
||||
<string name="info">Informations</string>
|
||||
<string name="prefer_ipv4">Préférer une connexion IPv4</string>
|
||||
<string name="allow">Autoriser</string>
|
||||
<string name="app_permission_title">Pont de contrôle du tunnel WG</string>
|
||||
<string name="app_permission_description">Contrôler les tunnels et les fonctions automatiques des tunnels.</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="app_permission_description">Alagutak és automatikus alagút funkciók vezérlése.</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="app_permission_description">Kontroller tunneler og auto-tunnel egenskaper.</string>
|
||||
<string name="vpn_channel_name">VPN varslingskanal</string>
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user