mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 619e3c1cde | |||
| 77f8a8215b | |||
| 8772036dd7 | |||
| 63625ccbd7 | |||
| 9ac7ae77b3 | |||
| e062fbb34d | |||
| 16d5586433 | |||
| 48a3ad64f4 | |||
| e5796d641d | |||
| daf5eebdd2 | |||
| 4c725491f4 | |||
| 7529c11172 | |||
| 83f530df42 | |||
| 8083ab9526 | |||
| 7d1312da0f | |||
| d4dbc43c70 | |||
| 294f2624c7 | |||
| 0603cb2fdd | |||
| 48ddbcbb0e | |||
| e6c3e3f5b3 | |||
| 0d75699b40 | |||
| 5c98aab9e0 | |||
| a1e3489ba2 | |||
| bcd19b5494 | |||
| 160a6ca84d | |||
| aaf7ebd326 | |||
| b8c75a45e4 | |||
| ac17a09e19 | |||
| c51a7ee393 | |||
| c534516e33 | |||
| 9c999cc62c | |||
| cc3c865211 | |||
| 8648a67fdc | |||
| 9ee1fa69ed | |||
| 379ffdcbbf | |||
| 6e3c1324b2 | |||
| 660bea0104 | |||
| 2b8610fa8a | |||
| 944034ac74 | |||
| 9f394aeffb | |||
| 6c3c6891eb | |||
| af1848f12d | |||
| 96cffdfa7d | |||
| afebd975ea | |||
| 588a2a18bd | |||
| 221b38a119 | |||
| 0008d8b9bb | |||
| 9f85638b9a | |||
| fe54c9cd0e | |||
| 554499f9de | |||
| 12c9b52653 | |||
| 03712a6c1d | |||
| 5f03a97fcc | |||
| 6788b05fa0 | |||
| 9494853dee | |||
| 01695c3286 | |||
| bb6e45ed92 | |||
| 63e257f419 | |||
| e145cd95e1 | |||
| 7e790acbfe | |||
| 334aaa1c2b | |||
| 6201671dd0 | |||
| 517d90c3bf | |||
| d78443e7fa | |||
| 40d0466c14 | |||
| 5220c1a10c | |||
| 0e4e421628 | |||
| abdbf74755 | |||
| 5bc49eec50 | |||
| c7040b8081 | |||
| 26ecfec3fc | |||
| 5408cf3954 | |||
| 22c4a303fc |
@@ -70,16 +70,16 @@ jobs:
|
||||
outputs:
|
||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up JDK 17
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
|
||||
@@ -72,15 +72,15 @@ jobs:
|
||||
outputs:
|
||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Set up JDK 17
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
cache: gradle
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
@@ -112,6 +112,9 @@ jobs:
|
||||
./gradlew :app:assemble${flavor^}Debug --stacktrace
|
||||
;;
|
||||
esac
|
||||
env:
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
||||
- name: Get release apk path
|
||||
id: apk-path
|
||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
has_new_commits: ${{ steps.check.outputs.new_commits }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Check for new commits
|
||||
id: check
|
||||
env:
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
name: on-pr
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
format_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 17
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Verify Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@v6
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Run ktfmt
|
||||
run: ./gradlew ktfmtCheck
|
||||
run: ./gradlew ktfmtCheck
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
name: publish-github
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
||||
- name: Install system dependencies
|
||||
@@ -187,57 +187,61 @@ jobs:
|
||||
repository: wgtunnel/fdroid
|
||||
event-type: fdroid-update
|
||||
|
||||
build-google-aab:
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'push' ||
|
||||
inputs.track != 'none'
|
||||
}}
|
||||
uses: ./.github/workflows/build-aab.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
build_type: release
|
||||
flavor: google
|
||||
|
||||
publish-play:
|
||||
if: ${{ github.event_name == 'push' || inputs.track != 'none' }}
|
||||
name: Publish to Google Play
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
KEY_STORE_FILE: 'android_keystore.jks'
|
||||
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
||||
|
||||
needs: build-google-aab
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
# Here we need to decode keystore.jks from base64 string and place it
|
||||
# in the folder specified in the release signing configuration
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v2.0
|
||||
- name: Download AAB artifact
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
fileName: ${{ env.KEY_STORE_FILE }}
|
||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||
encodedString: ${{ secrets.KEYSTORE }}
|
||||
name: google-play-aab
|
||||
path: ${{ github.workspace }}/aab
|
||||
|
||||
# create keystore path for gradle to read
|
||||
- name: Create keystore path env var
|
||||
- name: Find exact AAB file path
|
||||
id: find-aab
|
||||
run: |
|
||||
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
|
||||
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
|
||||
AAB_PATH=$(find "${{ github.workspace }}/aab" -name "*.aab" -type f | head -1)
|
||||
if [ -z "$AAB_PATH" ]; then
|
||||
echo "ERROR: No .aab file found after download!"
|
||||
find "${{ github.workspace }}/aab" -type f
|
||||
exit 1
|
||||
fi
|
||||
echo "Found AAB: $AAB_PATH"
|
||||
echo "aab_path=$AAB_PATH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create service_account.json
|
||||
id: createServiceAccount
|
||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||
|
||||
- name: Deploy with fastlane
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||
ruby-version: '3.4'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Distribute app to Prod track 🚀
|
||||
- name: Upload to Google Play
|
||||
run: |
|
||||
track=${{ github.event_name == 'push' && 'production' || inputs.track }}
|
||||
(cd ${{ github.workspace }} && bundle install && bundle exec fastlane $track --verbose)
|
||||
bundle exec fastlane run upload_to_play_store \
|
||||
track:"$track" \
|
||||
aab:"${{ steps.find-aab.outputs.aab_path }}" \
|
||||
json_key:"service_account.json" \
|
||||
package_name:"com.zaneschepke.wireguardautotunnel" \
|
||||
skip_upload_apk:true
|
||||
@@ -1,3 +1,4 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "fastlane"
|
||||
gem "fastlane"
|
||||
gem "multi_json"
|
||||
@@ -49,8 +49,8 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
||||
|
||||
## About
|
||||
|
||||
WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired by the official WireGuard Android app. It fills gaps in the official client by adding advanced features like auto-tunneling (on-demand VPN activation), while seamlessly supporting both protocols across app modes—including Kernel (for direct WireGuard kernel integration; AmneziaWG not supported), VPN (standard system-level tunneling), Lockdown (a custom kill switch for leak prevention), and Proxy (built-in HTTP/SOCKS5 forwarding)—for enhanced privacy, censorship resistance, and flexibility.
|
||||
|
||||
WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired by the official WireGuard Android app. It fills gaps in the official client by adding advanced features like auto-tunneling, AmneziaWG support, different app modes like **Lockdown** (a custom kill switch for leak prevention), and **Local Proxy** (expose a tunnel over a local SOCKS5/HTTP proxy server) for enhanced privacy, censorship resistance, and flexibility.
|
||||
|
||||
</div>
|
||||
|
||||
<div style="text-align: left;">
|
||||
@@ -67,21 +67,18 @@ WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired
|
||||
|
||||
## Features
|
||||
|
||||
- **Tunnel Import Methods**: Easily add tunnels using .conf files, ZIP archives, manual entry, or QR code scanning.
|
||||
- **Auto-Tunneling**: Automatically activate tunnels based on Wi-Fi SSID, Ethernet connections, or mobile data networks.
|
||||
- **Split Tunneling**: Flexible support for routing specific apps or traffic through the VPN.
|
||||
- **WireGuard Modes**: Full compatibility with WireGuard in both kernel and userspace implementations.
|
||||
- **AmneziaWG Integration**: Userspace mode for AmneziaWG, providing robust censorship evasion.
|
||||
- **Always-On VPN**: Ensures continuous protection with Android's Always-On VPN feature.
|
||||
- **Quick Controls**: Quick Settings tile and home screen shortcuts for easy VPN toggling.
|
||||
- **Automation Support**: Intent-based automation for controlling tunnels.
|
||||
- **Auto-Restore**: Seamlessly restores auto-tunneling and active tunnels after device restarts or app updates.
|
||||
- **Proxying Options**: Built-in HTTP and SOCKS5 proxy support within tunnels.
|
||||
- **Lockdown Mode**: Custom kill switch for maximum leak prevention and security.
|
||||
- **Dynamic DNS Handling**: Detects and updates DNS changes without tunnel restarts.
|
||||
- **Monitoring Tools**: Advanced tunnel monitoring features for tunnel performance monitoring.
|
||||
- **Android TV Support**: Android TV support for secure streaming and browsing.
|
||||
- **Advanced DNS**: DNS over HTTPS support for tunnel endpoint resolutions.
|
||||
- **Auto-Tunneling:** Automatically activate tunnels based on your device's active network details.
|
||||
- **Deferred Endpoint Bootstrapping:** Safely resolves endpoints and updates peers after the tunnel is up for better reliability and leak protection on startup.
|
||||
- **Handshake Monitoring:** Real-time handshake monitoring for instant tunnel health feedback.
|
||||
- **AmneziaWG Support:** Full support for AmneziaWG 2.0, providing robust censorship protection.
|
||||
- **Split Tunneling:** Flexible support for routing specific apps or traffic through the VPN.
|
||||
- **Local Proxy Mode:** Expose WireGuard tunnels over a local SOCKS5 or HTTP proxy to browsers or firewall apps (like AdGuard).
|
||||
- **Lockdown Mode:** Advanced in-app kill switch that blocks all traffic while the tunnel is down.
|
||||
- **Quick Controls:** Quick Settings tile and home screen shortcuts for easy toggling.
|
||||
- **Remote Control Support:** Intent-based automation for controlling tunnels and auto-tunneling from automation apps (like Tasker).
|
||||
- **Dynamic DNS Handling:** Automatically detect and update endpoints on server IP changes without requiring a restart.
|
||||
- **IPv6 Endpoints:** Automatically upgrade to IPv6 endpoints or fall back to IPv4 based on network conditions without requiring a restart.
|
||||
- **Android TV Support:** Full support for nearly all features on Android TV.
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
+38
-32
@@ -1,6 +1,5 @@
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.android.build.api.variant.FilterConfiguration
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
@@ -23,13 +22,6 @@ licensee {
|
||||
ignoreDependencies("com.github.topjohnwu.libsu")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
||||
}
|
||||
}
|
||||
|
||||
configure<ApplicationExtension> {
|
||||
namespace = Constants.APP_ID
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
@@ -46,10 +38,11 @@ configure<ApplicationExtension> {
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = !project.hasProperty("noSplits")
|
||||
val noSplits = providers.gradleProperty("noSplits").isPresent
|
||||
isEnable = !noSplits
|
||||
reset()
|
||||
include("armeabi-v7a", "arm64-v8a")
|
||||
isUniversalApk = !project.hasProperty("noSplits")
|
||||
isUniversalApk = !noSplits
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,14 +50,17 @@ configure<ApplicationExtension> {
|
||||
applicationId = Constants.APP_ID
|
||||
minSdk = Constants.MIN_SDK
|
||||
targetSdk = Constants.TARGET_SDK
|
||||
versionCode = computeVersionCode()
|
||||
versionName = computeVersionName()
|
||||
versionCode = Constants.VERSION_CODE
|
||||
versionName = Constants.VERSION_NAME
|
||||
|
||||
experimentalProperties["android.experimental.disableGitVersion"] = true
|
||||
|
||||
sourceSets {
|
||||
getByName("debug").assets.directories += "$projectDir/schemas"
|
||||
}
|
||||
|
||||
val languagesArray = buildLanguagesArray(languageList())
|
||||
val languagesProvider = project.languageListProvider()
|
||||
val languagesArray = buildLanguagesArray(languagesProvider.get())
|
||||
buildConfigField("String[]", "LANGUAGES", "new String[]{ $languagesArray }")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -134,8 +130,6 @@ configure<ApplicationExtension> {
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
@@ -148,31 +142,42 @@ configure<ApplicationExtension> {
|
||||
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
val isNightly = project.isNightlyBuild()
|
||||
|
||||
val abiNameMap =
|
||||
mapOf(
|
||||
"armeabi-v7a" to "armv7",
|
||||
"arm64-v8a" to "arm64",
|
||||
"x86" to "x86",
|
||||
"x86_64" to "x64",
|
||||
)
|
||||
if (isNightly) {
|
||||
variant.outputs.forEach { output ->
|
||||
|
||||
output.versionCode.set(
|
||||
output.versionCode.get() + project.getVersionCodeIncrement()
|
||||
)
|
||||
|
||||
val currentVersion = output.versionName.get()
|
||||
val nextVersion = bumpToNextPatchVersion(currentVersion)
|
||||
val gitHash = project.getGitCommitHash()
|
||||
|
||||
output.versionName.set("$nextVersion-nightly+git.$gitHash")
|
||||
}
|
||||
}
|
||||
|
||||
val abiNameMap = mapOf(
|
||||
"armeabi-v7a" to "armv7",
|
||||
"arm64-v8a" to "arm64",
|
||||
"x86" to "x86",
|
||||
"x86_64" to "x64",
|
||||
)
|
||||
|
||||
variant.outputs.forEach { output ->
|
||||
val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
|
||||
|
||||
val flavorName = variant.productFlavors.joinToString("-") { it.second }
|
||||
|
||||
val versionName = output.versionName.get()
|
||||
|
||||
val baseFileName = "${Constants.APP_NAME}-${flavorName}-v${versionName}"
|
||||
|
||||
val outputFileName =
|
||||
if (!abi.isNullOrEmpty()) {
|
||||
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
||||
"${baseFileName}-${shortAbiName}.apk"
|
||||
} else {
|
||||
"${baseFileName}.apk"
|
||||
}
|
||||
val outputFileName = if (!abi.isNullOrEmpty()) {
|
||||
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
||||
"${baseFileName}-${shortAbiName}.apk"
|
||||
} else {
|
||||
"${baseFileName}.apk"
|
||||
}
|
||||
|
||||
output.outputFileName.set(outputFileName)
|
||||
}
|
||||
@@ -225,6 +230,7 @@ dependencies {
|
||||
// UI utilities
|
||||
implementation(libs.bundles.ui.utilities)
|
||||
implementation(libs.lottie.compose)
|
||||
implementation(libs.sonner)
|
||||
|
||||
// Misc utilities
|
||||
implementation(libs.bundles.misc.utilities)
|
||||
|
||||
@@ -74,6 +74,13 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="wg" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
||||
@@ -169,7 +176,7 @@
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
<service
|
||||
android:name=".core.service.tile.TunnelControlTile"
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/tunnel_control"
|
||||
@@ -186,7 +193,7 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".core.service.tile.AutoTunnelControlTile"
|
||||
android:name=".service.tile.AutoTunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/auto_tunnel"
|
||||
@@ -203,7 +210,7 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".core.service.autotunnel.AutoTunnelService"
|
||||
android:name=".service.autotunnel.AutoTunnelService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
|
||||
@@ -19,17 +19,29 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CheckCircleOutline
|
||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -41,15 +53,12 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
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.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
@@ -59,6 +68,10 @@ import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import com.dokar.sonner.TextToastAction
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.dokar.sonner.Toaster
|
||||
import com.dokar.sonner.rememberToasterState
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
@@ -69,11 +82,8 @@ import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarInfo
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.rememberCustomSnackbarState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
|
||||
@@ -110,19 +120,28 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.ipv6.IPv6
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Heart
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_DECRYPTION_ERROR
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED
|
||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD
|
||||
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@@ -132,6 +151,7 @@ import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.orbitmvi.orbit.compose.collectAsState
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@@ -155,9 +175,10 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
handleIncomingIntent(intent)
|
||||
roomBackup = RoomBackup(this).database(appDatabase).enableLogDebug(true).maxFileCount(5)
|
||||
|
||||
roomBackup = RoomBackup(this)
|
||||
handleConfigFileIntent(intent)
|
||||
handleWgDeepLinkIntent(intent)
|
||||
|
||||
installSplashScreen().apply {
|
||||
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
|
||||
@@ -175,7 +196,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val snackbarState = rememberCustomSnackbarState()
|
||||
val toaster = rememberToasterState()
|
||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
||||
var requestingTunnelMode by remember {
|
||||
@@ -232,22 +253,18 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
is GlobalSideEffect.Snackbar -> {
|
||||
scope.launch {
|
||||
snackbarState.showSnackbar(
|
||||
SnackbarInfo(
|
||||
message =
|
||||
buildAnnotatedString {
|
||||
append(sideEffect.message.asString(context))
|
||||
},
|
||||
type = sideEffect.type ?: SnackbarType.INFO,
|
||||
durationMs = sideEffect.durationMs ?: 4000L,
|
||||
)
|
||||
)
|
||||
when (sideEffect.type) {
|
||||
ToastType.Warning,
|
||||
ToastType.Error -> toaster.dismissAll()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
is GlobalSideEffect.Toast ->
|
||||
scope.launch { context.showToast(sideEffect.message.asString(context)) }
|
||||
toaster.show(
|
||||
message = sideEffect.message.asString(context),
|
||||
type = sideEffect.type,
|
||||
duration = (sideEffect.durationMs ?: 4000L).milliseconds,
|
||||
)
|
||||
}
|
||||
|
||||
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url)
|
||||
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk)
|
||||
@@ -275,49 +292,37 @@ class MainActivity : AppCompatActivity() {
|
||||
},
|
||||
)
|
||||
|
||||
val annotatedMessage = buildAnnotatedString {
|
||||
append(context.getString(R.string.donation_prompt_prefix))
|
||||
append(" ")
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(
|
||||
tag = context.getString(R.string.support),
|
||||
styles =
|
||||
TextLinkStyles(
|
||||
style =
|
||||
SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
focusedStyle =
|
||||
SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
background =
|
||||
MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.2f
|
||||
),
|
||||
),
|
||||
),
|
||||
) {
|
||||
snackbarState.dismissCurrent()
|
||||
navController.push(Route.Donate)
|
||||
}
|
||||
) {
|
||||
append(context.getString(R.string.donation_prompt_link))
|
||||
}
|
||||
append(" ")
|
||||
append(context.getString(R.string.donation_prompt_suffix))
|
||||
uiState.pendingWgImportUrl?.let { url ->
|
||||
val host = Uri.parse(url).host ?: url
|
||||
InfoDialog(
|
||||
onDismiss = { viewModel.dismissWgImport() },
|
||||
onAttest = { viewModel.importFromUrl(url) },
|
||||
title = stringResource(R.string.add_from_url),
|
||||
body = { Text(stringResource(R.string.wg_url_confirm_message, host)) },
|
||||
confirmText = stringResource(R.string.okay),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
|
||||
viewModel.setShouldShowDonationSnackbar(false)
|
||||
snackbarState.showSnackbar(
|
||||
SnackbarInfo(
|
||||
message = annotatedMessage,
|
||||
type = SnackbarType.THANK_YOU,
|
||||
durationMs = 30_000L,
|
||||
)
|
||||
toaster.show(
|
||||
message =
|
||||
context.getString(R.string.donation_prompt_prefix) +
|
||||
" " +
|
||||
context.getString(R.string.donation_prompt_link) +
|
||||
" " +
|
||||
context.getString(R.string.donation_prompt_suffix),
|
||||
type = ToastType.Normal,
|
||||
duration = 30_000L.milliseconds,
|
||||
action =
|
||||
TextToastAction(
|
||||
text = context.getString(R.string.donate_title),
|
||||
onClick = { toastId ->
|
||||
toaster.dismiss(toastId)
|
||||
navController.push(Route.Donate)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -378,25 +383,6 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
snackbarState.SnackbarHost(
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 80.dp)
|
||||
) { info ->
|
||||
CustomSnackBar(
|
||||
message = info.message,
|
||||
type = info.type,
|
||||
onDismiss = { snackbarState.dismissCurrent() },
|
||||
containerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp
|
||||
),
|
||||
modifier =
|
||||
Modifier.wrapContentHeight(align = Alignment.Top),
|
||||
)
|
||||
}
|
||||
},
|
||||
topBar = { DynamicTopAppBar(navState) },
|
||||
bottomBar = {
|
||||
if (navState.showBottomItems) {
|
||||
@@ -548,6 +534,70 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
}
|
||||
Toaster(
|
||||
state = toaster,
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, -220),
|
||||
richColors = true,
|
||||
background = {
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||
)
|
||||
)
|
||||
},
|
||||
elevation = 1.dp,
|
||||
shadowAmbientColor =
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f),
|
||||
shadowSpotColor =
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
border = {
|
||||
BorderStroke(
|
||||
0.dp,
|
||||
androidx.compose.ui.graphics.Color.Transparent,
|
||||
)
|
||||
},
|
||||
actionSlot = { toast ->
|
||||
(toast.action as? TextToastAction)?.let { action ->
|
||||
TextButton(
|
||||
onClick = { action.onClick(toast) },
|
||||
colors =
|
||||
ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp),
|
||||
) {
|
||||
Text(text = action.text, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
},
|
||||
iconSlot = { toast ->
|
||||
val (icon, color) =
|
||||
when (toast.type) {
|
||||
ToastType.Success ->
|
||||
Icons.Outlined.CheckCircleOutline to SilverTree
|
||||
ToastType.Error ->
|
||||
Icons.Outlined.ErrorOutline to AlertRed
|
||||
ToastType.Warning ->
|
||||
Icons.Outlined.WarningAmber to Straw
|
||||
ToastType.Info ->
|
||||
Icons.Outlined.Info to
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
ToastType.Normal ->
|
||||
Icons.Outlined.FavoriteBorder to Heart
|
||||
}
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.padding(end = 12.dp),
|
||||
)
|
||||
},
|
||||
contentColor = { MaterialTheme.colorScheme.onSurface },
|
||||
shape = { RoundedCornerShape(16.dp) },
|
||||
showCloseButton = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -555,55 +605,14 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun performBackup() = lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.enableLogDebug(true)
|
||||
.maxFileCount(5)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.backup_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.backup_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun handleWgDeepLinkIntent(intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
val uri = intent.data ?: return
|
||||
if (uri.scheme == "wg") {
|
||||
val httpsUrl = uri.toString().replaceFirst("wg://", "https://")
|
||||
viewModel.promptWgImport(httpsUrl)
|
||||
}
|
||||
.backup()
|
||||
}
|
||||
|
||||
fun performRestore() = lifecycleScope.launch {
|
||||
roomBackup
|
||||
.database(appDatabase)
|
||||
.enableLogDebug(true)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
showToast(
|
||||
getString(
|
||||
R.string.restore_success,
|
||||
getString(R.string.restarting_app),
|
||||
)
|
||||
)
|
||||
restartApp()
|
||||
} else {
|
||||
showToast(R.string.restore_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.restore()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -611,21 +620,105 @@ class MainActivity : AppCompatActivity() {
|
||||
networkMonitor.checkPermissionsAndUpdateState()
|
||||
}
|
||||
|
||||
fun performBackup(encrypt: Boolean = false, password: String? = null) {
|
||||
roomBackup
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
if (encrypt && !password.isNullOrBlank()) {
|
||||
backupIsEncrypted(true)
|
||||
customEncryptPassword(password)
|
||||
}
|
||||
}
|
||||
.onCompleteListener { success, _, _ ->
|
||||
lifecycleScope.launch {
|
||||
val sideEffect =
|
||||
if (success) {
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.backup_success),
|
||||
ToastType.Success,
|
||||
)
|
||||
} else {
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.backup_failed),
|
||||
ToastType.Error,
|
||||
)
|
||||
}
|
||||
viewModel.postSideEffect(sideEffect)
|
||||
}
|
||||
}
|
||||
.backup()
|
||||
}
|
||||
|
||||
fun performRestore(encrypt: Boolean = false, password: String? = null) {
|
||||
roomBackup
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
if (encrypt && !password.isNullOrBlank()) {
|
||||
backupIsEncrypted(true)
|
||||
customEncryptPassword(password)
|
||||
}
|
||||
}
|
||||
.onCompleteListener { success, message, exitCode ->
|
||||
lifecycleScope.launch {
|
||||
if (success) {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.restore_success),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
roomBackup.restartApp(Intent(this@MainActivity, MainActivity::class.java))
|
||||
} else {
|
||||
Timber.w("Restore failed, exitCode=$exitCode, message=$message")
|
||||
|
||||
val errorMessage =
|
||||
when (exitCode) {
|
||||
EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD ->
|
||||
getString(R.string.restore_failed_wrong_password)
|
||||
|
||||
EXIT_CODE_ERROR,
|
||||
EXIT_CODE_ERROR_DECRYPTION_ERROR,
|
||||
EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED ->
|
||||
getString(R.string.restore_failed_invalid_file)
|
||||
|
||||
else -> getString(R.string.restore_failed)
|
||||
}
|
||||
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.DynamicString(errorMessage),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.restore()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handleIncomingIntent(intent)
|
||||
handleConfigFileIntent(intent)
|
||||
handleWgDeepLinkIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleIncomingIntent(intent: Intent?) {
|
||||
private fun handleConfigFileIntent(intent: Intent?) {
|
||||
intent ?: return
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_VIEW,
|
||||
Intent.ACTION_EDIT,
|
||||
Intent.ACTION_SEND -> {
|
||||
val uri: Uri? = intent.data
|
||||
uri?.let { viewModel.importFromUri(it) }
|
||||
val uri: Uri? = intent.data ?: return
|
||||
val name = uri?.lastPathSegment?.lowercase() ?: return
|
||||
if (
|
||||
!name.endsWith(FileUtils.CONF_FILE_EXTENSION) &&
|
||||
!name.endsWith(FileUtils.ZIP_FILE_EXTENSION)
|
||||
) {
|
||||
Timber.d("Ignoring non-config URI in handleIncomingIntent: $uri")
|
||||
return
|
||||
}
|
||||
viewModel.importFromUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,8 @@ import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.di.tunnelModule
|
||||
import com.zaneschepke.tunnel.service.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AppBoostrapCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
@@ -21,6 +18,9 @@ import com.zaneschepke.wireguardautotunnel.di.dispatchersModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.networkModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.tunnelBackendProviderModule
|
||||
import com.zaneschepke.wireguardautotunnel.di.workerModule
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -51,6 +51,13 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
|
||||
private val backend: Backend by inject()
|
||||
|
||||
private val alwaysOnCallback =
|
||||
object : VpnService.AlwaysOnCallback {
|
||||
override fun alwaysOnTriggered() {
|
||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(KoinViewModelScopeApi::class)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -71,11 +78,10 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
lazyModules(networkModule)
|
||||
}
|
||||
instance = this
|
||||
|
||||
notificationService.createAllChannels()
|
||||
|
||||
// Sync tiles
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
TunnelTileRefresher.refresh(this)
|
||||
syncTiles()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
@@ -87,13 +93,7 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
Timber.plant(ReleaseTree())
|
||||
}
|
||||
|
||||
backend.setAlwaysOnCallback(
|
||||
object : VpnService.AlwaysOnCallback {
|
||||
override fun alwaysOnTriggered() {
|
||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||
}
|
||||
}
|
||||
)
|
||||
backend.setAlwaysOnCallback(alwaysOnCallback)
|
||||
|
||||
val dispatcher = get<TunnelEventDispatcher>()
|
||||
val coordinator = get<TunnelCoordinator>()
|
||||
@@ -111,6 +111,11 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
applicationScope.launch(ioDispatcher) { boostrapCoordinator.bootstrap() }
|
||||
}
|
||||
|
||||
private fun syncTiles() {
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
TunnelTileRefresher.refresh(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
|
||||
+1
-1
@@ -3,11 +3,11 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
+160
-14
@@ -1,29 +1,41 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.event
|
||||
|
||||
import android.content.Context
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationLine
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationLine
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TunnelEventDispatcher(
|
||||
private val notificationManager: TunnelNotificationService,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val context: Context,
|
||||
private val appVisibilityObserver: AppVisibilityObserver,
|
||||
private val globalEffectRepository: GlobalEffectRepository,
|
||||
) {
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
fun bind(
|
||||
scope: CoroutineScope,
|
||||
providerEvents: Flow<TunnelEvent>,
|
||||
@@ -32,54 +44,174 @@ class TunnelEventDispatcher(
|
||||
tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>>,
|
||||
) {
|
||||
|
||||
// informational events
|
||||
// Informational events from tunnel backend
|
||||
providerEvents
|
||||
.distinctUntilChanged()
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is TunnelEvent.FallbackToIpv4 -> {
|
||||
val name = getTunnelName(event.tunnelId)
|
||||
notificationManager.showIpv4Fallback(name)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(
|
||||
R.string.notification_ipv4_fallback_message,
|
||||
name,
|
||||
)
|
||||
),
|
||||
type = ToastType.Info,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showIpv4Fallback(name) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelEvent.RecoveredToIpv6 -> {
|
||||
val name = getTunnelName(event.tunnelId)
|
||||
notificationManager.showIpv6Recovery(name)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(
|
||||
R.string.notification_ipv6_recovery_message,
|
||||
name,
|
||||
)
|
||||
),
|
||||
type = ToastType.Success,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showIpv6Recovery(name) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelEvent.DynamicDnsUpdate -> {
|
||||
val name = getTunnelName(event.tunnelId)
|
||||
notificationManager.showDynamicDnsUpdate(name)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(
|
||||
R.string.notification_dynamic_dns_message,
|
||||
name,
|
||||
)
|
||||
),
|
||||
type = ToastType.Info,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showDynamicDnsUpdate(name) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelEvent.NoRootShellAccess -> {
|
||||
notificationManager.showRootShellAccess()
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(R.string.error_root_denied)
|
||||
),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showRootShellAccess() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
// errors from the coordinator
|
||||
// Errors from our tunnel coordinator
|
||||
coordinatorErrors
|
||||
.distinctUntilChanged()
|
||||
.onEach { error ->
|
||||
when (error) {
|
||||
is TunnelErrorEvent.VpnPermissionDenied -> {
|
||||
notificationManager.showVpnRequired()
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message =
|
||||
StringValue.DynamicString(
|
||||
context.getString(R.string.vpn_permission_required)
|
||||
),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showVpnRequired() },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.InternalFailure -> {
|
||||
notificationManager.showError(error.message)
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message = StringValue.DynamicString(error.message),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = { notificationManager.showError(error.message) },
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.Socks5PortUnavailable -> {
|
||||
val name = getTunnelName(error.tunnelId)
|
||||
notificationManager.showSocks5PortUnavailable(error.port, name)
|
||||
val message =
|
||||
context.getString(R.string.error_socks5_port_unavailable, error.port)
|
||||
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message = StringValue.DynamicString(message),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = {
|
||||
notificationManager.showSocks5PortUnavailable(error.port, name)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelErrorEvent.HttpPortUnavailable -> {
|
||||
val name = getTunnelName(error.tunnelId)
|
||||
notificationManager.showHttpPortUnavailable(error.port, name)
|
||||
val message =
|
||||
context.getString(R.string.error_http_port_unavailable, error.port)
|
||||
|
||||
showOrNotify(
|
||||
scope = scope,
|
||||
foregroundAction = {
|
||||
globalEffectRepository.post(
|
||||
GlobalSideEffect.Snackbar(
|
||||
message = StringValue.DynamicString(message),
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
},
|
||||
backgroundAction = {
|
||||
notificationManager.showHttpPortUnavailable(error.port, name)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,6 +245,7 @@ class TunnelEventDispatcher(
|
||||
.associateBy { it.id }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.debounce(500.milliseconds) // give the service notification time to display
|
||||
.onEach { vpnLines -> notificationManager.updateVpnPersistentNotification(vpnLines) }
|
||||
.launchIn(scope)
|
||||
|
||||
@@ -140,12 +273,25 @@ class TunnelEventDispatcher(
|
||||
.associateBy { it.id }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.debounce(500.milliseconds) // give the service notification time to display
|
||||
.onEach { proxyLines ->
|
||||
notificationManager.updateProxyPersistentNotification(proxyLines)
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private fun showOrNotify(
|
||||
scope: CoroutineScope,
|
||||
foregroundAction: suspend () -> Unit,
|
||||
backgroundAction: () -> Unit,
|
||||
) {
|
||||
if (appVisibilityObserver.isForeground.value) {
|
||||
scope.launch { foregroundAction() }
|
||||
} else {
|
||||
backgroundAction()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTunnelName(tunnelId: Int): String {
|
||||
return tunnelRepository.getById(tunnelId)?.name ?: context.getString(R.string.unknown)
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
|
||||
class AutoTunnelCoordinator(
|
||||
private val repository: AutoTunnelSettingsRepository,
|
||||
|
||||
+21
-4
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelErrorEvent
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomDnsSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
@@ -17,6 +16,7 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepos
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -47,6 +47,9 @@ class TunnelCoordinator(
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
|
||||
private val _userOverrideFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||
val userOverrideFlow = _userOverrideFlow.asSharedFlow()
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>> =
|
||||
tunnelProvider.backendStatus
|
||||
@@ -107,11 +110,24 @@ class TunnelCoordinator(
|
||||
) = tunnelMutex.withLock {
|
||||
// wait for app to be bootstrapped
|
||||
bootstrapCoordinator.isReady.first { it }
|
||||
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
|
||||
// enforce single tunnel, for now
|
||||
if (backendStatus.value.activeTunnels.isNotEmpty()) {
|
||||
stopActiveTunnelsInternal()
|
||||
}
|
||||
|
||||
startTunnelInternal(config, source)
|
||||
}
|
||||
|
||||
suspend fun stopTunnel(id: Int, source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
stopTunnelInternal(id, source)
|
||||
}
|
||||
|
||||
@@ -167,9 +183,6 @@ class TunnelCoordinator(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO for now, enforce single tunnel until multi-tunneling is implement
|
||||
stopActiveTunnelsInternal()
|
||||
|
||||
tunnelProvider
|
||||
.startTunnel(
|
||||
tunnel =
|
||||
@@ -193,6 +206,10 @@ class TunnelCoordinator(
|
||||
|
||||
suspend fun toggleTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
|
||||
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||
if (active.isNotEmpty()) {
|
||||
lastActiveTunnels = active.keys.toList()
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.tunnel.ApplicationProvider
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationLine
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class AndroidApplicationProvider(
|
||||
private val notificationService: NotificationService,
|
||||
private val tunnelNotificationService: TunnelNotificationService,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
) : ApplicationProvider {
|
||||
|
||||
private val context: Context = notificationService.context
|
||||
|
||||
override fun refreshTile(context: Context) {
|
||||
TunnelTileRefresher.refresh(context)
|
||||
}
|
||||
|
||||
override fun createVpnConfigurePendingIntent(context: Context): PendingIntent {
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
}
|
||||
|
||||
override val vpnInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = AndroidNotificationService.NotificationChannels.Tunnel.VPN,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val proxyInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = AndroidNotificationService.NotificationChannels.Tunnel.Proxy,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val vpnNotificationId: Int
|
||||
get() = NotificationService.VPN_NOTIFICATION_ID
|
||||
|
||||
override val proxyNotificationId: Int
|
||||
get() = NotificationService.PROXY_NOTIFICATION_ID
|
||||
|
||||
override suspend fun buildVpnPersistentNotification(
|
||||
currentStatus: BackendStatus
|
||||
): Notification {
|
||||
val lines = computeVpnNotificationLines(currentStatus)
|
||||
return tunnelNotificationService.buildVpnPersistentNotification(lines)
|
||||
}
|
||||
|
||||
override suspend fun buildProxyPersistentNotification(
|
||||
currentStatus: BackendStatus
|
||||
): Notification {
|
||||
val lines = computeProxyNotificationLines(currentStatus)
|
||||
return tunnelNotificationService.buildProxyPersistentNotification(lines)
|
||||
}
|
||||
|
||||
private suspend fun computeVpnNotificationLines(
|
||||
status: BackendStatus
|
||||
): Map<Int, TunnelNotificationLine> {
|
||||
val activeTunnels = status.activeTunnels
|
||||
val allTunnels = tunnelRepository.userTunnelsFlow.first()
|
||||
return activeTunnels
|
||||
.mapNotNull { (id, activeTunnel) ->
|
||||
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||
if (mode !is BackendMode.Vpn && mode !is BackendMode.Proxy.KillSwitchPrimary)
|
||||
return@mapNotNull null
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
val displayState = DisplayTunnelState.from(activeTunnel)
|
||||
TunnelNotificationLine(id, tunnel.name, displayState)
|
||||
}
|
||||
.associateBy { it.id }
|
||||
}
|
||||
|
||||
private suspend fun computeProxyNotificationLines(
|
||||
status: BackendStatus
|
||||
): Map<Int, TunnelNotificationLine> {
|
||||
val activeTunnels = status.activeTunnels
|
||||
val allTunnels = tunnelRepository.userTunnelsFlow.first()
|
||||
return activeTunnels
|
||||
.mapNotNull { (id, activeTunnel) ->
|
||||
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||
if (mode !is BackendMode.Proxy.Standard) return@mapNotNull null
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
val displayState = DisplayTunnelState.from(activeTunnel)
|
||||
TunnelNotificationLine(id, tunnel.name, displayState)
|
||||
}
|
||||
.associateBy { it.id }
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import java.util.concurrent.TimeUnit
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ import android.os.StrictMode
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.logcatter.LogcatReader
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.tunnel.NotificationProvider
|
||||
import com.zaneschepke.tunnel.backend.RootShell
|
||||
import com.zaneschepke.tunnel.ApplicationProvider
|
||||
import com.zaneschepke.tunnel.util.RootShell
|
||||
import com.zaneschepke.tunnel.util.RootShellException
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidTunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.AndroidApplicationProvider
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelBackendProvider
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidTunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -36,40 +30,15 @@ import timber.log.Timber
|
||||
|
||||
val tunnelBackendProviderModule = module {
|
||||
single<TunnelNotificationService> { AndroidTunnelNotificationService(get()) }
|
||||
single { AppVisibilityObserver() }
|
||||
singleOf(::TunnelEventDispatcher)
|
||||
|
||||
single<NotificationProvider> {
|
||||
val notificationService = get<NotificationService>()
|
||||
val context = androidContext()
|
||||
object : NotificationProvider {
|
||||
override val vpnInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.Tunnel.VPN,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val proxyInitNotification: Notification
|
||||
get() =
|
||||
notificationService.createNotification(
|
||||
channel = NotificationChannels.Tunnel.Proxy,
|
||||
title = context.getString(R.string.initializing),
|
||||
onGoing = true,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
|
||||
override val vpnNotificationId: Int
|
||||
get() = NotificationService.VPN_NOTIFICATION_ID
|
||||
|
||||
override val proxyNotificationId: Int
|
||||
get() = NotificationService.PROXY_NOTIFICATION_ID
|
||||
|
||||
override fun refreshTile(context: Context) {
|
||||
TunnelTileRefresher.refresh(context)
|
||||
}
|
||||
}
|
||||
single<ApplicationProvider> {
|
||||
AndroidApplicationProvider(
|
||||
notificationService = get(),
|
||||
tunnelNotificationService = get(),
|
||||
tunnelRepository = get(),
|
||||
)
|
||||
}
|
||||
|
||||
single {
|
||||
|
||||
@@ -7,4 +7,6 @@ sealed interface AutoTunnelEvent {
|
||||
data class Sync(val start: Set<TunnelConfig>, val stop: Set<Int>) : AutoTunnelEvent
|
||||
|
||||
data object DoNothing : AutoTunnelEvent
|
||||
|
||||
data object StopAllDueToNoInternet : AutoTunnelEvent
|
||||
}
|
||||
|
||||
+7
-2
@@ -4,7 +4,12 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
|
||||
sealed interface TunnelActionEvent {
|
||||
|
||||
data class Started(val tunnelId: Int, val source: TunnelActionSource) : TunnelActionEvent
|
||||
val source: TunnelActionSource
|
||||
val tunnelId: Int
|
||||
|
||||
data class Stopped(val tunnelId: Int, val source: TunnelActionSource) : TunnelActionEvent
|
||||
data class Started(override val tunnelId: Int, override val source: TunnelActionSource) :
|
||||
TunnelActionEvent
|
||||
|
||||
data class Stopped(override val tunnelId: Int, override val source: TunnelActionSource) :
|
||||
TunnelActionEvent
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
||||
class GlobalEffectRepository {
|
||||
|
||||
private val _globalEffectFlow =
|
||||
MutableSharedFlow<GlobalSideEffect>(replay = 0, extraBufferCapacity = 1)
|
||||
MutableSharedFlow<GlobalSideEffect>(replay = 0, extraBufferCapacity = 0)
|
||||
val flow = _globalEffectFlow.asSharedFlow()
|
||||
|
||||
suspend fun post(effect: GlobalSideEffect) {
|
||||
|
||||
+2
-4
@@ -1,8 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.sideeffect
|
||||
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import java.io.File
|
||||
|
||||
@@ -10,14 +10,12 @@ sealed class GlobalSideEffect {
|
||||
|
||||
data class Snackbar(
|
||||
val message: StringValue,
|
||||
val type: SnackbarType? = null,
|
||||
val type: ToastType,
|
||||
val actionLabel: String? = null,
|
||||
val onAction: (() -> Unit)? = null,
|
||||
val durationMs: Long? = null,
|
||||
) : GlobalSideEffect()
|
||||
|
||||
data class Toast(val message: StringValue) : GlobalSideEffect()
|
||||
|
||||
data object PopBackStack : GlobalSideEffect()
|
||||
|
||||
data class LaunchUrl(val url: String) : GlobalSideEffect()
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.zaneschepke.wireguardautotunnel.lifecyle
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class AppVisibilityObserver : DefaultLifecycleObserver {
|
||||
|
||||
private val _isForeground = MutableStateFlow(false)
|
||||
val isForeground: StateFlow<Boolean> = _isForeground.asStateFlow()
|
||||
|
||||
init {
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
_isForeground.value = true
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
_isForeground.value = false
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
@@ -17,8 +17,8 @@ import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.EXTRA_ID
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.EXTRA_ID
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
class AndroidNotificationService(override val context: Context) : NotificationService {
|
||||
+130
-25
@@ -1,21 +1,116 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
||||
|
||||
class AndroidTunnelNotificationService(private val notificationService: NotificationService) :
|
||||
TunnelNotificationService {
|
||||
|
||||
private val context = notificationService.context
|
||||
|
||||
private fun createGroupNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
||||
channel: NotificationChannels.Tunnel,
|
||||
groupKey: String,
|
||||
): android.app.Notification {
|
||||
val title =
|
||||
if (tunnelNotificationLines.size == 1) {
|
||||
val name = tunnelNotificationLines.values.first().name
|
||||
when (channel) {
|
||||
is NotificationChannels.Tunnel.VPN ->
|
||||
"${context.getString(R.string.vpn)} • $name"
|
||||
is NotificationChannels.Tunnel.Proxy ->
|
||||
"${context.getString(R.string.proxy)} • $name"
|
||||
}
|
||||
} else {
|
||||
when (channel) {
|
||||
is NotificationChannels.Tunnel.VPN -> context.getString(R.string.vpn)
|
||||
is NotificationChannels.Tunnel.Proxy -> context.getString(R.string.proxy)
|
||||
}
|
||||
}
|
||||
|
||||
val formattedLines =
|
||||
tunnelNotificationLines.values.map { line ->
|
||||
val status = line.displayState.asLocalizedString(context)
|
||||
|
||||
if (tunnelNotificationLines.size == 1) {
|
||||
status
|
||||
} else {
|
||||
context.getString(R.string.notification_tunnel_status_format, line.name, status)
|
||||
}
|
||||
}
|
||||
val description = formattedLines.joinToString("\n")
|
||||
|
||||
val actions =
|
||||
if (tunnelNotificationLines.size == 1) {
|
||||
val tunnelId = tunnelNotificationLines.keys.first()
|
||||
listOf(
|
||||
notificationService.createNotificationAction(
|
||||
notificationAction = NotificationAction.TUNNEL_OFF,
|
||||
extraId = tunnelId,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
notificationService.createNotificationAction(
|
||||
notificationAction = NotificationAction.STOP_ALL,
|
||||
extraId = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val style =
|
||||
if (tunnelNotificationLines.size > 1) {
|
||||
NotificationCompat.InboxStyle()
|
||||
.setBigContentTitle(title)
|
||||
.setSummaryText(
|
||||
"${tunnelNotificationLines.size} ${context.getString(R.string.tunnels).lowercase()}"
|
||||
)
|
||||
.also { inbox -> formattedLines.forEach { inbox.addLine(it) } }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return notificationService.createNotification(
|
||||
channel = channel,
|
||||
title = title,
|
||||
description = description,
|
||||
actions = actions,
|
||||
onGoing = true,
|
||||
onlyAlertOnce = true,
|
||||
groupKey = groupKey,
|
||||
style = style,
|
||||
)
|
||||
}
|
||||
|
||||
override fun buildVpnPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): android.app.Notification {
|
||||
return createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.VPN,
|
||||
VPN_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
override fun buildProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): android.app.Notification {
|
||||
return createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.Proxy,
|
||||
PROXY_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateGroupNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
||||
notificationId: Int,
|
||||
@@ -88,26 +183,36 @@ class AndroidTunnelNotificationService(private val notificationService: Notifica
|
||||
notificationService.show(notificationId, notification)
|
||||
}
|
||||
|
||||
override fun updateProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
) {
|
||||
updateGroupNotification(
|
||||
tunnelNotificationLines = tunnelNotificationLines,
|
||||
notificationId = PROXY_NOTIFICATION_ID,
|
||||
channel = NotificationChannels.Tunnel.Proxy,
|
||||
groupKey = PROXY_GROUP_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateVpnPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
) {
|
||||
updateGroupNotification(
|
||||
tunnelNotificationLines = tunnelNotificationLines,
|
||||
notificationId = VPN_NOTIFICATION_ID,
|
||||
channel = NotificationChannels.Tunnel.VPN,
|
||||
groupKey = VPN_GROUP_KEY,
|
||||
)
|
||||
if (tunnelNotificationLines.isEmpty()) {
|
||||
notificationService.remove(VPN_NOTIFICATION_ID)
|
||||
return
|
||||
}
|
||||
val notification =
|
||||
createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.VPN,
|
||||
VPN_GROUP_KEY,
|
||||
)
|
||||
notificationService.show(VPN_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
override fun updateProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
) {
|
||||
if (tunnelNotificationLines.isEmpty()) {
|
||||
notificationService.remove(PROXY_NOTIFICATION_ID)
|
||||
return
|
||||
}
|
||||
val notification =
|
||||
createGroupNotification(
|
||||
tunnelNotificationLines,
|
||||
NotificationChannels.Tunnel.Proxy,
|
||||
PROXY_GROUP_KEY,
|
||||
)
|
||||
notificationService.show(PROXY_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
override fun showIpv4Fallback(tunnelName: String) {
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService.NotificationChannels
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
|
||||
interface NotificationService {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
|
||||
+11
-1
@@ -1,4 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||
package com.zaneschepke.wireguardautotunnel.notification
|
||||
|
||||
import android.app.Notification
|
||||
|
||||
interface TunnelNotificationService {
|
||||
|
||||
@@ -6,6 +8,14 @@ interface TunnelNotificationService {
|
||||
|
||||
fun updateVpnPersistentNotification(tunnelNotificationLines: Map<Int, TunnelNotificationLine>)
|
||||
|
||||
fun buildVpnPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): Notification
|
||||
|
||||
fun buildProxyPersistentNotification(
|
||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||
): Notification
|
||||
|
||||
fun showIpv4Fallback(tunnelName: String)
|
||||
|
||||
fun showIpv6Recovery(tunnelName: String)
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service
|
||||
package com.zaneschepke.wireguardautotunnel.service
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelService
|
||||
|
||||
class ServiceManager(private val context: Context) {
|
||||
|
||||
+13
-6
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
@@ -17,6 +17,7 @@ class AutoTunnelEngine {
|
||||
}
|
||||
}
|
||||
Decision.None -> AutoTunnelEvent.DoNothing
|
||||
is Decision.StopDueToNoInternet -> AutoTunnelEvent.StopAllDueToNoInternet
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +28,17 @@ class AutoTunnelEngine {
|
||||
|
||||
val activeTunnelIds = backend.activeTunnels.keys.toSet()
|
||||
|
||||
val desiredTunnels = resolveDesiredTunnels(state).map { it.id }.toSet()
|
||||
|
||||
// stop condition overrides everything
|
||||
if (!network.hasInternet() && settings.isStopOnNoInternetEnabled) {
|
||||
return Decision.Sync(start = emptySet(), stop = activeTunnelIds)
|
||||
if (!network.hasInternet()) {
|
||||
return if (settings.isStopOnNoInternetEnabled) {
|
||||
Decision.StopDueToNoInternet
|
||||
} else {
|
||||
// keep tunnel state neutral on no internet otherwise
|
||||
Decision.None
|
||||
}
|
||||
}
|
||||
|
||||
val desiredTunnels = resolveDesiredTunnels(state).map { it.id }.toSet()
|
||||
|
||||
val toStart = desiredTunnels - activeTunnelIds
|
||||
val toStop = activeTunnelIds - desiredTunnels
|
||||
|
||||
@@ -96,5 +101,7 @@ class AutoTunnelEngine {
|
||||
data class Sync(val start: Set<TunnelConfig>, val stop: Set<Int>) : Decision
|
||||
|
||||
data object None : Decision
|
||||
|
||||
data object StopDueToNoInternet : Decision
|
||||
}
|
||||
}
|
||||
+89
-75
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.app.ServiceCompat
|
||||
@@ -7,16 +7,12 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
@@ -24,15 +20,22 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepos
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelTileRefresher
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -63,8 +66,7 @@ class AutoTunnelService : LifecycleService() {
|
||||
private var autoTunnelJob: Job? = null
|
||||
private var permissionsJob: Job? = null
|
||||
private var overridesJob: Job? = null
|
||||
|
||||
@Volatile private var manualOverrideState = ManualOverrideState()
|
||||
private var noInternetStopJob: Job? = null
|
||||
|
||||
private data class PermissionWarningState(
|
||||
val detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
|
||||
@@ -73,19 +75,19 @@ class AutoTunnelService : LifecycleService() {
|
||||
val ssidReadRequired: Boolean,
|
||||
)
|
||||
|
||||
private data class ManualOverrideState(
|
||||
val fingerprint: AutoTunnelState.NetworkFingerprint? = null,
|
||||
val stoppedTunnelIds: Set<Int> = emptySet(),
|
||||
val startedTunnelIds: Set<Int> = emptySet(),
|
||||
)
|
||||
@Volatile private var hasUserOverride = false
|
||||
private var lastNetworkFingerprint: AutoTunnelState.NetworkFingerprint? = null
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private val autoTunnelStateFlow: Flow<AutoTunnelState> by lazy {
|
||||
val networkFlow = networkEngine.stableState.mapNotNull { it?.state?.toDomain() }
|
||||
|
||||
val settingsFlow = combineSettings()
|
||||
|
||||
val backendFlow =
|
||||
tunnelCoordinator.backendStatus.distinctUntilChangedBy { it.activeTunnels.keys.toSet() }
|
||||
tunnelCoordinator.backendStatus
|
||||
.distinctUntilChanged { old, new -> old.activeTunnels == new.activeTunnels }
|
||||
.debounce(300L.milliseconds)
|
||||
|
||||
combine(networkFlow, settingsFlow, backendFlow) { network, settings, backend ->
|
||||
AutoTunnelState(
|
||||
@@ -121,7 +123,7 @@ class AutoTunnelService : LifecycleService() {
|
||||
permissionsJob?.cancel()
|
||||
permissionsJob = startLocationPermissionsNotificationJob()
|
||||
overridesJob?.cancel()
|
||||
overridesJob = startOverridesJob()
|
||||
overridesJob = startUserOverrideJob()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
@@ -130,48 +132,23 @@ class AutoTunnelService : LifecycleService() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cancelNoInternetStopJob()
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stateHolder.setActive(false)
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun startOverridesJob(): Job =
|
||||
private fun startUserOverrideJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
tunnelCoordinator.actions.collect { action ->
|
||||
tunnelCoordinator.userOverrideFlow.collect {
|
||||
reconciliationMutex.withLock {
|
||||
manualOverrideState =
|
||||
when (action) {
|
||||
is TunnelActionEvent.Started -> {
|
||||
|
||||
if (action.source != TunnelActionSource.USER) {
|
||||
return@withLock
|
||||
}
|
||||
|
||||
manualOverrideState.copy(
|
||||
startedTunnelIds =
|
||||
manualOverrideState.startedTunnelIds + action.tunnelId,
|
||||
stoppedTunnelIds =
|
||||
manualOverrideState.stoppedTunnelIds - action.tunnelId,
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelActionEvent.Stopped -> {
|
||||
|
||||
if (action.source != TunnelActionSource.USER) {
|
||||
return@withLock
|
||||
}
|
||||
|
||||
manualOverrideState.copy(
|
||||
stoppedTunnelIds =
|
||||
manualOverrideState.stoppedTunnelIds + action.tunnelId,
|
||||
startedTunnelIds =
|
||||
manualOverrideState.startedTunnelIds - action.tunnelId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("Updated manual overrides: $manualOverrideState")
|
||||
if (!hasUserOverride) {
|
||||
Timber.d(
|
||||
"User manually overrode Auto Tunnel on current network. Pausing auto decisions."
|
||||
)
|
||||
}
|
||||
hasUserOverride = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,50 +179,83 @@ class AutoTunnelService : LifecycleService() {
|
||||
)
|
||||
}
|
||||
|
||||
// Instead of stopping tunnel right away on no internet, we kick off this job to add short delay
|
||||
// and re-evaluation to prevent unwanted stops
|
||||
// on flaky networks and network transitions
|
||||
private fun scheduleNoInternetStop() {
|
||||
noInternetStopJob?.cancel()
|
||||
|
||||
noInternetStopJob =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
delay(NO_INTERNET_GRACE_PERIOD_MS.milliseconds)
|
||||
|
||||
reconciliationMutex.withLock {
|
||||
val currentNetworkState = networkEngine.stableState.value?.state?.toDomain()
|
||||
|
||||
val stillNoInternet = currentNetworkState?.hasInternet() == false
|
||||
val stopOnNoInternetEnabled =
|
||||
autoTunnelRepository.flow.firstOrNull()?.isStopOnNoInternetEnabled == true
|
||||
|
||||
if (stillNoInternet && stopOnNoInternetEnabled) {
|
||||
val currentActiveIds =
|
||||
tunnelCoordinator.backendStatus.value.activeTunnels.keys
|
||||
|
||||
if (currentActiveIds.isNotEmpty()) {
|
||||
Timber.w(
|
||||
"No internet grace period expired and still no internet. Stopping tunnels: $currentActiveIds"
|
||||
)
|
||||
currentActiveIds.forEach { tunnelId ->
|
||||
tunnelCoordinator.stopTunnel(
|
||||
tunnelId,
|
||||
TunnelActionSource.AUTO_TUNNEL,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Timber.d(
|
||||
"No internet grace period expired, but internet is back or setting disabled. Doing nothing."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelNoInternetStopJob() {
|
||||
noInternetStopJob?.cancel()
|
||||
noInternetStopJob = null
|
||||
}
|
||||
|
||||
private fun startAutoTunnelStateJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
autoTunnelStateFlow.collectLatest { state ->
|
||||
reconciliationMutex.withLock {
|
||||
updateFingerprintIfNeeded(state)
|
||||
|
||||
val rawEvent = engine.evaluate(state)
|
||||
|
||||
val event = applyOverrides(rawEvent)
|
||||
|
||||
Timber.d("AutoTunnel reconciliation event: $event")
|
||||
|
||||
handleAutoTunnelEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFingerprintIfNeeded(state: AutoTunnelState) {
|
||||
val fingerprint = state.networkFingerPrint
|
||||
val currentFingerprint = state.networkFingerPrint
|
||||
|
||||
if (manualOverrideState.fingerprint != fingerprint) {
|
||||
Timber.d("Network changed, clearing overrides")
|
||||
|
||||
manualOverrideState = ManualOverrideState(fingerprint = fingerprint)
|
||||
if (lastNetworkFingerprint != currentFingerprint) {
|
||||
if (hasUserOverride) {
|
||||
Timber.d("Network fingerprint changed, clearing user override")
|
||||
}
|
||||
hasUserOverride = false
|
||||
lastNetworkFingerprint = currentFingerprint
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyOverrides(event: AutoTunnelEvent): AutoTunnelEvent {
|
||||
|
||||
if (event !is AutoTunnelEvent.Sync) {
|
||||
return event
|
||||
return if (hasUserOverride) {
|
||||
AutoTunnelEvent.DoNothing
|
||||
} else {
|
||||
event
|
||||
}
|
||||
|
||||
val filteredStart =
|
||||
event.start.filterNot { it.id in manualOverrideState.stoppedTunnelIds }.toSet()
|
||||
|
||||
val filteredStop =
|
||||
event.stop.filterNot { it in manualOverrideState.startedTunnelIds }.toSet()
|
||||
|
||||
if (filteredStart.isEmpty() && filteredStop.isEmpty()) {
|
||||
return AutoTunnelEvent.DoNothing
|
||||
}
|
||||
|
||||
return event.copy(start = filteredStart, stop = filteredStop)
|
||||
}
|
||||
|
||||
private fun combineSettings():
|
||||
@@ -334,7 +344,7 @@ class AutoTunnelService : LifecycleService() {
|
||||
private suspend fun handleAutoTunnelEvent(event: AutoTunnelEvent) {
|
||||
when (event) {
|
||||
is AutoTunnelEvent.Sync -> {
|
||||
|
||||
cancelNoInternetStopJob()
|
||||
event.stop.forEach { tunnelId ->
|
||||
Timber.d("Stopping tunnel: $tunnelId")
|
||||
tunnelCoordinator.stopTunnel(tunnelId, TunnelActionSource.AUTO_TUNNEL)
|
||||
@@ -345,8 +355,12 @@ class AutoTunnelService : LifecycleService() {
|
||||
tunnelCoordinator.startTunnel(config, TunnelActionSource.AUTO_TUNNEL)
|
||||
}
|
||||
}
|
||||
|
||||
AutoTunnelEvent.StopAllDueToNoInternet -> scheduleNoInternetStop()
|
||||
AutoTunnelEvent.DoNothing -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NO_INTERNET_GRACE_PERIOD_MS = 10_000L
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.Context
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Favorite
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Info
|
||||
import androidx.compose.material.icons.rounded.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Snackbar
|
||||
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.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
|
||||
@Composable
|
||||
fun CustomSnackBar(
|
||||
message: AnnotatedString,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
type: SnackbarType = SnackbarType.INFO,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val icon =
|
||||
when (type) {
|
||||
SnackbarType.INFO -> Icons.Rounded.Info
|
||||
SnackbarType.WARNING -> Icons.Rounded.Warning
|
||||
SnackbarType.THANK_YOU -> Icons.Outlined.Favorite
|
||||
}
|
||||
val iconDescription =
|
||||
when (type) {
|
||||
SnackbarType.INFO -> stringResource(R.string.info)
|
||||
SnackbarType.WARNING -> stringResource(R.string.warning)
|
||||
SnackbarType.THANK_YOU -> stringResource(R.string.thank_you)
|
||||
}
|
||||
|
||||
Snackbar(
|
||||
containerColor = containerColor,
|
||||
modifier =
|
||||
modifier
|
||||
.wrapContentHeight(align = Alignment.Top)
|
||||
.padding(horizontal = if (isTv) 48.dp else 16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
.width(IntrinsicSize.Min)
|
||||
.padding(vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = iconDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
text = message,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 8,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
Row {
|
||||
IconButton(onClick = onDismiss, modifier = Modifier.size(24.dp)) {
|
||||
Icon(
|
||||
Icons.Rounded.Close,
|
||||
contentDescription = stringResource(R.string.stop),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun rememberCustomSnackbarState(): CustomSnackbarState {
|
||||
return remember { CustomSnackbarState() }
|
||||
}
|
||||
|
||||
class CustomSnackbarState {
|
||||
private val _snackbars = Channel<SnackbarInfo>(Channel.BUFFERED)
|
||||
val snackbars: Channel<SnackbarInfo> = _snackbars
|
||||
|
||||
private var currentSnackbar by mutableStateOf<SnackbarInfo?>(null)
|
||||
private var isShowing by mutableStateOf(false)
|
||||
|
||||
fun showSnackbar(info: SnackbarInfo) {
|
||||
_snackbars.trySend(info)
|
||||
}
|
||||
|
||||
fun dismissCurrent() {
|
||||
currentSnackbar = null
|
||||
isShowing = false
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SnackbarHost(
|
||||
modifier: Modifier = Modifier,
|
||||
snackbar: @Composable (SnackbarInfo) -> Unit = { info ->
|
||||
CustomSnackBar(
|
||||
message = info.message,
|
||||
type = info.type,
|
||||
onDismiss = { dismissCurrent() },
|
||||
modifier = Modifier,
|
||||
containerColor = MaterialTheme.colorScheme.surface.copy(.1f),
|
||||
)
|
||||
},
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
for (info in snackbars) {
|
||||
currentSnackbar = info
|
||||
isShowing = true
|
||||
|
||||
scope.launch {
|
||||
delay(info.durationMs)
|
||||
if (currentSnackbar?.id == info.id) {
|
||||
dismissCurrent()
|
||||
}
|
||||
}
|
||||
|
||||
while (isShowing && currentSnackbar?.id == info.id) {
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentSnackbar?.let { info ->
|
||||
if (isShowing) {
|
||||
Box(modifier = modifier) { snackbar(info) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
|
||||
enum class SnackbarType {
|
||||
INFO,
|
||||
WARNING,
|
||||
THANK_YOU,
|
||||
}
|
||||
|
||||
data class SnackbarInfo(
|
||||
val message: AnnotatedString,
|
||||
val type: SnackbarType = SnackbarType.INFO,
|
||||
val durationMs: Long = 4000L,
|
||||
val id: String = System.currentTimeMillis().toString(),
|
||||
)
|
||||
+49
-33
@@ -1,13 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.textbox
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
@@ -29,48 +34,59 @@ fun ConfigurationTextBox(
|
||||
leading: (@Composable () -> Unit)? = null,
|
||||
trailing: (@Composable (Modifier) -> Unit)? = null,
|
||||
supportingText: (@Composable () -> Unit)? = null,
|
||||
interactionSource: MutableInteractionSource = MutableInteractionSource(),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
singleLine: Boolean = true,
|
||||
) {
|
||||
Box(modifier = modifier.padding(top = 6.dp)) {
|
||||
CustomTextField(
|
||||
isError = isError,
|
||||
textStyle =
|
||||
MaterialTheme.typography.bodySmall.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth().defaultMinSize(minHeight = 48.dp),
|
||||
value = value,
|
||||
visualTransformation = visualTransformation,
|
||||
singleLine = singleLine,
|
||||
interactionSource = interactionSource,
|
||||
onValueChange = onValueChange,
|
||||
label = null, // Disable built in label
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
placeholder = {
|
||||
Text(
|
||||
text = hint,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
trailing = trailing,
|
||||
supportingText = supportingText,
|
||||
leading = leading,
|
||||
readOnly = readOnly,
|
||||
enabled = enabled,
|
||||
)
|
||||
|
||||
CustomTextField(
|
||||
isError = isError,
|
||||
textStyle =
|
||||
MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 48.dp),
|
||||
value = value,
|
||||
visualTransformation = visualTransformation,
|
||||
singleLine = singleLine,
|
||||
interactionSource = interactionSource,
|
||||
onValueChange = { onValueChange(it) },
|
||||
label = {
|
||||
// custom static label notch
|
||||
if (label.isNotEmpty()) {
|
||||
Text(
|
||||
label,
|
||||
text = label,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
modifier =
|
||||
Modifier.padding(start = 12.dp)
|
||||
.offset(y = (-8).dp)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(horizontal = 4.dp),
|
||||
)
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
placeholder = {
|
||||
Text(
|
||||
hint,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
trailing = trailing,
|
||||
supportingText = supportingText,
|
||||
leading = leading,
|
||||
readOnly = readOnly,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-15
@@ -22,7 +22,9 @@ import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -33,7 +35,7 @@ fun CustomTextField(
|
||||
modifier: Modifier = Modifier,
|
||||
textStyle: TextStyle =
|
||||
MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
label: @Composable () -> Unit,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
containerColor: Color,
|
||||
onValueChange: (value: String) -> Unit = {},
|
||||
singleLine: Boolean = true,
|
||||
@@ -47,10 +49,19 @@ fun CustomTextField(
|
||||
readOnly: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
interactionSource: MutableInteractionSource = MutableInteractionSource(),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
val space = " "
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
|
||||
val textFieldValue =
|
||||
remember(value, textFieldValueState) {
|
||||
if (textFieldValueState.text == value) {
|
||||
textFieldValueState
|
||||
} else {
|
||||
textFieldValueState.copy(text = value, selection = TextRange(value.length))
|
||||
}
|
||||
}
|
||||
|
||||
val cursorBrush =
|
||||
if (isFocused) SolidColor(MaterialTheme.colorScheme.primary)
|
||||
else SolidColor(Color.Transparent)
|
||||
@@ -67,9 +78,14 @@ fun CustomTextField(
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = value,
|
||||
value = textFieldValue,
|
||||
textStyle = effectiveTextStyle,
|
||||
onValueChange = { onValueChange(it) },
|
||||
onValueChange = { newTextFieldValue ->
|
||||
textFieldValueState = newTextFieldValue
|
||||
if (value != newTextFieldValue.text) {
|
||||
onValueChange(newTextFieldValue.text)
|
||||
}
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
keyboardOptions = keyboardOptions,
|
||||
readOnly = readOnly,
|
||||
@@ -90,15 +106,9 @@ fun CustomTextField(
|
||||
visualTransformation = visualTransformation,
|
||||
) {
|
||||
OutlinedTextFieldDefaults.DecorationBox(
|
||||
value = space + value,
|
||||
innerTextField = {
|
||||
if (value.isEmpty()) {
|
||||
if (placeholder != null) {
|
||||
placeholder()
|
||||
}
|
||||
}
|
||||
it.invoke()
|
||||
},
|
||||
value = value,
|
||||
innerTextField = it,
|
||||
placeholder = placeholder,
|
||||
contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 14.dp, bottom = 14.dp),
|
||||
leadingIcon = leading,
|
||||
trailingIcon =
|
||||
@@ -141,7 +151,6 @@ fun CustomTextField(
|
||||
label = label,
|
||||
visualTransformation = visualTransformation,
|
||||
interactionSource = interactionSource,
|
||||
placeholder = placeholder,
|
||||
container = {
|
||||
OutlinedTextFieldDefaults.Container(
|
||||
enabled = enabled,
|
||||
|
||||
@@ -42,7 +42,12 @@ sealed class Route : NavKey {
|
||||
|
||||
@Keep @Serializable data object Display : Route()
|
||||
|
||||
@Keep @Serializable data object Tunnels : Route()
|
||||
@Keep
|
||||
@Serializable
|
||||
data object Tunnels : Route(), SecureRoute {
|
||||
override val requiresProtection: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
@Keep @Serializable data class TunnelSettings(val id: Int) : Route()
|
||||
|
||||
|
||||
+2
-2
@@ -250,7 +250,7 @@ fun currentRouteAsNavbarState(
|
||||
val global = route !is ConfigEdit
|
||||
val tunnelName =
|
||||
if (!global) globalState.tunnelNames[route.id]
|
||||
else context.getString(R.string.configuration_globals)
|
||||
else context.getString(R.string.tunnel_configuration)
|
||||
NavbarState(
|
||||
topLeading = { TvBackButton { navController.pop() } },
|
||||
showBottomItems = true,
|
||||
@@ -297,7 +297,7 @@ fun currentRouteAsNavbarState(
|
||||
is SplitTunnelGlobal -> {
|
||||
val tunnelName =
|
||||
if (route is SplitTunnel) globalState.tunnelNames[route.id]
|
||||
else context.getString(R.string.global_split_tunneling)
|
||||
else context.getString(R.string.splt_tunneling)
|
||||
NavbarState(
|
||||
topLeading = { TvBackButton { navController.pop() } },
|
||||
topTitle = tunnelName ?: "",
|
||||
|
||||
+5
-2
@@ -302,7 +302,9 @@ fun AutoTunnelScreen(
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.PublicOff, contentDescription = null) },
|
||||
title = stringResource(R.string.stop_on_no_internet),
|
||||
description = { DescriptionText(stringResource(R.string.stop_on_internet_loss)) },
|
||||
description = {
|
||||
DescriptionText(stringResource(R.string.stop_on_no_internet_desc))
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = uiState.autoTunnelSettings.isStopOnNoInternetEnabled,
|
||||
@@ -318,7 +320,7 @@ fun AutoTunnelScreen(
|
||||
}
|
||||
Column {
|
||||
GroupLabel(
|
||||
stringResource(R.string.other),
|
||||
stringResource(R.string.automation),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
SurfaceRow(
|
||||
@@ -330,6 +332,7 @@ fun AutoTunnelScreen(
|
||||
onClick = { viewModel.setStartAtBoot(it) },
|
||||
)
|
||||
},
|
||||
description = { DescriptionText(stringResource(R.string.start_on_boot_desc)) },
|
||||
onClick = { viewModel.setStartAtBoot(!uiState.autoTunnelSettings.startOnBoot) },
|
||||
)
|
||||
}
|
||||
|
||||
+9
-2
@@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
@@ -45,11 +46,17 @@ fun PinLockScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
textColor = MaterialTheme.colorScheme.onSurface,
|
||||
onPinCorrect = { onPinCorrect() },
|
||||
onPinIncorrect = {
|
||||
sharedViewModel.showToast(StringValue.StringResource(R.string.incorrect_pin))
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.incorrect_pin),
|
||||
ToastType.Warning,
|
||||
)
|
||||
},
|
||||
onPinCreated = {
|
||||
pinCreated = true
|
||||
sharedViewModel.showToast(StringValue.StringResource(R.string.pin_created))
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.pin_created),
|
||||
ToastType.Success,
|
||||
)
|
||||
sharedViewModel.setPinLockEnabled(true)
|
||||
onPinCorrect()
|
||||
},
|
||||
|
||||
+51
-10
@@ -38,6 +38,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
@@ -51,12 +52,12 @@ import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupBottomSheet
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupEncryptionDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.AppModeBottomSheet
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
@@ -88,6 +89,9 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
var showBackupSheet by rememberSaveable { mutableStateOf(false) }
|
||||
var showEncryptionDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var isRestoreAction by remember { mutableStateOf(false) }
|
||||
|
||||
var showAppModeSheet by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val appMode = uiState.settings.tunnelMode
|
||||
@@ -99,19 +103,53 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
fun performBackupRestore(action: () -> Unit) {
|
||||
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive)
|
||||
return context.showToast(R.string.all_services_disabled)
|
||||
showBackupSheet = false
|
||||
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive) {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.all_services_disabled),
|
||||
ToastType.Warning,
|
||||
)
|
||||
return
|
||||
}
|
||||
action()
|
||||
}
|
||||
|
||||
if (showBackupSheet)
|
||||
if (showBackupSheet) {
|
||||
BackupBottomSheet(
|
||||
{ performBackupRestore { (context as? MainActivity)?.performBackup() } },
|
||||
{ performBackupRestore { (context as? MainActivity)?.performRestore() } },
|
||||
) {
|
||||
showBackupSheet = false
|
||||
}
|
||||
onBackup = {
|
||||
showBackupSheet = false
|
||||
isRestoreAction = false
|
||||
showEncryptionDialog = true
|
||||
},
|
||||
onRestore = {
|
||||
showBackupSheet = false
|
||||
isRestoreAction = true
|
||||
showEncryptionDialog = true
|
||||
},
|
||||
onDismiss = { showBackupSheet = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showEncryptionDialog) {
|
||||
BackupEncryptionDialog(
|
||||
isRestore = isRestoreAction,
|
||||
onConfirm = { encrypt, password ->
|
||||
showEncryptionDialog = false
|
||||
|
||||
if (isRestoreAction) {
|
||||
performBackupRestore {
|
||||
(context as? MainActivity)?.performRestore(encrypt, password)
|
||||
}
|
||||
} else {
|
||||
performBackupRestore {
|
||||
(context as? MainActivity)?.performBackup(encrypt, password)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = { showEncryptionDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showAppModeSheet)
|
||||
AppModeBottomSheet(sharedViewModel::setAppMode, uiState.settings.tunnelMode) {
|
||||
showAppModeSheet = false
|
||||
@@ -168,7 +206,8 @@ fun SettingsScreen(
|
||||
StringValue.StringResource(
|
||||
R.string.mode_disabled_template,
|
||||
appMode.asString(context),
|
||||
)
|
||||
),
|
||||
ToastType.Info,
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -176,6 +215,7 @@ fun SettingsScreen(
|
||||
leading = { Icon(Icons.Outlined.Public, contentDescription = null) },
|
||||
title = stringResource(R.string.tunnel_globals),
|
||||
onClick = { navController.push(Route.TunnelGlobals) },
|
||||
description = { DescriptionText(stringResource(R.string.tunnel_globals_desc)) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Terminal, contentDescription = null) },
|
||||
@@ -232,6 +272,7 @@ fun SettingsScreen(
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
description = { DescriptionText(stringResource(R.string.local_logging_desc)) },
|
||||
onClick = { navController.push(Route.Logs) },
|
||||
)
|
||||
SurfaceRow(
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
|
||||
|
||||
@Composable
|
||||
fun BackupEncryptionDialog(
|
||||
isRestore: Boolean,
|
||||
onConfirm: (encrypt: Boolean, password: String?) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var encrypt by remember { mutableStateOf(false) }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var confirmPassword by remember { mutableStateOf("") }
|
||||
var showPasswordError by remember { mutableStateOf(false) }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
var confirmPasswordVisible by remember { mutableStateOf(false) }
|
||||
|
||||
InfoDialog(
|
||||
title =
|
||||
if (isRestore) {
|
||||
stringResource(R.string.restore)
|
||||
} else {
|
||||
stringResource(R.string.backup)
|
||||
},
|
||||
confirmText =
|
||||
if (isRestore) {
|
||||
stringResource(R.string.restore)
|
||||
} else {
|
||||
stringResource(R.string.backup)
|
||||
},
|
||||
onAttest = {
|
||||
if (!isRestore && encrypt && password != confirmPassword) {
|
||||
showPasswordError = true
|
||||
return@InfoDialog
|
||||
}
|
||||
if (encrypt && password.isBlank()) {
|
||||
return@InfoDialog
|
||||
}
|
||||
|
||||
val finalPassword = if (encrypt) password else null
|
||||
onConfirm(encrypt, finalPassword)
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
body = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(stringResource(R.string.encrypted))
|
||||
ThemedSwitch(checked = encrypt, onClick = { encrypt = it })
|
||||
}
|
||||
|
||||
if (encrypt) {
|
||||
CustomTextField(
|
||||
value = password,
|
||||
onValueChange = {
|
||||
password = it
|
||||
showPasswordError = false
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
label = { Text(stringResource(R.string.password)) },
|
||||
visualTransformation =
|
||||
if (passwordVisible) VisualTransformation.None
|
||||
else PasswordVisualTransformation(),
|
||||
trailing = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (passwordVisible) Icons.Outlined.VisibilityOff
|
||||
else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (passwordVisible) stringResource(R.string.hide_password)
|
||||
else stringResource(R.string.show_password),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
if (!isRestore) {
|
||||
CustomTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = {
|
||||
confirmPassword = it
|
||||
showPasswordError = false
|
||||
},
|
||||
label = { Text(stringResource(R.string.confirm_password)) },
|
||||
visualTransformation =
|
||||
if (confirmPasswordVisible) VisualTransformation.None
|
||||
else PasswordVisualTransformation(),
|
||||
trailing = {
|
||||
IconButton(
|
||||
onClick = { confirmPasswordVisible = !confirmPasswordVisible }
|
||||
) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (confirmPasswordVisible) Icons.Outlined.VisibilityOff
|
||||
else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (confirmPasswordVisible)
|
||||
stringResource(R.string.hide_password)
|
||||
else stringResource(R.string.show_password),
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = showPasswordError,
|
||||
)
|
||||
}
|
||||
|
||||
if (showPasswordError) {
|
||||
Text(
|
||||
text = stringResource(R.string.passwords_do_not_match),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
+2
-2
@@ -56,7 +56,7 @@ fun TunnelGlobalsScreen(
|
||||
)
|
||||
},
|
||||
enabled = sharedUiState.tunnelMode != TunnelMode.PROXY,
|
||||
title = stringResource(R.string.global_split_tunneling),
|
||||
title = stringResource(R.string.splt_tunneling),
|
||||
trailing = { modifier ->
|
||||
SwitchWithDivider(
|
||||
checked = uiState.settings.isGlobalSplitTunnelEnabled,
|
||||
@@ -82,7 +82,7 @@ fun TunnelGlobalsScreen(
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Description, contentDescription = null) },
|
||||
title = stringResource(R.string.configuration_globals),
|
||||
title = stringResource(R.string.tunnel_configuration),
|
||||
onClick = {
|
||||
uiState.globalTunnelConfig?.let {
|
||||
navController.push(Route.ConfigGlobal(id = it.id))
|
||||
|
||||
+6
-4
@@ -11,7 +11,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||
import androidx.compose.material.icons.filled.AppShortcut
|
||||
import androidx.compose.material.icons.filled.SmartToy
|
||||
import androidx.compose.material.icons.filled.SettingsRemote
|
||||
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.Key
|
||||
@@ -133,20 +133,22 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = koinViewModel()) {
|
||||
onClick = { viewModel.setShortcutsEnabled(it) },
|
||||
)
|
||||
},
|
||||
title = stringResource(R.string.enabled_app_shortcuts),
|
||||
title = stringResource(R.string.app_shortcuts),
|
||||
description = { DescriptionText(stringResource(R.string.app_shortcuts_desc)) },
|
||||
onClick = {
|
||||
viewModel.setShortcutsEnabled(!settingsState.settings.isShortcutsEnabled)
|
||||
},
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
|
||||
leading = { Icon(Icons.Filled.SettingsRemote, contentDescription = null) },
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = settingsState.isRemoteEnabled,
|
||||
onClick = { viewModel.setRemoteEnabled(it) },
|
||||
)
|
||||
},
|
||||
title = stringResource(R.string.enable_remote_app_control),
|
||||
title = stringResource(R.string.remote_control),
|
||||
description = { DescriptionText(stringResource(R.string.remote_control_desc)) },
|
||||
onClick = { viewModel.setRemoteEnabled(!settingsState.isRemoteEnabled) },
|
||||
)
|
||||
AnimatedVisibility(settingsState.isRemoteEnabled) {
|
||||
|
||||
+5
-2
@@ -12,6 +12,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogList
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogsBottomSheet
|
||||
@@ -86,13 +87,15 @@ fun LogsScreen(
|
||||
},
|
||||
onCanceled = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_canceled)
|
||||
StringValue.StringResource(R.string.export_canceled),
|
||||
ToastType.Warning,
|
||||
)
|
||||
showLogsSheet = false
|
||||
},
|
||||
onUnsupported = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_unsupported)
|
||||
StringValue.StringResource(R.string.export_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
showLogsSheet = false
|
||||
},
|
||||
|
||||
+9
-1
@@ -1,10 +1,12 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components
|
||||
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -18,7 +20,13 @@ fun LogList(
|
||||
) {
|
||||
LazyColumn(
|
||||
state = lazyColumnListState,
|
||||
modifier = modifier.padding(horizontal = 12.dp),
|
||||
modifier =
|
||||
modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.scrollbar(
|
||||
state = lazyColumnListState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
itemsIndexed(items = logs, key = { index, _ -> index }) { _, log -> LogItem(log = log) }
|
||||
|
||||
+8
@@ -2,6 +2,8 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
|
||||
@@ -27,6 +29,12 @@ fun AppModeBottomSheet(
|
||||
onDismiss()
|
||||
onAppModeChange(it)
|
||||
},
|
||||
description =
|
||||
when (it) {
|
||||
TunnelMode.VPN -> stringResource(R.string.vpn_desc)
|
||||
TunnelMode.PROXY -> stringResource(R.string.local_proxy_desc)
|
||||
TunnelMode.LOCK_DOWN -> stringResource(R.string.lockdown_desc)
|
||||
},
|
||||
selected = tunnelMode == it,
|
||||
)
|
||||
}
|
||||
|
||||
+6
-1
@@ -19,6 +19,7 @@ import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import org.koin.compose.viewmodel.koinActivityViewModel
|
||||
@@ -45,13 +46,16 @@ fun SecurityScreen(viewModel: SharedAppViewModel = koinActivityViewModel()) {
|
||||
onClick = { viewModel.setScreenRecordingSecurity(it) },
|
||||
)
|
||||
},
|
||||
description = {
|
||||
DescriptionText(stringResource(R.string.screen_recording_protection_desc))
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setScreenRecordingSecurity(!uiState.isScreenRecordingProtectionEnabled)
|
||||
},
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Pin, contentDescription = null) },
|
||||
title = stringResource(R.string.enable_app_lock),
|
||||
title = stringResource(R.string.app_lock),
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = uiState.pinLockEnabled,
|
||||
@@ -64,6 +68,7 @@ fun SecurityScreen(viewModel: SharedAppViewModel = koinActivityViewModel()) {
|
||||
},
|
||||
)
|
||||
},
|
||||
description = { DescriptionText(stringResource(R.string.app_lock_desc)) },
|
||||
onClick = {
|
||||
if (!uiState.pinLockEnabled) {
|
||||
navController.push(Route.Lock)
|
||||
|
||||
+67
-15
@@ -26,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -37,8 +38,10 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
||||
@@ -49,12 +52,13 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.PermissionDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateDialog
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreListing
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreReview
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.orbitmvi.orbit.compose.collectAsState
|
||||
|
||||
@@ -63,11 +67,24 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val supportState by viewModel.collectAsState()
|
||||
|
||||
val clipboardManager = rememberClipboardHelper()
|
||||
|
||||
val issuesUrl = stringResource(R.string.github_url)
|
||||
val izzyUrl = stringResource(R.string.fdroid_url)
|
||||
val telegramUrl = stringResource(R.string.telegram_url)
|
||||
val matrixUrl = stringResource(R.string.matrix_url)
|
||||
val docsUrl = stringResource(R.string.docs_url)
|
||||
val websiteUrl = stringResource(R.string.website_url)
|
||||
val translationUrl = stringResource(R.string.translation_url)
|
||||
val privacyPolicyUrl = stringResource(R.string.privacy_policy_url)
|
||||
val playStoreUrl = "https://play.google.com/store/apps/details?id=${context.packageName}"
|
||||
val playReviewsUrl =
|
||||
"https://play.google.com/store/apps/details?id=${context.packageName}&showAllReviews=true"
|
||||
|
||||
val version = remember {
|
||||
"v${BuildConfig.VERSION_NAME +
|
||||
if(BuildConfig.DEBUG) "-debug" else "" }"
|
||||
@@ -95,6 +112,19 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
PermissionDialog(context = context, onDismiss = { showPermissionDialog = false })
|
||||
}
|
||||
|
||||
fun openWebUrl(url: String) {
|
||||
context.openWebUrl(url).onFailure {
|
||||
scope.launch {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.no_browser_detected),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -115,19 +145,19 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
)
|
||||
SurfaceRow(
|
||||
stringResource(R.string.docs_description),
|
||||
onClick = { context.openWebUrl(context.getString(R.string.docs_url)) },
|
||||
onClick = { openWebUrl(docsUrl) },
|
||||
leading = { Icon(Icons.Outlined.Book, contentDescription = null) },
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
)
|
||||
SurfaceRow(
|
||||
stringResource(R.string.website),
|
||||
onClick = { context.openWebUrl(context.getString(R.string.website_url)) },
|
||||
onClick = { openWebUrl(websiteUrl) },
|
||||
leading = { Icon(Icons.Outlined.Web, contentDescription = null) },
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
)
|
||||
SurfaceRow(
|
||||
stringResource(R.string.translation),
|
||||
onClick = { context.openWebUrl(context.getString(R.string.translation_url)) },
|
||||
onClick = { openWebUrl(translationUrl) },
|
||||
description = { DescriptionText(stringResource(R.string.help_translate)) },
|
||||
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
@@ -141,14 +171,16 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
leading = { Icon(Icons.Outlined.Policy, contentDescription = null) },
|
||||
title = stringResource(R.string.privacy_policy),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.privacy_policy_url)) },
|
||||
onClick = { openWebUrl(privacyPolicyUrl) },
|
||||
)
|
||||
if (BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR) {
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Reviews, contentDescription = null) },
|
||||
title = stringResource(R.string.review),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.launchPlayStoreReview() },
|
||||
onClick = {
|
||||
context.launchPlayStoreReview().onFailure { openWebUrl(playReviewsUrl) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -167,7 +199,7 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
},
|
||||
title = stringResource(R.string.join_matrix),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.matrix_url)) },
|
||||
onClick = { openWebUrl(matrixUrl) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = {
|
||||
@@ -179,7 +211,7 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
},
|
||||
title = stringResource(R.string.join_telegram),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.telegram_url)) },
|
||||
onClick = { openWebUrl(telegramUrl) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = {
|
||||
@@ -191,13 +223,24 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
},
|
||||
title = stringResource(R.string.open_issue),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.openWebUrl(context.getString(R.string.github_url)) },
|
||||
onClick = { openWebUrl(issuesUrl) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Mail, contentDescription = null) },
|
||||
title = stringResource(R.string.email_description),
|
||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||
onClick = { context.launchSupportEmail() },
|
||||
onClick = {
|
||||
context.launchSupportEmail().onFailure {
|
||||
scope.launch {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.no_email_detected),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Column {
|
||||
@@ -222,12 +265,21 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
||||
leading = { Icon(Icons.Outlined.InstallMobile, contentDescription = null) },
|
||||
title = stringResource(R.string.check_for_update),
|
||||
onClick = {
|
||||
if (BuildConfig.DEBUG)
|
||||
return@SurfaceRow context.showToast(R.string.update_check_unsupported)
|
||||
if (BuildConfig.DEBUG) {
|
||||
scope.launch {
|
||||
viewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.update_check_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
)
|
||||
}
|
||||
return@SurfaceRow
|
||||
}
|
||||
when (BuildConfig.FLAVOR) {
|
||||
Constants.GOOGLE_PLAY_FLAVOR -> context.launchPlayStoreListing()
|
||||
Constants.FDROID_FLAVOR ->
|
||||
context.openWebUrl(context.getString(R.string.fdroid_url))
|
||||
Constants.GOOGLE_PLAY_FLAVOR ->
|
||||
context.launchPlayStoreListing().onFailure { openWebUrl(playStoreUrl) }
|
||||
Constants.FDROID_FLAVOR -> openWebUrl(izzyUrl)
|
||||
else -> viewModel.checkForStandaloneUpdate()
|
||||
}
|
||||
},
|
||||
|
||||
+10
-1
@@ -1,10 +1,12 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto
|
||||
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -13,10 +15,17 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.comp
|
||||
|
||||
@Composable
|
||||
fun AddressesScreen() {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.scrollbar(
|
||||
state = scrollState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
) {
|
||||
val clipboard = rememberClipboardHelper()
|
||||
Address.allAddresses.forEach { AddressItem(it) { address -> clipboard.copy(address) } }
|
||||
|
||||
+13
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support.license.component
|
||||
|
||||
import LicenseFileEntry
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -9,11 +10,13 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -24,8 +27,17 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
@Composable
|
||||
fun LicenseList(licenses: List<LicenseFileEntry>) {
|
||||
val context = LocalContext.current
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.scrollbar(
|
||||
state = lazyListState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
state = lazyListState,
|
||||
) {
|
||||
items(licenses) { entry ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
||||
+17
-7
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
@@ -50,11 +51,15 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
rememberFileExportLauncherForResult(
|
||||
onSuccess = { uri -> sharedViewModel.exportSelectedTunnels(uri) },
|
||||
onCanceled = {
|
||||
sharedViewModel.showToast(StringValue.StringResource(R.string.export_canceled))
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_canceled),
|
||||
ToastType.Warning,
|
||||
)
|
||||
},
|
||||
onUnsupported = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.export_unsupported)
|
||||
StringValue.StringResource(R.string.export_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -73,7 +78,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
selectedTunnelsExportLauncher.launch(fileName)
|
||||
} else {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.error_no_file_explorer)
|
||||
StringValue.StringResource(R.string.error_no_file_explorer),
|
||||
ToastType.Error,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -87,7 +93,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
rememberFileImportLauncherForResult(
|
||||
onNoFileExplorer = {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.error_no_file_explorer)
|
||||
StringValue.StringResource(R.string.error_no_file_explorer),
|
||||
ToastType.Error,
|
||||
)
|
||||
},
|
||||
onData = { data -> sharedViewModel.importFromUri(data) },
|
||||
@@ -101,13 +108,15 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
}
|
||||
QRResult.QRMissingPermission -> {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.camera_permission_required)
|
||||
StringValue.StringResource(R.string.camera_permission_required),
|
||||
ToastType.Warning,
|
||||
)
|
||||
}
|
||||
is QRResult.QRSuccess -> {
|
||||
result.content.rawValue?.let { sharedViewModel.importFromQr(it) }
|
||||
?: sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.config_error)
|
||||
StringValue.StringResource(R.string.config_error),
|
||||
ToastType.Error,
|
||||
)
|
||||
}
|
||||
QRResult.QRUserCanceled -> Unit
|
||||
@@ -119,7 +128,8 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
||||
->
|
||||
if (!isGranted) {
|
||||
sharedViewModel.showSnackMessage(
|
||||
StringValue.StringResource(R.string.camera_permission_required)
|
||||
StringValue.StringResource(R.string.camera_permission_required),
|
||||
ToastType.Warning,
|
||||
)
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
|
||||
+3
@@ -42,5 +42,8 @@ fun PeerStatisticsSection(peer: ActivePeer) {
|
||||
style = style,
|
||||
color = color,
|
||||
)
|
||||
peer.endpoint?.let {
|
||||
StatText(stringResource(R.string.endpoint_template, it), style = style, color = color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -42,6 +42,6 @@ private fun TransferMetric(icon: ImageVector, text: String, style: TextStyle, co
|
||||
modifier = Modifier.size(12.dp),
|
||||
)
|
||||
|
||||
Text(text = text.lowercase(), style = style, color = color)
|
||||
Text(text = text, style = style, color = color)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-4
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -12,8 +13,10 @@ import androidx.compose.foundation.rememberOverscrollEffect
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Circle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -66,7 +69,11 @@ fun TunnelList(
|
||||
viewModel.clearSelectedTunnels()
|
||||
}
|
||||
}
|
||||
.overscroll(rememberOverscrollEffect()),
|
||||
.overscroll(rememberOverscrollEffect())
|
||||
.scrollbar(
|
||||
state = lazyListState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
state = lazyListState,
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = false,
|
||||
@@ -87,8 +94,7 @@ fun TunnelList(
|
||||
uiState.backendStatus.activeTunnels[tunnel.id] ?: ActiveTunnel()
|
||||
}
|
||||
|
||||
val displayState =
|
||||
uiState.displayStates[tunnel.id] ?: DisplayTunnelState.from(activeTunnel)
|
||||
val displayState = remember(activeTunnel) { DisplayTunnelState.from(activeTunnel) }
|
||||
|
||||
val isRunning = uiState.backendStatus.activeTunnels.containsKey(tunnel.id)
|
||||
|
||||
@@ -107,7 +113,7 @@ fun TunnelList(
|
||||
Icon(
|
||||
Icons.Rounded.Circle,
|
||||
contentDescription = stringResource(R.string.tunnel_monitoring),
|
||||
tint = displayState.asColor(),
|
||||
tint = remember(displayState) { displayState.asColor() },
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
},
|
||||
|
||||
+27
-29
@@ -141,6 +141,10 @@ fun TunnelSettingsScreen(
|
||||
onClick = { navController.push(Route.IPv6(tunnel.id)) },
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val meteredDisabled = sharedUiState.tunnelMode == TunnelMode.PROXY
|
||||
val meteredTunnelDesc =
|
||||
if (meteredDisabled) stringResource(R.string.unavailable_in_mode)
|
||||
else stringResource(R.string.metered_tunnel_desc)
|
||||
SurfaceRow(
|
||||
leading = {
|
||||
Icon(
|
||||
@@ -153,15 +157,9 @@ fun TunnelSettingsScreen(
|
||||
},
|
||||
title = stringResource(R.string.metered_tunnel),
|
||||
enabled = sharedUiState.tunnelMode != TunnelMode.PROXY,
|
||||
description =
|
||||
if (sharedUiState.tunnelMode == TunnelMode.PROXY) {
|
||||
{
|
||||
DescriptionText(
|
||||
stringResource(R.string.unavailable_in_mode),
|
||||
disabled = true,
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
description = {
|
||||
DescriptionText(meteredTunnelDesc, disabled = meteredDisabled)
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = tunnel.isMetered,
|
||||
@@ -172,26 +170,26 @@ fun TunnelSettingsScreen(
|
||||
onClick = { viewModel.onMetered(!tunnel.isMetered) },
|
||||
)
|
||||
}
|
||||
Column {
|
||||
GroupLabel(
|
||||
stringResource(R.string.automation),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
|
||||
title = stringResource(R.string.ddns_auto_update),
|
||||
description = {
|
||||
DescriptionText(stringResource(R.string.ddns_auto_update_description))
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = tunnel.dynamicDnsEnabled,
|
||||
onClick = { viewModel.onDynamicDns(it) },
|
||||
)
|
||||
},
|
||||
onClick = { viewModel.onDynamicDns(!tunnel.dynamicDnsEnabled) },
|
||||
)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
GroupLabel(
|
||||
stringResource(R.string.automation),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
|
||||
title = stringResource(R.string.ddns_auto_update),
|
||||
description = {
|
||||
DescriptionText(stringResource(R.string.ddns_auto_update_description))
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = tunnel.dynamicDnsEnabled,
|
||||
onClick = { viewModel.onDynamicDns(it) },
|
||||
)
|
||||
},
|
||||
onClick = { viewModel.onDynamicDns(!tunnel.dynamicDnsEnabled) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-3
@@ -1,5 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config
|
||||
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -9,6 +10,7 @@ import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -26,14 +28,16 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.components.QrCodeDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigHeaderColor
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigKeyColor
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isTextTooLargeForQr
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||
import org.koin.compose.viewmodel.koinActivityViewModel
|
||||
@@ -57,6 +61,8 @@ fun ConfigScreen(
|
||||
|
||||
var showQrModal by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
val rawConfig by
|
||||
remember(liveConfig, uiState.activeConfig, uiState.tunnel?.quickConfig) {
|
||||
derivedStateOf {
|
||||
@@ -72,7 +78,12 @@ fun ConfigScreen(
|
||||
when (sideEffect) {
|
||||
is LocalSideEffect.Modal.QR -> {
|
||||
if (tunnel.quickConfig.isTextTooLargeForQr()) {
|
||||
context.showToast(R.string.text_too_large_for_qr)
|
||||
sharedViewModel.postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.text_too_large_for_qr),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
showQrModal = true
|
||||
}
|
||||
@@ -90,7 +101,13 @@ fun ConfigScreen(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.scrollbar(
|
||||
state = scrollState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
) {
|
||||
val displayText by
|
||||
remember(rawConfig, showKeys) { derivedStateOf { maskSensitive(rawConfig, showKeys) } }
|
||||
|
||||
+14
-4
@@ -1,5 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.edit
|
||||
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -12,6 +13,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.HdrAuto
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -49,11 +51,12 @@ fun ConfigEditScreen(
|
||||
val uiState by viewModel.collectAsState()
|
||||
|
||||
if (uiState.isLoading) return
|
||||
|
||||
val locale = Locale.current.platformLocale
|
||||
|
||||
var showSelectionDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
sharedViewModel.collectSideEffect { sideEffect ->
|
||||
when (sideEffect) {
|
||||
is LocalSideEffect.SaveChanges -> {
|
||||
@@ -104,7 +107,14 @@ fun ConfigEditScreen(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
|
||||
modifier = Modifier.fillMaxSize().imePadding().verticalScroll(rememberScrollState()),
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.imePadding()
|
||||
.verticalScroll(scrollState)
|
||||
.scrollbar(
|
||||
state = scrollState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
) {
|
||||
if (uiState.isGlobalConfig) {
|
||||
Column {
|
||||
@@ -112,7 +122,7 @@ fun ConfigEditScreen(
|
||||
leading = {
|
||||
Icon(ImageVector.vectorResource(R.drawable.host), contentDescription = null)
|
||||
},
|
||||
title = stringResource(R.string.global_dns_servers),
|
||||
title = stringResource(R.string.dns_servers),
|
||||
trailing = { modifier ->
|
||||
ThemedSwitch(
|
||||
checked = uiState.globalSettings.dnsEnabled,
|
||||
@@ -126,7 +136,7 @@ fun ConfigEditScreen(
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.HdrAuto, contentDescription = null) },
|
||||
title = stringResource(R.string.global_amnezia_configuration),
|
||||
title = stringResource(R.string.amnezia_configuration),
|
||||
trailing = { modifier ->
|
||||
ThemedSwitch(
|
||||
checked = uiState.globalSettings.amneziaEnabled,
|
||||
|
||||
+7
-1
@@ -1,5 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort
|
||||
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -17,6 +18,7 @@ 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.material3.scrollbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -107,7 +109,11 @@ fun SortScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
|
||||
Modifier.pointerInput(Unit) {
|
||||
if (tunnelsUiState.tunnels.isEmpty()) return@pointerInput
|
||||
}
|
||||
.overscroll(rememberOverscrollEffect()),
|
||||
.overscroll(rememberOverscrollEffect())
|
||||
.scrollbar(
|
||||
state = lazyListState.scrollIndicatorState,
|
||||
orientation = Orientation.Vertical,
|
||||
),
|
||||
state = lazyListState,
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = false,
|
||||
|
||||
+17
-50
@@ -26,38 +26,33 @@ sealed class DisplayTunnelState {
|
||||
|
||||
data object Connected : DisplayTunnelState()
|
||||
|
||||
data object Degraded : DisplayTunnelState()
|
||||
data object HandshakeFailure : DisplayTunnelState()
|
||||
|
||||
@StringRes
|
||||
fun labelRes(): Int {
|
||||
return when (this) {
|
||||
fun labelRes(): Int =
|
||||
when (this) {
|
||||
Disconnected -> R.string.tunnel_state_disconnected
|
||||
Connecting -> R.string.tunnel_state_starting
|
||||
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
||||
Connecting,
|
||||
EstablishingConnection -> R.string.tunnel_state_establishing_connection
|
||||
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
||||
Ready -> R.string.ready
|
||||
Connected -> R.string.tunnel_state_connected
|
||||
Degraded -> R.string.tunnel_state_handshake_failure
|
||||
HandshakeFailure -> R.string.tunnel_state_handshake_failure
|
||||
}
|
||||
}
|
||||
|
||||
fun asLocalizedString(context: Context): String {
|
||||
return context.getString(labelRes())
|
||||
}
|
||||
|
||||
fun asColor(): Color {
|
||||
return when (this) {
|
||||
fun asColor(): Color =
|
||||
when (this) {
|
||||
Disconnected -> CoolGray
|
||||
|
||||
Connecting,
|
||||
ResolvingDns,
|
||||
EstablishingConnection,
|
||||
Ready -> Straw
|
||||
|
||||
Connected -> SilverTree
|
||||
|
||||
Degraded -> AlertRed
|
||||
HandshakeFailure -> AlertRed
|
||||
}
|
||||
|
||||
fun asLocalizedString(context: Context): String {
|
||||
return context.getString(labelRes())
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -67,49 +62,21 @@ sealed class DisplayTunnelState {
|
||||
val mode = activeTunnel.mode
|
||||
val isVpnStyle = mode is BackendMode.Vpn || mode is BackendMode.Proxy.KillSwitchPrimary
|
||||
|
||||
// Static peers bootstrap never goes to complete, treat none the same
|
||||
val bootstrapPhaseDone =
|
||||
bootstrap is BootstrapState.Complete || bootstrap is BootstrapState.None
|
||||
|
||||
return when {
|
||||
transport is Tunnel.State.Down -> Disconnected
|
||||
bootstrap is BootstrapState.Failed -> HandshakeFailure
|
||||
|
||||
bootstrap is BootstrapState.Failed -> Degraded
|
||||
|
||||
// DNS resolution still in progress
|
||||
bootstrap is BootstrapState.ResolvingDns ||
|
||||
bootstrap is BootstrapState.UpdatingPeers -> ResolvingDns
|
||||
|
||||
transport is Tunnel.State.Up.Healthy -> Connected
|
||||
|
||||
transport is Tunnel.State.Up.HandshakeFailure -> {
|
||||
val age = System.currentTimeMillis() - activeTunnel.lastStateChangeMs
|
||||
transport is Tunnel.State.Up.HandshakeFailure -> HandshakeFailure
|
||||
|
||||
if (age > 15_000L && bootstrapPhaseDone) {
|
||||
Degraded
|
||||
} else if (isVpnStyle && bootstrapPhaseDone) {
|
||||
EstablishingConnection
|
||||
} else if (bootstrapPhaseDone) {
|
||||
// For regular proxy mode, we go to ready once past bootstrap phase
|
||||
Ready
|
||||
} else {
|
||||
Connecting
|
||||
}
|
||||
}
|
||||
transport is Tunnel.State.Starting ->
|
||||
if (isVpnStyle) EstablishingConnection else Ready
|
||||
|
||||
transport is Tunnel.State.Starting -> {
|
||||
when {
|
||||
bootstrapPhaseDone -> {
|
||||
if (isVpnStyle) EstablishingConnection else Ready
|
||||
}
|
||||
else -> Connecting
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback after bootstrap phase is done
|
||||
bootstrapPhaseDone -> if (isVpnStyle) EstablishingConnection else Ready
|
||||
|
||||
else -> Connecting
|
||||
else -> if (isVpnStyle) EstablishingConnection else Ready
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +18,6 @@ data class GlobalAppUiState(
|
||||
val selectedTunnelCount: Int = 0,
|
||||
val alreadyDonated: Boolean = false,
|
||||
val isPinVerified: Boolean = false,
|
||||
val pendingWgImportUrl: String? = null,
|
||||
val isScreenRecordingProtectionEnabled: Boolean = false,
|
||||
)
|
||||
|
||||
@@ -16,14 +16,14 @@ val ElectricTeal = Color(0xFF4DD0E1)
|
||||
// Status colors
|
||||
val SilverTree = Color(0xFF6DB58B)
|
||||
val AlertRed = Color(0xFFCF6679)
|
||||
|
||||
val Straw = Color(0xFFD4C483)
|
||||
|
||||
val Disabled = CoolGray.copy(alpha = 0.4f)
|
||||
|
||||
// Config colors
|
||||
// Other colors
|
||||
val ConfigHeaderColor = Color(0xFFBB86FC)
|
||||
val ConfigKeyColor = Color(0xFF03DAC5)
|
||||
val Heart = Color(0xFFDB61A2)
|
||||
|
||||
sealed class ThemeColors(
|
||||
val background: Color,
|
||||
|
||||
+15
-30
@@ -14,13 +14,12 @@ import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.service.quicksettings.TileService
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.state.TunnelApp
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
@@ -30,17 +29,11 @@ import java.util.Locale
|
||||
import kotlin.system.exitProcess
|
||||
import timber.log.Timber
|
||||
|
||||
fun Context.openWebUrl(url: String): Result<Unit> {
|
||||
return kotlin
|
||||
.runCatching {
|
||||
val webpage: Uri = url.toUri()
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
.onFailure { showToast(R.string.no_browser_detected) }
|
||||
fun Context.openWebUrl(url: String): Result<Unit> = runCatching {
|
||||
val webpage: Uri = url.toUri()
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, webpage).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun Context.isBatteryOptimizationsDisabled(): Boolean {
|
||||
@@ -109,15 +102,7 @@ fun Context.launchShareFile(file: File) {
|
||||
this.startActivity(chooserIntent)
|
||||
}
|
||||
|
||||
fun Context.showToast(resId: Int) {
|
||||
Toast.makeText(this, this.getString(resId), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
fun Context.showToast(message: String) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
fun Context.launchSupportEmail() {
|
||||
fun Context.launchSupportEmail(): Result<Unit> = runCatching {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
data = "mailto:".toUri()
|
||||
@@ -132,7 +117,7 @@ fun Context.launchSupportEmail() {
|
||||
}
|
||||
)
|
||||
} else {
|
||||
showToast(R.string.no_email_detected)
|
||||
throw IllegalStateException("No email client found")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,9 +237,9 @@ fun Context.installApk(apkFile: File) {
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun Context.launchPlayStoreListing() {
|
||||
fun Context.launchPlayStoreListing(): Result<Unit> = runCatching {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName")).apply {
|
||||
Intent(Intent.ACTION_VIEW, "market://details?id=$packageName".toUri()).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
setPackage("com.android.vending")
|
||||
}
|
||||
@@ -262,12 +247,12 @@ fun Context.launchPlayStoreListing() {
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
openWebUrl("https://play.google.com/store/apps/details?id=$packageName")
|
||||
throw IllegalStateException("Play Store not found")
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.launchPlayStoreReview() {
|
||||
val uri = Uri.parse("market://details?id=$packageName&showAllReviews=true")
|
||||
fun Context.launchPlayStoreReview(): Result<Unit> = runCatching {
|
||||
val uri = "market://details?id=$packageName&showAllReviews=true".toUri()
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, uri).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
@@ -276,7 +261,7 @@ fun Context.launchPlayStoreReview() {
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
openWebUrl("https://play.google.com/store/apps/details?id=$packageName&showAllReviews=true")
|
||||
throw IllegalStateException("Play Store not found")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
|
||||
suspend fun HttpResponse.isHtmlResponse(): Boolean {
|
||||
val contentType = headers["Content-Type"] ?: ""
|
||||
if (contentType.contains("text/html", ignoreCase = true)) return true
|
||||
|
||||
val bodyStart = bodyAsText().trimStart()
|
||||
return bodyStart.startsWith("<!DOCTYPE", ignoreCase = true) ||
|
||||
bodyStart.startsWith("<html", ignoreCase = true)
|
||||
}
|
||||
+1
-1
@@ -61,7 +61,7 @@ fun TunnelMode.asTitleString(context: Context): String {
|
||||
fun TunnelMode.asString(context: Context): String {
|
||||
return when (this) {
|
||||
TunnelMode.VPN -> context.getString(R.string.vpn)
|
||||
TunnelMode.PROXY -> context.getString(R.string.proxy)
|
||||
TunnelMode.PROXY -> context.getString(R.string.local_proxy)
|
||||
TunnelMode.LOCK_DOWN -> context.getString(R.string.lockdown)
|
||||
}
|
||||
}
|
||||
|
||||
+25
-10
@@ -2,13 +2,12 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.tunnel.backend.RootShell
|
||||
import com.zaneschepke.tunnel.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
@@ -16,6 +15,8 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsR
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AutoTunnelUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -99,7 +100,10 @@ class AutoTunnelViewModel(
|
||||
val trimmed = name.trim()
|
||||
if (state.autoTunnelSettings.trustedNetworkSSIDs.contains(name)) {
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.error_ssid_exists))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_ssid_exists),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
setTrustedNetworkNames(
|
||||
@@ -153,11 +157,19 @@ class AutoTunnelViewModel(
|
||||
when (method) {
|
||||
WifiDetectionMethod.ROOT -> {
|
||||
val accepted = RootShell.requestRootPermission()
|
||||
val message =
|
||||
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
||||
else StringValue.StringResource(R.string.root_accepted)
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
if (!accepted) return@intent
|
||||
if (!accepted)
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_root_denied),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.root_accepted),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
WifiDetectionMethod.SHIZUKU -> {
|
||||
requestShizuku()
|
||||
@@ -188,7 +200,10 @@ class AutoTunnelViewModel(
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.shizuku_not_detected))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.shizuku_not_detected),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-6
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.MimicMode
|
||||
@@ -93,7 +94,6 @@ class ConfigEditViewModel(
|
||||
tunnel = tunnel,
|
||||
tunnels = tunnels,
|
||||
isRunning = isRunning,
|
||||
globalSettings = globalSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,10 @@ class ConfigEditViewModel(
|
||||
if (state.isTunnelNameTaken) {
|
||||
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.tunnel_name_taken))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.tunnel_name_taken),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
|
||||
return@intent
|
||||
@@ -114,7 +117,10 @@ class ConfigEditViewModel(
|
||||
|
||||
if (state.draft.tunnelName.isBlank()) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.name_error_empty))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.name_error_empty),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@intent
|
||||
}
|
||||
@@ -148,8 +154,9 @@ class ConfigEditViewModel(
|
||||
}
|
||||
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.config_changes_saved)
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -165,7 +172,7 @@ class ConfigEditViewModel(
|
||||
else -> StringValue.StringResource(R.string.unknown_error)
|
||||
}
|
||||
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message, ToastType.Error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.DnsSettingsCoordinator
|
||||
@@ -9,7 +10,6 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsReposito
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DnsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.DnsValidator
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
@@ -70,7 +70,7 @@ class DnsViewModel(
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(result.error.labelRes()),
|
||||
type = SnackbarType.WARNING,
|
||||
type = ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@intent
|
||||
@@ -87,7 +87,10 @@ class DnsViewModel(
|
||||
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
@@ -52,7 +53,10 @@ class LockdownViewModel(
|
||||
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
@@ -61,7 +62,10 @@ class LoggerViewModel(
|
||||
fun exportLogs(uri: Uri?) = intent {
|
||||
if (uri == null) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.export_unsupported))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.export_unsupported),
|
||||
ToastType.Warning,
|
||||
)
|
||||
)
|
||||
return@intent
|
||||
}
|
||||
@@ -76,11 +80,12 @@ class LoggerViewModel(
|
||||
Timber.e(action)
|
||||
intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(
|
||||
R.string.export_failed,
|
||||
": ${action.localizedMessage}",
|
||||
)
|
||||
),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+27
-17
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
@@ -27,21 +28,26 @@ class ProxySettingsViewModel(
|
||||
combine(tunnelCoordinator.backendStatus, proxySettingsRepository.flow) {
|
||||
backendStatus,
|
||||
settings ->
|
||||
ProxySettingsUiState(
|
||||
proxySettings = settings,
|
||||
backendStatus = backendStatus,
|
||||
isLoading = false,
|
||||
socks5Enabled = settings.socks5ProxyEnabled,
|
||||
httpEnabled = settings.httpProxyEnabled,
|
||||
socksBindAddress = settings.socks5ProxyBindAddress ?: "",
|
||||
httpBindAddress = settings.httpProxyBindAddress ?: "",
|
||||
proxyUsername = settings.proxyUsername ?: "",
|
||||
proxyPassword = settings.proxyPassword ?: "",
|
||||
)
|
||||
if (state.isLoading) {
|
||||
ProxySettingsUiState(
|
||||
proxySettings = settings,
|
||||
backendStatus = backendStatus,
|
||||
isLoading = false,
|
||||
socks5Enabled = settings.socks5ProxyEnabled,
|
||||
httpEnabled = settings.httpProxyEnabled,
|
||||
socksBindAddress = settings.socks5ProxyBindAddress ?: "",
|
||||
httpBindAddress = settings.httpProxyBindAddress ?: "",
|
||||
proxyUsername = settings.proxyUsername ?: "",
|
||||
proxyPassword = settings.proxyPassword ?: "",
|
||||
)
|
||||
} else {
|
||||
state.copy(backendStatus = backendStatus)
|
||||
}
|
||||
}
|
||||
.collect { reduce { it } }
|
||||
}
|
||||
|
||||
// TODO add a dialog requesting restart if any tunnels active
|
||||
fun save() = intent {
|
||||
reduce { state.copy(showSaveModal = false) }
|
||||
|
||||
@@ -74,7 +80,8 @@ class ProxySettingsViewModel(
|
||||
if (socksPort == null || httpPort == null || socksPort == httpPort) {
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.ports_must_differ)
|
||||
StringValue.StringResource(R.string.ports_must_differ),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -91,18 +98,21 @@ class ProxySettingsViewModel(
|
||||
|
||||
if (updated.proxyPassword?.any { it.isWhitespace() } == true) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.password_no_spaces))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.password_no_spaces),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@intent reduce { state.copy(isPasswordError = true) }
|
||||
}
|
||||
|
||||
proxySettingsRepository.upsert(updated)
|
||||
|
||||
tunnelCoordinator.toggleTunnels()
|
||||
tunnelCoordinator.toggleTunnels()
|
||||
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
|
||||
+15
-6
@@ -1,7 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zaneschepke.tunnel.backend.RootShell
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.tunnel.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
@@ -96,11 +97,19 @@ class SettingsViewModel(
|
||||
fun setTunnelScriptedEnabled(to: Boolean) = intent {
|
||||
if (to) {
|
||||
val accepted = RootShell.requestRootPermission()
|
||||
val message =
|
||||
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
||||
else StringValue.StringResource(R.string.root_accepted)
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
if (!accepted) return@intent
|
||||
if (!accepted)
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_root_denied),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.root_accepted),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
}
|
||||
settingsRepository.upsert(state.settings.copy(tunnelScriptingEnabled = to))
|
||||
}
|
||||
|
||||
+71
-32
@@ -3,11 +3,10 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelModeCoordinator
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
@@ -17,7 +16,10 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepo
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.parser.ConfigParseException
|
||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.GlobalAppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
@@ -27,6 +29,7 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.QuickConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelName
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isHtmlResponse
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
import io.ktor.client.HttpClient
|
||||
@@ -71,10 +74,16 @@ class SharedAppViewModel(
|
||||
tunnelRepository.userTunnelsFlow,
|
||||
tunnelCoordinator.backendStatus,
|
||||
selectedTunnelsRepository.flow,
|
||||
tunnelCoordinator.tunnelDisplayStates,
|
||||
) { tunnels, backendStatus, selectedTuns, displayStates ->
|
||||
) { tunnels, backendStatus, selectedTuns ->
|
||||
val sortedTunnels = tunnels.sortedBy { it.position }
|
||||
|
||||
val displayStates =
|
||||
backendStatus.activeTunnels.mapValues { (_, activeTunnel) ->
|
||||
DisplayTunnelState.from(activeTunnel)
|
||||
}
|
||||
|
||||
TunnelsUiState(
|
||||
tunnels = tunnels,
|
||||
tunnels = sortedTunnels,
|
||||
backendStatus = backendStatus,
|
||||
displayStates = displayStates,
|
||||
selectedTunnels = selectedTuns,
|
||||
@@ -170,6 +179,10 @@ class SharedAppViewModel(
|
||||
appStateRepository.setShouldShowDonationSnackbar(to)
|
||||
}
|
||||
|
||||
fun showSnackMessage(message: StringValue, type: ToastType) = intent {
|
||||
postGlobalSideEffect(GlobalSideEffect.Snackbar(message, type))
|
||||
}
|
||||
|
||||
suspend fun postSideEffect(globalSideEffect: GlobalSideEffect) {
|
||||
globalEffectRepository.post(globalSideEffect)
|
||||
}
|
||||
@@ -180,12 +193,6 @@ class SharedAppViewModel(
|
||||
globalEffectRepository.post(sideEffect)
|
||||
}
|
||||
|
||||
fun showSnackMessage(message: StringValue) = intent {
|
||||
postGlobalSideEffect(GlobalSideEffect.Snackbar(message))
|
||||
}
|
||||
|
||||
fun showToast(message: StringValue) = intent { postSideEffect(GlobalSideEffect.Toast(message)) }
|
||||
|
||||
fun disableBatteryOptimizationsShown() = intent {
|
||||
appStateRepository.setBatteryOptimizationDisableShown(true)
|
||||
}
|
||||
@@ -193,14 +200,20 @@ class SharedAppViewModel(
|
||||
fun saveSortChanges(tunnels: List<TunnelConfig>) = intent {
|
||||
tunnelRepository.saveAll(tunnels.mapIndexed { index, conf -> conf.copy(position = index) })
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
|
||||
fun sortByLatency(tunnels: List<TunnelConfig>) = intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.pinging_servers))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.pinging_servers),
|
||||
ToastType.Info,
|
||||
)
|
||||
)
|
||||
val sortedResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -242,12 +255,17 @@ class SharedAppViewModel(
|
||||
TunnelConfig.tunnelConfFromQuick(config, name)
|
||||
}
|
||||
tunnelRepository.saveTunnelsUniquely(tunnelConfigs, state.tunnelNames.map { it.value })
|
||||
} catch (_: IOException) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.read_failed))
|
||||
)
|
||||
} catch (e: ConfigParseException) {
|
||||
postSideEffect(GlobalSideEffect.Snackbar(e.asStringValue()))
|
||||
} catch (e: Exception) {
|
||||
if (e is ConfigParseException) {
|
||||
postSideEffect(GlobalSideEffect.Snackbar(e.asStringValue(), ToastType.Error))
|
||||
} else {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_error),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,22 +275,38 @@ class SharedAppViewModel(
|
||||
|
||||
fun importFromQr(conf: String) = intent { importFromClipboard(conf) }
|
||||
|
||||
fun promptWgImport(url: String) = intent { reduce { state.copy(pendingWgImportUrl = url) } }
|
||||
|
||||
fun dismissWgImport() = intent { reduce { state.copy(pendingWgImportUrl = null) } }
|
||||
|
||||
fun importFromUrl(url: String) = intent {
|
||||
reduce { state.copy(pendingWgImportUrl = null) }
|
||||
|
||||
try {
|
||||
httpClient.prepareGet(url).execute { response ->
|
||||
if (response.status.value in 200..299) {
|
||||
val body = response.bodyAsText()
|
||||
importFromClipboard(body)
|
||||
} else {
|
||||
throw IOException(
|
||||
"Failed to download file with error status: ${response.status.value}"
|
||||
)
|
||||
if (response.status.value !in 200..299) {
|
||||
throw IOException("Server returned error: ${response.status.value}")
|
||||
}
|
||||
|
||||
if (response.isHtmlResponse()) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_invalid_config_url),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@execute
|
||||
}
|
||||
val body = response.bodyAsText()
|
||||
importFromClipboard(body)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.error_download_failed))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_download_failed),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -287,7 +321,7 @@ class SharedAppViewModel(
|
||||
is IOException -> StringValue.StringResource(R.string.error_download_failed)
|
||||
else -> StringValue.StringResource(R.string.error_file_extension)
|
||||
}
|
||||
postSideEffect(GlobalSideEffect.Toast(message))
|
||||
postSideEffect(GlobalSideEffect.Snackbar(message, ToastType.Error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +353,8 @@ class SharedAppViewModel(
|
||||
if (selectedTuns.any { activeTunIds?.contains(it.id) == true })
|
||||
return@intent postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.delete_active_message)
|
||||
StringValue.StringResource(R.string.delete_active_message),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
tunnelRepository.delete(selectedTuns)
|
||||
@@ -342,11 +377,12 @@ class SharedAppViewModel(
|
||||
val onFailure = { action: Throwable ->
|
||||
intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(
|
||||
R.string.export_failed,
|
||||
": ${action.localizedMessage}",
|
||||
)
|
||||
),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -363,7 +399,10 @@ class SharedAppViewModel(
|
||||
if (it.exists()) it.delete()
|
||||
}
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.export_success))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.export_success),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
clearSelectedTunnels()
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
|
||||
@@ -88,9 +89,14 @@ class SplitTunnelViewModel(
|
||||
editableInterface.copy(includedApplications = included, excludedApplications = excluded)
|
||||
val updatedProxyConfig = editableConfig.copy(`interface` = updatedInterface)
|
||||
val updatedConfig = updatedProxyConfig.buildConfig()
|
||||
tunnelRepository.save(tunnel.copy(quickConfig = updatedConfig.asQuickString()))
|
||||
tunnelRepository.save(
|
||||
tunnel.copy(quickConfig = updatedConfig.withName(tunnel.name).asQuickString())
|
||||
)
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.config_changes_saved),
|
||||
ToastType.Success,
|
||||
)
|
||||
)
|
||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||
}
|
||||
|
||||
+15
-6
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dokar.sonner.ToastType
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppUpdate
|
||||
@@ -25,7 +26,10 @@ class SupportViewModel(
|
||||
|
||||
fun checkForStandaloneUpdate() = intent {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.checking_for_update))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.checking_for_update),
|
||||
ToastType.Info,
|
||||
)
|
||||
)
|
||||
reduce { state.copy(isLoading = true) }
|
||||
updateRepository
|
||||
@@ -33,15 +37,19 @@ class SupportViewModel(
|
||||
.onSuccess { update ->
|
||||
if (update == null) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.latest_installed)
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.latest_installed),
|
||||
ToastType.Info,
|
||||
)
|
||||
)
|
||||
} else reduce { state.copy(appUpdate = update.sanitized()) }
|
||||
}
|
||||
.onFailure {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(StringValue.StringResource(R.string.update_check_failed))
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.update_check_failed),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
reduce { state.copy(isLoading = false) }
|
||||
@@ -83,8 +91,9 @@ class SupportViewModel(
|
||||
.onSuccess { postSideEffect(GlobalSideEffect.InstallApk(it)) }
|
||||
.onFailure {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Toast(
|
||||
StringValue.StringResource(R.string.update_download_failed)
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.update_download_failed),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -27,7 +26,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -60,7 +58,7 @@
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
@@ -133,7 +131,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -156,7 +153,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -257,7 +253,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -272,7 +267,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
<string name="preshared_key">Předsdílený klíč</string>
|
||||
<string name="seconds">s</string>
|
||||
<string name="cancel">Zrušit</string>
|
||||
<string name="enabled_app_shortcuts">Zapnout zkratky aplikací</string>
|
||||
<string name="unknown_error">Došlo k neznámé chybě</string>
|
||||
<string name="tunnel_on_wifi">Tunelovat na Wi-Fi</string>
|
||||
<string name="email_subject">WG Tunnel podpora</string>
|
||||
@@ -65,7 +64,6 @@
|
||||
<string name="no_browser_detected">Žádný prohlížeč nebyl nalezen</string>
|
||||
<string name="pin_created">PIN úspěšně vytvořen</string>
|
||||
<string name="enter_pin">Zadejte PIN</string>
|
||||
<string name="enable_app_lock">Zapnout zámek aplikace</string>
|
||||
<string name="settings">Nastavení</string>
|
||||
<string name="support">Podpora</string>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
@@ -90,7 +88,6 @@
|
||||
<string name="primary_tunnel">Výchozí tunel</string>
|
||||
<string name="skip">Přeskočit</string>
|
||||
<string name="donate">Přispět na projekt</string>
|
||||
<string name="stop_on_internet_loss">Zastavit tunel při ztrátě internetu</string>
|
||||
<string name="stop_on_no_internet">Zastavit při ztrátě internetu</string>
|
||||
<string name="native_kill_switch">Nativní kill switch</string>
|
||||
<string name="vpn_channel_description">Kanál pro oznámení o stavu VPN</string>
|
||||
@@ -144,7 +141,6 @@
|
||||
<string name="vpn_denied_dialog_title">Oprávnění zamítnuto</string>
|
||||
<string name="app_permission_title">Ovládání tunelů a funkcí automatického tunelování</string>
|
||||
<string name="app_permission_description">https://hosted.weblate.org/translate/wg-tunnel/strings/en/?checksum=e52d7eb2e28a9a12 Ovládání funkcí tunelu a automatického tunelování.</string>
|
||||
<string name="enable_remote_app_control">Povolit vzdálené ovládání aplikace</string>
|
||||
<string name="export_success">Export byl úspěšně dokončen</string>
|
||||
<string name="download">Stáhnout</string>
|
||||
<string name="check_for_update">Zkontrolovat aktualizaci</string>
|
||||
@@ -249,7 +245,7 @@
|
||||
<string name="root_required_template">%1$s (vyžaduje root)</string>
|
||||
<string name="recommended_template">%1$s (doporučeno)</string>
|
||||
<string name="hint_template">(%1$s)</string>
|
||||
<string name="backup_success">Úspěšně zazálohováno. %1$s</string>
|
||||
<string name="backup_success">Úspěšně zazálohováno</string>
|
||||
<string name="config_error_template">Špatná konfigurace. %1$s v umístění: %2$s.</string>
|
||||
<string name="donation_dev_message">Jako jediný vývojář neúnavně pracuji na tom, aby se WG Tunnel stal nejlepším bezplatným a open-source WireGuard klientem pro Android, ale to je možné pouze s vaší podporou.</string>
|
||||
<string name="google_donation_message">Bohužel, kvůli pravidlům společnosti Google nejsou odkazy na darování povoleny ve verzi této aplikace z Obchodu Play. Projděte si prosím webové stránky projektu, abyste zjistili, kde můžete přispět.</string>
|
||||
@@ -276,7 +272,6 @@
|
||||
<string name="back">Zpět</string>
|
||||
<string name="already_donated">Již darováno</string>
|
||||
<string name="selected">Vybrané</string>
|
||||
<string name="global_split_tunneling">Globální dělené tunelování</string>
|
||||
<string name="active_network">Aktivní síť:</string>
|
||||
<string name="delete_active_message">Aktivní tunel nelze odstranit.</string>
|
||||
<string name="help_translate">Pomozte s překladem aplikace</string>
|
||||
@@ -284,7 +279,6 @@
|
||||
<string name="other">Ostatní</string>
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Konfigurace</string>
|
||||
<string name="global_dns_servers">Globální DNS servery</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="backup_and_restore">Zálohování a obnovení</string>
|
||||
<string name="about">O aplikaci</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -29,7 +28,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -62,7 +60,7 @@
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
@@ -135,7 +133,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -158,7 +155,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -258,7 +254,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -273,7 +268,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
<string name="base64_key">base64-Schlüssel</string>
|
||||
<string name="delete_tunnel">Tunnel löschen</string>
|
||||
<string name="persistent_keepalive">Dauerhaftes Keepalive</string>
|
||||
<string name="enable_app_lock">App-Sperre aktivieren</string>
|
||||
<string name="interface_">Schnittstelle</string>
|
||||
<string name="listen_port">Eingehender Port</string>
|
||||
<string name="random">(zufällig)</string>
|
||||
@@ -45,7 +44,6 @@
|
||||
<string name="seconds">Sekunden</string>
|
||||
<string name="cancel">Abbrechen</string>
|
||||
<string name="preshared_key">Geteilter Schlüssel</string>
|
||||
<string name="enabled_app_shortcuts">App-Verknüpfungen aktivieren</string>
|
||||
<string name="tunnel_on_wifi">Tunnel bei WLAN</string>
|
||||
<string name="email_subject">WG Tunnel Unterstützung</string>
|
||||
<string name="docs_description">Dokumentation lesen</string>
|
||||
@@ -125,7 +123,6 @@
|
||||
<string name="pre_down">Vor Deaktivierung</string>
|
||||
<string name="post_down">Nach Deaktivierung</string>
|
||||
<string name="quick_actions">Schnellaktionen</string>
|
||||
<string name="stop_on_internet_loss">Stoppen, wenn die Internetverbindung getrennt wird</string>
|
||||
<string name="bypass_lan_for_kill_switch">LAN umgehen für Notschalter</string>
|
||||
<string name="hide_scripts">Skripte verbergen</string>
|
||||
<string name="enable_amnezia_compatibility">Amnezia Kompatibilität aktivieren</string>
|
||||
@@ -141,6 +138,7 @@
|
||||
<string name="config_error">Ungültige Konfiguration</string>
|
||||
<string name="join_matrix">Matrix-Community beitreten</string>
|
||||
<string name="error_download_failed">Download der Konfiguration fehlgeschlagen</string>
|
||||
<string name="wg_url_confirm_message">Möchtest du wirklich Tunnel von %1$s hinzufügen? Verbinde dich niemals mit einem nicht vertrauenswürdigen VPN!</string>
|
||||
<string name="add_from_url">Von URL hinzufügen</string>
|
||||
<string name="export_logs">Gespeicherte Logs exportieren</string>
|
||||
<string name="app_permission_title">Steuere Tunnel und Auto-Tunnel Funktionen.</string>
|
||||
@@ -153,7 +151,6 @@
|
||||
<string name="copy">Kopieren</string>
|
||||
<string name="service_running_error">Dienst läuft nicht</string>
|
||||
<string name="auth_error">Nicht autorisiert</string>
|
||||
<string name="enable_remote_app_control">App-Fernsteuerung aktivieren</string>
|
||||
<string name="export_failed">Export fehlgeschlagen</string>
|
||||
<string name="app_permission_description">https://hosted.weblate.org/translate/wg-tunnel/strings/en/?checksum=e52d7eb2e28a9a12Steuere Tunnel und Auto-Tunnel Funktionen.</string>
|
||||
<string name="dns_resolve_error">DNS-Auflösung fehlgeschlagen</string>
|
||||
@@ -206,7 +203,7 @@
|
||||
<string name="auto_tunnel_running">Auto-Tunnel läuft</string>
|
||||
<string name="auto_tunnel_not_running">Auto-Tunnel läuft nicht</string>
|
||||
<string name="tunnel_monitoring">Tunnelüberwachung</string>
|
||||
<string name="backup_success">Backuperfolg. %1$s</string>
|
||||
<string name="backup_success">Backuperfolg</string>
|
||||
<string name="restore_success">Wiederherstellerfolg. %1$s</string>
|
||||
<string name="restarting_app">Starte App neu, um Änderungen anzuwenden …</string>
|
||||
<string name="restore_failed">Wiederherstellung aus Backup fehlgeschlagen.</string>
|
||||
@@ -281,14 +278,12 @@
|
||||
<string name="resources">Resourcen</string>
|
||||
<string name="back">Zurück</string>
|
||||
<string name="already_donated">Bereits gespendet</string>
|
||||
<string name="global_split_tunneling">Globales Split-Tunneling</string>
|
||||
<string name="active_network">Aktives Netzwerk:</string>
|
||||
<string name="help_translate">Hilf mit, die App zu übersetzen</string>
|
||||
<string name="ethernet">Ethernet</string>
|
||||
<string name="other">Sonstiges</string>
|
||||
<string name="kill_switch">Notschalter</string>
|
||||
<string name="configuration">Konfiguration</string>
|
||||
<string name="global_dns_servers">Globale DNS Server</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="backup_and_restore">Sichern und Wiederherstellen</string>
|
||||
<string name="about">Über</string>
|
||||
@@ -391,7 +386,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +427,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +460,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
<string name="optional">(opcional)</string>
|
||||
<string name="seconds">Segundos</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="enabled_app_shortcuts">Habilitar acesos directos de app</string>
|
||||
<string name="unknown_error">Error desconocido</string>
|
||||
<string name="email_subject">Ayuda de WG Tunnel</string>
|
||||
<string name="interface_">Interfaz</string>
|
||||
@@ -65,7 +64,6 @@
|
||||
<string name="pin_created">Pin creado con éxito</string>
|
||||
<string name="enter_pin">Introduce tu pin</string>
|
||||
<string name="create_pin">Crear pin</string>
|
||||
<string name="enable_app_lock">Activar el bloqueo de aplicaciones</string>
|
||||
<string name="set_primary_tunnel">Establecer como túnel Principal</string>
|
||||
<string name="edit_tunnel">Editar túnel</string>
|
||||
<string name="settings">Ajustes</string>
|
||||
@@ -121,7 +119,6 @@
|
||||
<string name="learn_more">Saber más</string>
|
||||
<string name="wildcards_active">Comodines activos</string>
|
||||
<string name="stop_on_no_internet">Detener cuando no hay internet</string>
|
||||
<string name="stop_on_internet_loss">Detener túnel cuando se pierda el internet</string>
|
||||
<string name="native_kill_switch">Interruptor de apagado nativo</string>
|
||||
<string name="allow_lan_traffic">Permitir tráfico LAN</string>
|
||||
<string name="bypass_lan_for_kill_switch">Excluir LAN del interruptor de apagado</string>
|
||||
@@ -166,7 +163,6 @@
|
||||
<string name="check_for_update">Comprobar actualización</string>
|
||||
<string name="update_check_failed">Fallo al comprobar la actualización.</string>
|
||||
<string name="flavor_template">Variante: %1$s</string>
|
||||
<string name="enable_remote_app_control">Activar control remoto de la app</string>
|
||||
<string name="export_success">Exportación completada</string>
|
||||
<string name="download">Descargar</string>
|
||||
<string name="latest_installed">Ya se está ejecutando la última versión.</string>
|
||||
@@ -206,7 +202,7 @@
|
||||
<string name="auto_tunnel_running">El túnel automático está activo</string>
|
||||
<string name="auto_tunnel_not_running">El túnel automático no está activo</string>
|
||||
<string name="tunnel_monitoring">Monitoreo del túnel</string>
|
||||
<string name="backup_success">Copia de seguridad realizada con éxito. %1$s</string>
|
||||
<string name="backup_success">Copia de seguridad realizada con éxito</string>
|
||||
<string name="restore_success">Restauración completada. %1$s</string>
|
||||
<string name="restarting_app">Reiniciando la app para aplicar los cambios…</string>
|
||||
<string name="restore_failed">No se pudo restaurar la copia de seguridad.</string>
|
||||
@@ -280,14 +276,12 @@
|
||||
<string name="resources">Recursos</string>
|
||||
<string name="back">Atrás</string>
|
||||
<string name="already_donated">Ya he donado</string>
|
||||
<string name="global_split_tunneling">Túnel dividido global</string>
|
||||
<string name="active_network">Red activa:</string>
|
||||
<string name="help_translate">Ayuda a traducir la app</string>
|
||||
<string name="ethernet">Ethernet</string>
|
||||
<string name="other">Otros</string>
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuración</string>
|
||||
<string name="global_dns_servers">Servidores DNS globales</string>
|
||||
<string name="contact">Contacto</string>
|
||||
<string name="backup_and_restore">Copia de seguridad/Restaurar</string>
|
||||
<string name="about">Acerca de</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
<string name="allow">Luba</string>
|
||||
<string name="select_all">Vali kõik</string>
|
||||
<string name="export_success">Eksportimine õnnestus</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>
|
||||
@@ -151,8 +150,6 @@
|
||||
<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">Luba rakenduse lukustamine</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>
|
||||
@@ -181,7 +178,6 @@
|
||||
<string name="_default">Vaikimisi meetod</string>
|
||||
<string name="wifi_detection_method">WiFi tuvastamise meetod</string>
|
||||
<string name="current_template">Praegune: %1$s</string>
|
||||
<string name="enabled_app_shortcuts">Luba rakenduse otseteed</string>
|
||||
<string name="auto_tunnel_title">Tunneli automaatne loomine</string>
|
||||
<string name="auto_tunnel_not_running">Tunneli automaatne käivitamine pole kasutusel</string>
|
||||
<string name="auto_tunnel_running">Tunneli automaatne käivitamine on kasutusel</string>
|
||||
@@ -198,7 +194,7 @@
|
||||
<string name="recommended_template">%1$s (soovitatav)</string>
|
||||
<string name="hint_template">(%1$s)</string>
|
||||
<string name="tunnel_monitoring">Tunnelo monitooring</string>
|
||||
<string name="backup_success">Varundus õnnestus. %1$s</string>
|
||||
<string name="backup_success">Varundus õnnestus</string>
|
||||
<string name="restore_success">Taastamine õnnestus. %1$s</string>
|
||||
<string name="restarting_app">Muudatuste jõustamiseks taaskäivitan rakenduse…</string>
|
||||
<string name="restore_failed">Varukoopiast taastamine ei õnnestunud.</string>
|
||||
@@ -280,14 +276,12 @@
|
||||
<string name="resources">Ressursid</string>
|
||||
<string name="back">Tagasi</string>
|
||||
<string name="already_donated">Juba annetasin</string>
|
||||
<string name="global_split_tunneling">Üldine jagatud tunneldus</string>
|
||||
<string name="active_network">Aktiivne võrk:</string>
|
||||
<string name="help_translate">Aita seda rakendust tõlkida</string>
|
||||
<string name="ethernet">Ethernet</string>
|
||||
<string name="other">Muu</string>
|
||||
<string name="kill_switch">kiirpeatamine</string>
|
||||
<string name="configuration">Seadistused</string>
|
||||
<string name="global_dns_servers">Üldised nimeserverid</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="backup_and_restore">Varundus ja taastamine</string>
|
||||
<string name="about">Rakenduse teave</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
<string name="save">Save</string>
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -39,7 +38,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -72,7 +70,7 @@
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
@@ -143,7 +141,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -165,7 +162,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -259,7 +255,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -274,7 +269,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
<string name="pin_created">Pin luotu</string>
|
||||
<string name="enter_pin">Syötä pin-koodi</string>
|
||||
<string name="create_pin">Luo pin-koodi</string>
|
||||
<string name="enable_app_lock">Ota käyttöön sovelluksen lukitus</string>
|
||||
<string name="set_primary_tunnel">Aseta ensisijaiseksi tunneliksi</string>
|
||||
<string name="edit_tunnel">Muokkaa tunnelia</string>
|
||||
<string name="settings">Asetukset</string>
|
||||
@@ -87,7 +86,6 @@
|
||||
<string name="add_wifi_name">Lisää WIFI:n nimi</string>
|
||||
<string name="language">Kieli</string>
|
||||
<string name="include">Sisällytä</string>
|
||||
<string name="enabled_app_shortcuts">Salli sovelluksen pikakuvakkeet</string>
|
||||
<string name="automatic">Automaattinen</string>
|
||||
<string name="tunnel_name">Tunnelin nimi</string>
|
||||
<string name="restart_at_boot">Käynnistä laitteen käynnistyksen yhteydessä</string>
|
||||
@@ -114,7 +112,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="google_donation_message">Unfortunately, due to Google\'s policies, donation links are not allowed in the Play Store version of this app. Please browse the project\'s webpages to find where to donate.</string>
|
||||
@@ -139,7 +136,7 @@
|
||||
<string name="show_qr">Show QR</string>
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="backup_failed">Failed to create backup.</string>
|
||||
@@ -187,7 +184,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -203,7 +199,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
<string name="join_matrix">Join Matrix community</string>
|
||||
@@ -282,7 +277,6 @@
|
||||
<string name="fix">Fix</string>
|
||||
<string name="tunnel_running_name_message">Name unchangeable while tunnel is active.</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
<string name="show_amnezia_properties">Voir les propriétés d\'Amnezia</string>
|
||||
<string name="never">Jamais</string>
|
||||
<string name="logs">Journaux</string>
|
||||
<string name="enabled_app_shortcuts">Activer les raccourcis</string>
|
||||
<string name="unknown_error">Une erreur inconnue s\'est produite</string>
|
||||
<string name="email_subject">Assistance WG Tunnel</string>
|
||||
<string name="yes">Oui</string>
|
||||
@@ -79,7 +78,6 @@
|
||||
<string name="set_primary_tunnel">Tunnel utilisé quand aucun tunnel favori n\'est défini</string>
|
||||
<string name="auto">(Auto)</string>
|
||||
<string name="pin_created">Code PIN bien créé</string>
|
||||
<string name="enable_app_lock">Activer le verrouillage de l\'appli</string>
|
||||
<string name="edit_tunnel">Éditer le tunnel</string>
|
||||
<string name="settings">Réglages</string>
|
||||
<string name="junk_packet_count">Nombre de paquets indésirables</string>
|
||||
@@ -118,7 +116,6 @@
|
||||
<string name="remove_amnezia_compatibility">Retirer la prise en charge d\'Amnezia</string>
|
||||
<string name="hide_amnezia_properties">Cacher les propriétés d\'Amnezia</string>
|
||||
<string name="stop_on_no_internet">Arrêt en l\'absence d\'internet</string>
|
||||
<string name="stop_on_internet_loss">Couper les tunnels en l\'absence d\'internet</string>
|
||||
<string name="native_kill_switch">Arrêt d\'urgence natif</string>
|
||||
<string name="allow_lan_traffic">Autoriser le trafic LAN</string>
|
||||
<string name="bypass_lan_for_kill_switch">Contourner le LAN en cas d\'arrêt d\'urgence</string>
|
||||
@@ -174,7 +171,7 @@
|
||||
<string name="mimic_quic">Imiter QUIC</string>
|
||||
<string name="show_qr">Afficher le QR</string>
|
||||
<string name="wifi_settings">Paramètres Wi-Fi</string>
|
||||
<string name="backup_success">Sauvegarde réussie. %1$s</string>
|
||||
<string name="backup_success">Sauvegarde réussie</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="backup_failed">Échec de la création de la sauvegarde.</string>
|
||||
<string name="location_permissions">Permissions de localisation</string>
|
||||
@@ -213,7 +210,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Avec toute ma gratitude,</string>
|
||||
<string name="selected">Sélectionné</string>
|
||||
<string name="global_split_tunneling">Tunneling sélectif global</string>
|
||||
<string name="active_network">Réseau actif:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Impossible de supprimer un tunnel actif.</string>
|
||||
@@ -227,7 +223,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Poignée de glissement</string>
|
||||
<string name="global_dns_servers">Serveurs DNS global</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Échec. Les serveurs proxy doivent utiliser des ports différents.</string>
|
||||
<string name="backup_and_restore">Sauvegarde et restauration</string>
|
||||
@@ -287,7 +282,6 @@
|
||||
<string name="fix">Corriger</string>
|
||||
<string name="tunnel_running_name_message">Le nom ne peut pas être modifié tant que le tunnel est actif.</string>
|
||||
<string name="export_failed">L\'exportation a échoué</string>
|
||||
<string name="enable_remote_app_control">Activer le contrôle à distance des applications</string>
|
||||
<string name="donation_closing">Mon rêve serait de travailler à plein temps pour vous sur ce projet.</string>
|
||||
<string name="update_download_failed">La mise à jour n\'a pas pu être téléchargée.</string>
|
||||
<string name="network_name">Réseau:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -27,7 +26,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -60,7 +58,7 @@
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
@@ -134,7 +132,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -157,7 +154,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -259,7 +255,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -274,7 +269,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -390,7 +384,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -432,7 +425,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -466,7 +458,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="delete_tunnel">Alagút törlése</string>
|
||||
<string name="tunnel_mobile_data">Alagút mobiladat-forgalmon</string>
|
||||
<string name="logs">Naplók</string>
|
||||
<string name="enable_app_lock">Alkalmazászár engedélyezése</string>
|
||||
<string name="config_changes_saved">Konfigurációs módosítások mentve.</string>
|
||||
<string name="join_telegram">Csatlakozás a Telegram közösséghez</string>
|
||||
<string name="pin_created">PIN-kód sikeresen létrehozva</string>
|
||||
@@ -29,7 +28,6 @@
|
||||
<string name="remote_key">Távoli kulcs</string>
|
||||
<string name="mobile_data">Mobiladat</string>
|
||||
<string name="use_shell_via_shizuku">Shell használata Shizuku-n keresztül a Wi-Fi információkhoz, így nincs szükség helymeghatározási engedélyre nem rootolt eszközökön</string>
|
||||
<string name="stop_on_internet_loss">Alagút leállítása az internet megszűnésekor</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Alapértelmezett alagút indítása rendszerindításkor</string>
|
||||
<string name="allow_lan_traffic">LAN forgalom engedélyezése</string>
|
||||
@@ -62,7 +60,7 @@
|
||||
<string name="wifi_settings">Wi-Fi beállítások</string>
|
||||
<string name="tunnel_on_wifi">Alagút Wi-Fi-n</string>
|
||||
<string name="add_peer">Peer hozzáadása</string>
|
||||
<string name="backup_success">Sikeres mentés. %1$s</string>
|
||||
<string name="backup_success">Sikeres mentés</string>
|
||||
<string name="persistent_keepalive">Kapcsolatmegőrzés</string>
|
||||
<string name="info">Infó</string>
|
||||
<string name="exclude">Kizárás</string>
|
||||
@@ -135,7 +133,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Hálával,</string>
|
||||
<string name="selected">Kiválasztva</string>
|
||||
<string name="global_split_tunneling">Globális split tunneling</string>
|
||||
<string name="active_network">Aktív hálózat:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Aktív alagút nem törölhető.</string>
|
||||
@@ -158,7 +155,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Konfiguráció</string>
|
||||
<string name="drag_handle">Húzóka</string>
|
||||
<string name="global_dns_servers">Globális DNS szerverek</string>
|
||||
<string name="display_theme">Megjelenítési téma</string>
|
||||
<string name="contact">Kapcsolat</string>
|
||||
<string name="ports_must_differ">Sikertelen. A proxyknak különböző portokat kell használniuk.</string>
|
||||
@@ -258,7 +254,6 @@
|
||||
<string name="unavailable_in_mode">Nem érhető el a jelenlegi módban</string>
|
||||
<string name="server_port">Szerver:Port</string>
|
||||
<string name="camera_permission_required">Kamera engedély szükséges</string>
|
||||
<string name="enabled_app_shortcuts">App gyorsindítók engedélyezése</string>
|
||||
<string name="preferred_tunnel">Preferált alagút</string>
|
||||
<string name="allow">Engedélyezés</string>
|
||||
<string name="underload_packet_magic_header">Underload csomag magic header</string>
|
||||
@@ -273,7 +268,6 @@
|
||||
<string name="settings">Beállítások</string>
|
||||
<string name="incorrect_pin">A PIN-kód helytelen</string>
|
||||
<string name="export_failed">Exportálás sikertelen</string>
|
||||
<string name="enable_remote_app_control">Távoli vezérlés engedélyezése</string>
|
||||
<string name="donation_closing">Minden álmom, hogy teljes munkaidőben ezen a projekten dolgozhassak.</string>
|
||||
<string name="update_download_failed">Frissítés letöltése sikertelen.</string>
|
||||
<string name="network_name">Hálózat:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<string name="seconds">Detik</string>
|
||||
<string name="persistent_keepalive">Keepalive persisten</string>
|
||||
<string name="cancel">Batal</string>
|
||||
<string name="enabled_app_shortcuts">Aktifkan pintasan aplikasi</string>
|
||||
<string name="unknown_error">Terjadi kesalahan tidak dikenal</string>
|
||||
<string name="tunnel_on_wifi">Terowongan pada Wi-Fi</string>
|
||||
<string name="email_subject">Dukungan WG Tunnel</string>
|
||||
@@ -64,7 +63,6 @@
|
||||
<string name="prominent_background_location_message">Fitur ini memerlukan izin lokasi latar belakang untuk mengaktifkan pemantauan SSID Wi-Fi bahkan saat aplikasi ditutup. Untuk detail selengkapnya, silakan lihat Kebijakan Privasi yang tertaut di layar Dukungan.</string>
|
||||
<string name="copy_public_key">Salin kunci publik</string>
|
||||
<string name="base64_key">Kunci Base64</string>
|
||||
<string name="enable_app_lock">Aktifkan kunci aplikasi</string>
|
||||
<string name="delete_tunnel">Hapus terowongan</string>
|
||||
<string name="delete_tunnel_message">Apakah Anda yakin ingin menghapus terowongan yang dipilih?</string>
|
||||
<string name="no_email_detected">Tidak ada aplikasi email yang terdeteksi</string>
|
||||
@@ -93,7 +91,6 @@
|
||||
<string name="remote_key">Kunci jarak jauh</string>
|
||||
<string name="mobile_data">Data seluler</string>
|
||||
<string name="use_shell_via_shizuku">Gunakan shell melalui Shizuku untuk mendapatkan informasi Wi-Fi, sehingga tidak memerlukan izin lokasi pada perangkat yang tidak di-root</string>
|
||||
<string name="stop_on_internet_loss">Hentikan terowongan saat koneksi internet terputus</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Mulai terowongan bawaan saat booting</string>
|
||||
<string name="allow_lan_traffic">Izinkan lalu lintas LAN</string>
|
||||
@@ -119,7 +116,7 @@
|
||||
<string name="auto_tunnel_channel_description">Saluran untuk notifikasi status terowongan otomatis</string>
|
||||
<string name="show_qr">Tampilkan QR</string>
|
||||
<string name="wifi_settings">Pengaturan Wi-Fi</string>
|
||||
<string name="backup_success">Pencadangan berhasil. %1$s</string>
|
||||
<string name="backup_success">Pencadangan berhasil</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Kecualikan</string>
|
||||
<string name="backup_failed">Gagal membuat cadangan.</string>
|
||||
@@ -174,7 +171,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Dengan penuh rasa syukur,</string>
|
||||
<string name="selected">Dipilih</string>
|
||||
<string name="global_split_tunneling">Terowongan terpisah global</string>
|
||||
<string name="active_network">Jaringan Aktif:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Tidak dapat menghapus terowongan yang sedang aktif.</string>
|
||||
@@ -192,7 +188,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Konfigurasi</string>
|
||||
<string name="drag_handle">Pegangan Seret</string>
|
||||
<string name="global_dns_servers">Server DNS global</string>
|
||||
<string name="display_theme">Tema tampilan</string>
|
||||
<string name="contact">Kontak</string>
|
||||
<string name="ports_must_differ">Gagal. Proksi harus memiliki port yang berbeda.</string>
|
||||
@@ -279,7 +274,6 @@
|
||||
<string name="fix">Perbaiki</string>
|
||||
<string name="tunnel_running_name_message">Nama tidak dapat diubah saat terowongan aktif.</string>
|
||||
<string name="export_failed">Ekspor gagal</string>
|
||||
<string name="enable_remote_app_control">Aktifkan kontrol aplikasi jarak jauh</string>
|
||||
<string name="donation_closing">Impian saya adalah bekerja penuh waktu untuk Anda pada proyek ini.</string>
|
||||
<string name="update_download_failed">Unduhan pembaruan gagal.</string>
|
||||
<string name="network_name">Jaringan:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<string name="public_key">Chiave pubblica</string>
|
||||
<string name="addresses">Indirizzi</string>
|
||||
<string name="dns_servers">Server DNS</string>
|
||||
<string name="enabled_app_shortcuts">Abilita le scorciatoie da app</string>
|
||||
<string name="email_subject">Supporto di Tunnel WG</string>
|
||||
<string name="email_chooser">Invia una email…</string>
|
||||
<string name="docs_description">Leggi la documentazione</string>
|
||||
@@ -64,7 +63,6 @@
|
||||
<string name="no_browser_detected">Nessun browser rilevato</string>
|
||||
<string name="incorrect_pin">Il PIN non è corretto</string>
|
||||
<string name="pin_created">PIN correttamente creato</string>
|
||||
<string name="enable_app_lock">Abilita blocco app</string>
|
||||
<string name="set_primary_tunnel">Tunnel utilizzato quando non è configurato alcun tunnel preferito</string>
|
||||
<string name="edit_tunnel">Modifica tunnel</string>
|
||||
<string name="settings">Impostazioni</string>
|
||||
@@ -88,7 +86,6 @@
|
||||
<string name="prominent_background_location_message">Questa caratteristica richiede il permesso di localizzazione in background per abilitare il monitoraggio dell\'SSID delle reti Wi-fi anche quando l\'applicazione è chiusa. Per maggiori dettagli, consultare il link alla Privacy Policy presente nella schermata \"Supporto\".</string>
|
||||
<string name="auto_tunnel_title">Servizio tunneling automatico</string>
|
||||
<string name="use_root_shell_for_wifi">Utilizzare una shell root per ottenere le informazioni Wi-Fi, evitando la necessità dell\'autorizzazioni della posizione</string>
|
||||
<string name="stop_on_internet_loss">Arresta il tunnel in caso di assenza di connessione internet</string>
|
||||
<string name="allow_lan_traffic">Abilita traffico LAN</string>
|
||||
<string name="tunnel_control">Controllo tunnel</string>
|
||||
<string name="include_lan">Includi la LAN</string>
|
||||
@@ -158,7 +155,6 @@
|
||||
<string name="service_running_error">Il servizio non è in esecuzione</string>
|
||||
<string name="config_error">Configurazione non valida</string>
|
||||
<string name="dns_resolve_error">Risoluzione DNS fallita</string>
|
||||
<string name="enable_remote_app_control">Abilita il controllo dell\'app da remoto</string>
|
||||
<string name="version_template">Versione: %1$s</string>
|
||||
<string name="flavor_template">Caratteristica: %1$s</string>
|
||||
<string name="update_available">Aggiornamento disponibile!</string>
|
||||
@@ -210,7 +206,7 @@
|
||||
<string name="restore_failed">Impossibile ripristinare dal backup.</string>
|
||||
<string name="restarting_app">Riavviando l\'app per applicare le modifiche…</string>
|
||||
<string name="restore_success">Ripristino riuscito. %1$s</string>
|
||||
<string name="backup_success">Bakcup riuscito. %1$s</string>
|
||||
<string name="backup_success">Bakcup riuscito</string>
|
||||
<string name="current_template">Corrente: %1$s</string>
|
||||
<string name="root_required_template">%1$s (root richiesto)</string>
|
||||
<string name="recommended_template">%1$s (raccomandato)</string>
|
||||
@@ -272,7 +268,6 @@
|
||||
<string name="back">Indietro</string>
|
||||
<string name="active_tunnel_update_failed">Aggiornamento tunnel attivo non riuscito</string>
|
||||
<string name="already_donated">Già donato</string>
|
||||
<string name="global_split_tunneling">Tunneling diviso globale</string>
|
||||
<string name="active_network">Rete attiva:</string>
|
||||
<string name="help_translate">Aiuta a tradurre l\'app</string>
|
||||
<string name="ethernet">Ethernet</string>
|
||||
@@ -280,7 +275,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configurazione</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Server DNS globali</string>
|
||||
<string name="contact">Contatti</string>
|
||||
<string name="backup_and_restore">Backup e ripristino</string>
|
||||
<string name="about">Informazioni</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
<string name="cancel">キャンセル</string>
|
||||
<string name="unknown_error">不明なエラーが発生しました</string>
|
||||
<string name="error_no_file_explorer">ファイルエクスプローラーはインストールされていません</string>
|
||||
<string name="enabled_app_shortcuts">アプリのショートカットを有効にする</string>
|
||||
<string name="no_email_detected">メールアプリは検出されません</string>
|
||||
<string name="email_description">メールを送る</string>
|
||||
<string name="no_browser_detected">ブラウザは検出されません</string>
|
||||
@@ -55,7 +54,6 @@
|
||||
<string name="auto_tunnel_title">自動トンネルサービス</string>
|
||||
<string name="edit_tunnel">トンネルの編集</string>
|
||||
<string name="create_pin">新規PINを作成</string>
|
||||
<string name="enable_app_lock">アプリロックを有効にする</string>
|
||||
<string name="always_on_message">VPN接続の許可が拒否されました。</string>
|
||||
<string name="always_on_message2">他のすべてのアプリで常時接続VPNがオフになっていることを確認して、再度お試しください</string>
|
||||
<string name="notifications">通知</string>
|
||||
@@ -83,7 +81,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -110,7 +107,7 @@
|
||||
<string name="auto_tunnel_channel_description">A channel for auto-tunnel state notifications</string>
|
||||
<string name="show_qr">Show QR</string>
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="backup_failed">Failed to create backup.</string>
|
||||
<string name="junk_packet_minimum_size">Junk packet minimum size</string>
|
||||
@@ -167,7 +164,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -184,7 +180,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -277,7 +272,6 @@
|
||||
<string name="fix">Fix</string>
|
||||
<string name="tunnel_running_name_message">Name unchangeable while tunnel is active.</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
<string name="seconds">წამები</string>
|
||||
<string name="persistent_keepalive">მუდმივი keepalive</string>
|
||||
<string name="cancel">უარყოფა</string>
|
||||
<string name="enabled_app_shortcuts">გააქტიურე აპლიკაციის მალსახმობები</string>
|
||||
<string name="unknown_error">დაფიქსირდა უცნობი შეცდომა</string>
|
||||
<string name="tunnel_on_wifi">ტუნელის გამოტენება Wi-Fi-ს მეშვეობით</string>
|
||||
<string name="email_subject">ვგ ტუნელის დახმარება</string>
|
||||
@@ -61,7 +60,6 @@
|
||||
<string name="pin_created">პინი წარმატებით შეიქმნა</string>
|
||||
<string name="enter_pin">შეიყვანეთ პინი</string>
|
||||
<string name="create_pin">შექმენით პინი</string>
|
||||
<string name="enable_app_lock">გააქტიურე აპლიკაციის დაბლოკვა</string>
|
||||
<string name="restart_at_boot">ჩართვისას დაწყება</string>
|
||||
<string name="vpn_denied_dialog_title">უფლება უარყოფილია</string>
|
||||
<string name="auto_tunnel_channel_id" translatable="false">Auto-tunnel Channel</string>
|
||||
@@ -141,7 +139,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -169,7 +166,7 @@
|
||||
<string name="auto_tunnel_channel_description">A channel for auto-tunnel state notifications</string>
|
||||
<string name="show_qr">Show QR</string>
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="backup_failed">Failed to create backup.</string>
|
||||
<string name="junk_packet_minimum_size">Junk packet minimum size</string>
|
||||
@@ -218,7 +215,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -236,7 +232,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -319,7 +314,6 @@
|
||||
<string name="tunnel_running_name_message">Name unchangeable while tunnel is active.</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<string name="delete_tunnel">터널 삭제</string>
|
||||
<string name="tunnel_mobile_data">모바일 데이터에서 터널 사용</string>
|
||||
<string name="logs">로그</string>
|
||||
<string name="enable_app_lock">앱 잠금 켜기</string>
|
||||
<string name="config_changes_saved">변경한 설정이 저장되었습니다.</string>
|
||||
<string name="join_telegram">Telegram 커뮤니티 참여</string>
|
||||
<string name="pin_created">Pin 생성 성공</string>
|
||||
@@ -27,7 +26,6 @@
|
||||
<string name="remote_key">원격 키</string>
|
||||
<string name="mobile_data">모바일 데이터</string>
|
||||
<string name="use_shell_via_shizuku">Shizuku 셸을 이용하여, 루팅하지 않은 기기에서 위치 권한을 요구하지 않고 Wi-Fi 정보를 얻습니다</string>
|
||||
<string name="stop_on_internet_loss">인터넷이 끊겼을 때 터널 중지</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">부팅 시 기본 터널 시작</string>
|
||||
<string name="allow_lan_traffic">LAN 트래픽 허용</string>
|
||||
@@ -60,7 +58,7 @@
|
||||
<string name="wifi_settings">Wi-Fi 설정</string>
|
||||
<string name="tunnel_on_wifi">Wi-Fi에서 터널 사용</string>
|
||||
<string name="add_peer">피어 추가</string>
|
||||
<string name="backup_success">백업 성공. %1$s</string>
|
||||
<string name="backup_success">백업 성공</string>
|
||||
<string name="persistent_keepalive">지속적 연결 유지</string>
|
||||
<string name="info">정보</string>
|
||||
<string name="exclude">제외</string>
|
||||
@@ -134,7 +132,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">감사를 담아,</string>
|
||||
<string name="selected">선택함</string>
|
||||
<string name="global_split_tunneling">글로벌 분할 터널링</string>
|
||||
<string name="active_network">활성 네트워크:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">활성화된 터널은 삭제할 수 없습니다.</string>
|
||||
@@ -157,7 +154,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">설정</string>
|
||||
<string name="drag_handle">핸들 드래그</string>
|
||||
<string name="global_dns_servers">글로벌 DNS 서버</string>
|
||||
<string name="display_theme">표시 테마</string>
|
||||
<string name="contact">연락</string>
|
||||
<string name="ports_must_differ">실패. 프록시는 다른 포트를 이용해야 합니다.</string>
|
||||
@@ -259,7 +255,6 @@
|
||||
<string name="unavailable_in_mode">현재 모드에서는 이용 불가</string>
|
||||
<string name="server_port">서버:포트</string>
|
||||
<string name="camera_permission_required">카메라 권한 필요</string>
|
||||
<string name="enabled_app_shortcuts">앱 바로가기 켜기</string>
|
||||
<string name="preferred_tunnel">선호 터널</string>
|
||||
<string name="allow">허용</string>
|
||||
<string name="underload_packet_magic_header">언더로드 패킷 매직 헤더</string>
|
||||
@@ -274,7 +269,6 @@
|
||||
<string name="settings">설정</string>
|
||||
<string name="incorrect_pin">Pin이 잘못되었습니다</string>
|
||||
<string name="export_failed">내보내기 실패</string>
|
||||
<string name="enable_remote_app_control">원격 앱 제어 켜기</string>
|
||||
<string name="donation_closing">프로젝트에 모든 시간을 할애하는 것이 제 꿈입니다.</string>
|
||||
<string name="update_download_failed">업데이트 다운로드에 실패했습니다.</string>
|
||||
<string name="network_name">네트워크:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -29,7 +28,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -62,7 +60,7 @@
|
||||
<string name="wifi_settings">Wi-Fi settings</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="backup_success">Backup success. %1$s</string>
|
||||
<string name="backup_success">Backup success</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
@@ -135,7 +133,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -158,7 +155,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -258,7 +254,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -273,7 +268,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="name">Naam</string>
|
||||
<string name="seconds">Seconden</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="enabled_app_shortcuts">App snelkoppelingen inschakelen</string>
|
||||
<string name="random">(willekeurig)</string>
|
||||
<string name="thank_you">Bedankt voor het gebruiken van WG Tunnel!</string>
|
||||
<string name="trusted_ssid_value_description">Verstuur SSID</string>
|
||||
@@ -44,7 +43,6 @@
|
||||
<string name="auto_tunnel_title">Auto-tunnel service</string>
|
||||
<string name="open_issue">Open een melding</string>
|
||||
<string name="create_pin">Maak PIN</string>
|
||||
<string name="enable_app_lock">Schakel app-lock in</string>
|
||||
<string name="init_packet_junk_size">Initiële junk packet grootte</string>
|
||||
<string name="junk_packet_maximum_size">Junk packet maximum grootte</string>
|
||||
<string name="response_packet_junk_size">Response junk packet grootte</string>
|
||||
@@ -110,7 +108,6 @@
|
||||
<string name="enable_amnezia_compatibility">Amnezia compatibiliteit inschakelen</string>
|
||||
<string name="add_from_url">Toevoegen met link</string>
|
||||
<string name="stop_on_no_internet">Stoppen wanneer geen internet</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel bij verlies van internet</string>
|
||||
<string name="wildcards_active">Wildcards actief</string>
|
||||
<string name="use_root_shell_for_wifi">Gebruik root shell om wifi naam te bepalen, zodat locatie toestemmingen niet nodig zijn</string>
|
||||
<string name="start_auto">Start auto-tunnel</string>
|
||||
@@ -154,7 +151,6 @@
|
||||
<string name="auto_tunnel_not_running">Auto-tunnel is niet actief</string>
|
||||
<string name="auth_error">Niet toegelaten</string>
|
||||
<string name="service_running_error">Service niet actief</string>
|
||||
<string name="enable_remote_app_control">Activeer applicatie controle vanop afstand</string>
|
||||
<string name="select_all">Selecteer alles</string>
|
||||
<string name="export_success">Export gelukt</string>
|
||||
<string name="download">Download</string>
|
||||
@@ -226,7 +222,7 @@
|
||||
<string name="website">App website</string>
|
||||
<string name="mimic_quic">Mimic QUIC</string>
|
||||
<string name="wifi_settings">Wi-Fi instellingen</string>
|
||||
<string name="backup_success">Backup succesvol. %1$s</string>
|
||||
<string name="backup_success">Backup succesvol</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="backup_failed">Backup maken mislukt.</string>
|
||||
<string name="unknown">Onbekend</string>
|
||||
@@ -251,7 +247,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Hartelijk bedankt,</string>
|
||||
<string name="selected">Geselecteerd</string>
|
||||
<string name="global_split_tunneling">Globale split tunneling</string>
|
||||
<string name="active_network">Actief Netwerk:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Actieve tunnel kan niet worden verwijderd.</string>
|
||||
@@ -262,7 +257,6 @@
|
||||
<string name="new_tunnel">Nieuwe tunnel</string>
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuratie</string>
|
||||
<string name="global_dns_servers">Globale DNS servers</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Mislukt. Proxyservers moeten verschillende poorten hebben.</string>
|
||||
<string name="backup_and_restore">Backup en herstellen</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</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