Compare commits

..

34 Commits

Author SHA1 Message Date
dependabot[bot] a848fe0cd6 chore(deps): bump androidGradlePlugin from 8.9.2 to 8.10.0
Bumps `androidGradlePlugin` from 8.9.2 to 8.10.0.

Updates `com.android.application` from 8.9.2 to 8.10.0

Updates `com.android.library` from 8.9.2 to 8.10.0

---
updated-dependencies:
- dependency-name: com.android.application
  dependency-version: 8.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.android.library
  dependency-version: 8.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-07 14:02:06 +00:00
Weblate (bot) 6d77ef878d Translations update from Hosted Weblate (#702)
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: kometchtech <kometch@gmail.com>
Co-authored-by: 翻譯得真好下次別翻了 <x86_64-pc-linux-gnu@proton.me>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Kachelkaiser <kachelkaiser@htpst.de>
Co-authored-by: catelixor <catelixor+weblate@proton.me>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Faisal Gull <mail.faisalrehman.345@gmail.com>
Co-authored-by: vm <varga.m007@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: sgauthiertremblay <info@sgauthiertremblay.dev>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Valentin <velentin.s@yandex.ru>
Co-authored-by: adkostatt <adkostatt@gmail.com>
Co-authored-by: VertekPlus <vertekplus@users.noreply.hosted.weblate.org>
Co-authored-by: Jasper <jasper@ennik.com>
Co-authored-by: Tommaso <mrduckhunt@users.noreply.hosted.weblate.org>
Co-authored-by: dct <dct@trnh.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
2025-05-06 20:58:29 -04:00
Zane Schepke a05f11739d chore: bump version with notes 2025-05-01 15:25:09 -04:00
Zane Schepke fe519be5cc refactor: remove use cache setting 2025-05-01 15:16:16 -04:00
dependabot[bot] ab0f67c897 chore(deps): bump com.zaneschepke:wireguard-android from 1.2.16 to 1.3.0 (#772)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-01 14:56:25 -04:00
dependabot[bot] a5639cd129 chore(deps): bump com.google.devtools.ksp from 2.1.20-2.0.0 to 2.1.20-2.0.1 (#774)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-01 14:56:08 -04:00
dependabot[bot] babbab051f chore(deps): bump org.semver4j:semver4j from 5.6.0 to 5.7.0 (#773)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-01 14:55:54 -04:00
dependabot[bot] 2e984e8b6f chore(deps): bump accompanist from 0.37.2 to 0.37.3 (#762)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-01 14:55:42 -04:00
Zane Schepke 044e6da7f5 fix: tunnel connectivity bug (#769) 2025-05-01 14:55:15 -04:00
Zane Schepke 77aa2c30d7 feat: display qr for individual tunnels 2025-04-30 06:23:23 -04:00
Zane Schepke e773238e6b ci: refactor and fix bugs (#767) 2025-04-29 07:31:18 -04:00
Zane Schepke 85316bec3f build: change flavor name to improve clarity 2025-04-29 04:59:22 -04:00
Zane Schepke 1935653309 chore(deps): bump compose, datastore 2025-04-29 04:45:11 -04:00
Zane Schepke e3e24b4a06 fix: cleanup logs on update 2025-04-29 04:37:32 -04:00
Zane Schepke 7af53dcc18 fix: skip ping job for static configured tunnels
#741
2025-04-28 17:35:31 -04:00
Zane Schepke 2eb0ab0f19 fix: vpn permission bug
closes #754
2025-04-28 16:07:01 -04:00
Zane Schepke 07857a53c2 fix: regenerate icon to also trigger auth
closes #757
2025-04-28 15:17:55 -04:00
Zane Schepke 25fd31e252 fix: tunnel lock (#765)
fix: start up logger bug
refactor: switch to bound services
refactor: expose resolved peer endpoint
2025-04-28 15:06:43 -04:00
Zane Schepke 0c90b33813 feat: display Wi-Fi security type for Android 12 and greater
refactor: deprecated clipboard manager
2025-04-25 19:25:06 -04:00
Zane Schepke e6671fd3b4 fix: switching APs or Wi-Fi bands with same SSID bug
#741
closes #154
2025-04-25 16:11:37 -04:00
Zane Schepke 735e38e989 feat: add darker theme options
closes #706
2025-04-25 01:59:57 -04:00
Zane Schepke 90698c2b17 fix: select split tunnel apps should appear at top of list
#662
closes #640
2025-04-25 01:17:14 -04:00
Zane Schepke 245b8ee3e7 ci: sort primary to always be first 2025-04-25 00:19:35 -04:00
dependabot[bot] 343554407a chore(deps): bump androidx.datastore:datastore-preferences from 1.1.4 to 1.1.5 (#748)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 00:12:22 -04:00
dependabot[bot] b493d83730 chore(deps): bump androidx.compose:compose-bom from 2025.04.00 to 2025.04.01 (#747)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 00:12:14 -04:00
dependabot[bot] 53cd717340 chore(deps): bump ClementTsang/delete-tag-and-release from 0.3.1 to 0.4.0 (#738)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 00:11:52 -04:00
dependabot[bot] 76574e3dd2 chore(deps): bump androidx.work:work-runtime-ktx from 2.10.0 to 2.10.1 (#746)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 00:11:16 -04:00
dependabot[bot] 282a752389 chore(deps): bump roomVersion from 2.7.0 to 2.7.1 (#745)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 00:11:06 -04:00
Zane Schepke 5aa9145361 fix: single click in select mode
closes #739
2025-04-25 00:04:30 -04:00
Zane Schepke 586726c848 ci: fix multiple artifacts 2025-04-23 06:32:04 -04:00
Zane Schepke af759a3909 ci: fix removed publish actions 2025-04-23 06:10:18 -04:00
Zane Schepke b467d66554 chore: bump version with notes 2025-04-23 05:59:03 -04:00
Zane Schepke c833e15c8f fix: disable version checker for google, for now 2025-04-23 05:58:42 -04:00
Zane Schepke eec1bbd2f6 ci: fix publish (#737) 2025-04-23 05:27:57 -04:00
130 changed files with 1981 additions and 661 deletions
+10 -10
View File
@@ -1,4 +1,6 @@
name: Build name: build
permissions:
contents: read
on: on:
workflow_dispatch: workflow_dispatch:
@@ -20,7 +22,7 @@ on:
default: fdroid default: fdroid
options: options:
- fdroid - fdroid
- full - standalone
secrets: secrets:
SIGNING_KEY_ALIAS: SIGNING_KEY_ALIAS:
required: false required: false
@@ -94,10 +96,7 @@ jobs:
run: | run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }} store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Create service_account.json
if: ${{ inputs.build_type != 'debug' }}
id: createServiceAccount
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Build APK - name: Build APK
run: | run: |
flavor=${{ inputs.flavor }} flavor=${{ inputs.flavor }}
@@ -119,9 +118,10 @@ jobs:
- name: Get release apk path - name: Get release apk path
id: 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 run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload release apk - name: Upload APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.UPLOAD_DIR_ANDROID }} name: android_artifacts_${{ inputs.flavor }}
path: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }} path: app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/wgtunnel-${{ inputs.flavor }}${{ inputs.flavor == 'fdroid' && '-release' || '' }}-*.apk
retention-days: 1 retention-days: 1
if-no-files-found: warn
+127
View File
@@ -0,0 +1,127 @@
name: nightly
permissions:
contents: write
packages: write
on:
workflow_dispatch:
schedule:
- cron: "4 3 * * *"
jobs:
check_commits:
name: Check for New Commits
runs-on: ubuntu-latest
outputs:
has_new_commits: ${{ steps.check.outputs.new_commits }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Check for new commits
id: check
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build-standalone-nightly:
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: "nightly"
flavor: standalone
publish:
needs:
- check_commits
- build-standalone-nightly
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-nightly
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install system dependencies
run: |
sudo apt update && sudo apt install -y gh apksigner
- name: Set latest tag
uses: rickstaa/action-create-tag@v1
id: tag_creation
with:
tag: "latest"
message: "Automated tag for HEAD commit"
force_push_tag: true
github_token: ${{ secrets.GITHUB_TOKEN }}
tag_exists_error: false
- name: Generate Changelog
id: changelog
uses: requarks/changelog-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
toTag: "nightly"
fromTag: "latest"
writeToFile: false
- name: Make download dir
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v4
with:
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
- name: Set release notes
run: |
echo "RELEASE_NOTES=Nightly build for the latest development version of the app." >> $GITHUB_ENV
- name: Delete previous nightly version
uses: ClementTsang/delete-tag-and-release@v0.4.0
with:
tag_name: "nightly"
delete_release: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get checksum
id: checksum
run: |
file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | head -n 1)
if [ -z "$file_path" ]; then
echo "No APK file found"
exit 1
fi
checksum=$(apksigner verify --print-certs "$file_path" | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")
echo "checksum=$checksum" >> $GITHUB_OUTPUT
- name: Create nightly release
id: create_release
uses: softprops/action-gh-release@v2
with:
body: |
${{ env.RELEASE_NOTES }}
SHA-256 fingerprints for the 4096-bit signing certificate:
```sh
${{ steps.checksum.outputs.checksum }}
```
To verify fingerprint:
```sh
apksigner verify --print-certs [path to APK file] | grep SHA-256
```
### Changelog
${{ steps.changelog.outputs.changes }}
tag_name: nightly
name: nightly
draft: false
prerelease: true
make_latest: false
files: |
${{ github.workspace }}/temp/**/*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2
View File
@@ -1,4 +1,6 @@
name: on-pr name: on-pr
permissions:
contents: read
on: on:
workflow_dispatch: workflow_dispatch:
+101 -78
View File
@@ -1,8 +1,9 @@
name: publish name: publish
permissions:
contents: write
packages: write
on: on:
schedule:
- cron: "4 3 * * *"
workflow_dispatch: workflow_dispatch:
inputs: inputs:
track: track:
@@ -22,95 +23,60 @@ on:
options: options:
- none - none
- prerelease - prerelease
- nightly
- release - release
default: release default: release
required: true required: true
tag_name: tag_name:
description: "Tag name for release" description: "Tag name for release"
required: false required: false
default: nightly default: 1.1.1
flavor: flavor:
type: choice type: choice
description: "Product flavor" description: "Product flavor"
required: true required: true
default: full default: standalone
options: options:
- fdroid - fdroid
- full - standalone
workflow_call: workflow_call:
inputs: inputs:
flavor: flavor:
type: string type: string
description: "Product flavor" description: "Product flavor"
required: false required: false
default: full default: standalone
env:
UPLOAD_DIR_ANDROID: android_artifacts
permissions:
contents: write
packages: write
jobs: jobs:
check_commits:
name: Check for New Commits
runs-on: ubuntu-latest
outputs:
has_new_commits: ${{ steps.check.outputs.new_commits }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for new commits
id: check
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build-fdroid: build-fdroid:
if: ${{ inputs.release_type == 'release' || inputs.flavor == 'fdroid' }} if: ${{ inputs.release_type == 'release' || inputs.flavor == 'fdroid' }}
uses: ./.github/workflows/build.yml uses: ./.github/workflows/build.yml
secrets: inherit secrets: inherit
with: with:
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }} build_type: ${{ inputs.release_type }}
flavor: fdroid flavor: fdroid
build-full: build-standalone:
if: ${{ inputs.release_type == 'release' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' || inputs.flavor == 'full' }} if: ${{ inputs.release_type == 'release' || inputs.release_type == 'prerelease' || inputs.flavor == 'standalone' }}
uses: ./.github/workflows/build.yml uses: ./.github/workflows/build.yml
secrets: inherit secrets: inherit
with: with:
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }} build_type: ${{ inputs.release_type }}
flavor: full flavor: standalone
publish: publish:
needs: needs:
- check_commits - build-standalone
- build-full
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-github name: publish-github
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0
ref: main ref: main
- name: Install system dependencies - name: Install system dependencies
run: | run: |
sudo apt update && sudo apt install -y gh apksigner sudo apt update && sudo apt install -y gh apksigner
- name: Set TAG_NAME
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV
elif [ "${{ github.event_name }}" = "schedule" ]; then
echo "TAG_NAME=nightly" >> $GITHUB_ENV
echo "RELEASE_TYPE=nightly" >> $GITHUB_ENV
fi
- name: Set latest tag - name: Set latest tag
uses: rickstaa/action-create-tag@v1 uses: rickstaa/action-create-tag@v1
id: tag_creation id: tag_creation
@@ -126,21 +92,26 @@ jobs:
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
latest: true latest: true
- name: Generate Changelog - name: Generate Changelog
id: changelog id: changelog
uses: requarks/changelog-action@v1 uses: requarks/changelog-action@v1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
toTag: ${{ github.event_name == 'schedule' && 'nightly' || steps.latest_release.outputs.tag_name }} toTag: ${{ steps.latest_release.outputs.tag_name }}
fromTag: "latest" fromTag: "latest"
writeToFile: false writeToFile: false
- name: Make download dir - name: Make download dir
run: mkdir ${{ github.workspace }}/temp run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: ${{ env.UPLOAD_DIR_ANDROID }} pattern: android_artifacts_*
path: ${{ github.workspace }}/temp path: ${{ github.workspace }}/temp
merge-multiple: true
- name: Set version release notes - name: Set version release notes
if: ${{ inputs.release_type == 'release' }} if: ${{ inputs.release_type == 'release' }}
run: | run: |
@@ -150,36 +121,22 @@ jobs:
echo "$RELEASE_NOTES" >> $GITHUB_ENV echo "$RELEASE_NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
- name: On nightly release notes
if: ${{ contains(env.TAG_NAME, 'nightly') }}
run: |
echo "RELEASE_NOTES=Nightly build for the latest development version of the app." >> $GITHUB_ENV
- name: On prerelease release notes - name: On prerelease release notes
if: ${{ inputs.release_type == 'prerelease' }} if: ${{ inputs.release_type == 'prerelease' }}
run: | run: |
echo "RELEASE_NOTES=Testing version of app for specific feature." >> $GITHUB_ENV echo "RELEASE_NOTES=Testing version of app for specific feature." >> $GITHUB_ENV
- name: Delete previous release - name: Get checksum
if: ${{ contains(env.TAG_NAME, 'nightly') || inputs.release_type == 'prerelease' }}
uses: ClementTsang/delete-tag-and-release@v0.3.1
with:
tag_name: ${{ env.TAG_NAME }}
delete_release: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get checksums
id: checksum id: checksum
run: | run: |
checksums="" file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | head -n 1)
for file_path in $(find ${{ github.workspace }}/temp -type f -iname "*.apk"); do if [ -z "$file_path" ]; then
checksum=$(apksigner verify -print-certs $file_path | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]") echo "No APK file found"
checksums="$checksums\n$file_path: $checksum" exit 1
done fi
echo "checksum<<EOF" >> $GITHUB_OUTPUT checksum=$(apksigner verify --print-certs "$file_path" | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")
echo -e "$checksums" >> $GITHUB_OUTPUT echo "checksum=$checksum" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release - name: Create Release
id: create_release id: create_release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
@@ -199,12 +156,78 @@ jobs:
### Changelog ### Changelog
${{ steps.changelog.outputs.changes }} ${{ steps.changelog.outputs.changes }}
tag_name: ${{ env.TAG_NAME }} tag_name: ${{ github.event.inputs.tag_name }}
name: ${{ env.TAG_NAME }} name: ${{ github.event.inputs.tag_name }}
draft: false draft: false
prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || inputs.release_type == 'nightly' }} prerelease: ${{ inputs.release_type == 'prerelease' }}
make_latest: ${{ inputs.release_type == 'release' }} make_latest: ${{ inputs.release_type == 'release' }}
files: | files: |
${{ github.workspace }}/temp/* ${{ github.workspace }}/temp/**/*.apk
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-fdroid-public:
runs-on: ubuntu-latest
needs:
- build-fdroid
if: inputs.release_type == 'release'
steps:
- name: Dispatch update for fdroid repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: wgtunnel/fdroid
event-type: fdroid-update
publish-play:
if: ${{ 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/
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- 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@v1.2
with:
fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
# create keystore path for gradle to read
- name: Create keystore path env var
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Create service_account.json
id: createServiceAccount
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Deploy with fastlane
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2' # Not needed with a .ruby-version file
bundler-cache: true
- name: Distribute app to Prod track 🚀
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane ${{ inputs.track }})
+7 -4
View File
@@ -103,13 +103,16 @@ android {
dimension = "type" dimension = "type"
buildConfigField("String", "FLAVOR", "\"google\"") buildConfigField("String", "FLAVOR", "\"google\"")
} }
create("full") { dimension = "type" } create("standalone") {
dimension = "type"
buildConfigField("String", "FLAVOR", "\"standalone\"")
}
} }
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
} }
kotlinOptions { jvmTarget = Constants.JVM_TARGET } kotlinOptions { jvmTarget = Constants.JVM_TARGET }
buildFeatures { buildFeatures {
@@ -121,7 +124,6 @@ android {
licensee { licensee {
Constants.allowedLicenses.forEach { allow(it) } Constants.allowedLicenses.forEach { allow(it) }
allowUrl(Constants.XZING_LICENSE_URL) allowUrl(Constants.XZING_LICENSE_URL)
allowUrl("https://rafaellins.mit-license.org/2021/")
} }
applicationVariants.all { applicationVariants.all {
@@ -195,6 +197,7 @@ dependencies {
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.zxing.android.embedded) implementation(libs.zxing.android.embedded)
implementation(libs.material.icons.extended) implementation(libs.material.icons.extended)
implementation(libs.androidx.biometric.ktx) implementation(libs.androidx.biometric.ktx)
@@ -207,7 +210,7 @@ dependencies {
implementation(libs.androidx.work.runtime) implementation(libs.androidx.work.runtime)
implementation(libs.androidx.hilt.work) implementation(libs.androidx.hilt.work)
implementation(libs.qrcode.kotlin) implementation(libs.qrose)
implementation(libs.semver4j) implementation(libs.semver4j)
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
+5 -9
View File
@@ -62,6 +62,10 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.App.Start" android:theme="@style/Theme.App.Start"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
tools:replace="screenOrientation" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -78,10 +82,6 @@
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" /> <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
tools:replace="screenOrientation" />
<activity <activity
android:name=".core.shortcut.ShortcutsActivity" android:name=".core.shortcut.ShortcutsActivity"
@@ -166,15 +166,11 @@
<receiver <receiver
android:name=".core.broadcast.RestartReceiver" android:name=".core.broadcast.RestartReceiver"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false">
android:directBootAware="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SCREEN_ON" />
<action android:name="android.intent.action.USER_PRESENT" />
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" /> <action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" /> <action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
@@ -22,7 +22,6 @@ import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -39,7 +38,6 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute import androidx.navigation.toRoute
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
@@ -55,7 +53,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.Loca
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.TunnelAutoTunnelScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.TunnelAutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.scanner.ScannerScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunnelScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
@@ -110,6 +107,7 @@ class MainActivity : AppCompatActivity() {
val isTv = isRunningOnTv() val isTv = isRunningOnTv()
val appUiState by viewModel.uiState.collectAsStateWithLifecycle() val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
val appViewState by viewModel.appViewState.collectAsStateWithLifecycle() val appViewState by viewModel.appViewState.collectAsStateWithLifecycle()
val tunnelError by viewModel.tunnelManager.errorEvents.collectAsStateWithLifecycle(null)
val navController = rememberNavController() val navController = rememberNavController()
val backStackEntry by navController.currentBackStackEntryAsState() val backStackEntry by navController.currentBackStackEntryAsState()
@@ -134,6 +132,7 @@ class MainActivity : AppCompatActivity() {
vpnPermissionDenied = true vpnPermissionDenied = true
} else { } else {
vpnPermissionDenied = false vpnPermissionDenied = false
showVpnPermissionDialog = false
} }
}, },
) )
@@ -151,6 +150,15 @@ class MainActivity : AppCompatActivity() {
viewModel.handleEvent(AppEvent.SetBatteryOptimizeDisableShown) viewModel.handleEvent(AppEvent.SetBatteryOptimizeDisableShown)
} }
LaunchedEffect(tunnelError) {
if (tunnelError == null) return@LaunchedEffect
val message = tunnelError!!.second.toStringRes()
val context = this@MainActivity
snackbar.showSnackbar(
context.getString(R.string.tunnel_error_template, context.getString(message))
)
}
with(appViewState) { with(appViewState) {
LaunchedEffect(isConfigChanged) { LaunchedEffect(isConfigChanged) {
if (isConfigChanged) { if (isConfigChanged) {
@@ -166,21 +174,6 @@ class MainActivity : AppCompatActivity() {
viewModel.handleEvent(AppEvent.MessageShown) viewModel.handleEvent(AppEvent.MessageShown)
} }
} }
LaunchedEffect(appUiState.activeTunnels) {
appUiState.activeTunnels.mapNotNull { (tunnelConf, tunnelState) ->
(tunnelState.status as? TunnelStatus.Error)?.let { error ->
val message = error.error.toStringRes()
val context = this@MainActivity
snackbar.showSnackbar(
context.getString(
R.string.tunnel_error_template,
context.getString(message),
)
)
viewModel.handleEvent(AppEvent.ClearTunnelError(tunnelConf))
}
}
}
LaunchedEffect(popBackStack) { LaunchedEffect(popBackStack) {
if (popBackStack) { if (popBackStack) {
navController.popBackStack() navController.popBackStack()
@@ -214,7 +207,10 @@ class MainActivity : AppCompatActivity() {
WireguardAutoTunnelTheme(theme = appUiState.appState.theme) { WireguardAutoTunnelTheme(theme = appUiState.appState.theme) {
VpnDeniedDialog( VpnDeniedDialog(
showVpnPermissionDialog, showVpnPermissionDialog,
onDismiss = { showVpnPermissionDialog = false }, onDismiss = {
showVpnPermissionDialog = false
vpnPermissionDenied = false
},
) )
Scaffold( Scaffold(
@@ -300,11 +296,10 @@ class MainActivity : AppCompatActivity() {
appUiState.tunnels appUiState.tunnels
.firstOrNull { it.id == args.id } .firstOrNull { it.id == args.id }
?.let { config -> ?.let { config ->
TunnelOptionsScreen(config, viewModel) TunnelOptionsScreen(config, viewModel, appViewState)
} }
} }
composable<Route.Lock> { PinLockScreen(viewModel) } composable<Route.Lock> { PinLockScreen(viewModel) }
composable<Route.Scanner> { ScannerScreen(viewModel) }
composable<Route.KillSwitch> { composable<Route.KillSwitch> {
KillSwitchScreen(appUiState, viewModel) KillSwitchScreen(appUiState, viewModel)
} }
@@ -3,12 +3,12 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@@ -26,20 +26,20 @@ class RestartReceiver : BroadcastReceiver() {
@Inject lateinit var tunnelManager: TunnelManager @Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var logReader: LogReader
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher @Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
Timber.d("RestartReceiver triggered with action: ${intent.action}") Timber.d("RestartReceiver triggered with action: ${intent.action}")
// screen on for Android TV only to help with sleep shutdowns
val isTv = context.isRunningOnTv()
if (intent.action == Intent.ACTION_SCREEN_ON && !isTv) return
if (intent.action == Intent.ACTION_USER_PRESENT && !isTv) return
serviceManager.updateTunnelTile() serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile() serviceManager.updateAutoTunnelTile()
applicationScope.launch(ioDispatcher) { applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get() val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) { if (settings.isRestoreOnBootEnabled) {
if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) { if (
settings.isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null
) {
Timber.d("Starting auto-tunnel on boot/update") Timber.d("Starting auto-tunnel on boot/update")
serviceManager.startAutoTunnel() serviceManager.startAutoTunnel()
} else { } else {
@@ -49,6 +49,7 @@ class RestartReceiver : BroadcastReceiver() {
} else { } else {
Timber.d("Restore on boot disabled, skipping") Timber.d("Restore on boot disabled, skipping")
} }
if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) logReader.deleteAndClearLogs()
} }
} }
} }
@@ -1,10 +1,11 @@
package com.zaneschepke.wireguardautotunnel.core.service package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Service import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.net.VpnService import android.net.VpnService
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import android.os.IBinder
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
@@ -13,7 +14,6 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -37,23 +37,37 @@ constructor(
private val autoTunnelMutex = Mutex() private val autoTunnelMutex = Mutex()
private val _autoTunnelActive = MutableStateFlow(false) private val _tunnelService = MutableStateFlow<TunnelForegroundService?>(null)
val autoTunnelActive = _autoTunnelActive.asStateFlow() private val _autoTunnelService = MutableStateFlow<AutoTunnelService?>(null)
val autoTunnelService = _autoTunnelService.asStateFlow()
var autoTunnelService = CompletableDeferred<AutoTunnelService>() private val tunnelServiceConnection =
var backgroundService = CompletableDeferred<TunnelForegroundService>() object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
private fun <T : Service> startService(cls: Class<T>, background: Boolean) { val binder = service as? TunnelForegroundService.LocalBinder
runCatching { _tunnelService.value = binder?.service
val intent = Intent(context, cls) Timber.d("TunnelForegroundService connected")
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
} }
.onFailure { Timber.e(it) }
} override fun onServiceDisconnected(name: ComponentName) {
_tunnelService.value = null
Timber.d("TunnelForegroundService disconnected")
}
}
private val autoTunnelServiceConnection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as? AutoTunnelService.LocalBinder
_autoTunnelService.value = binder?.service
Timber.d("AutoTunnelService connected")
}
override fun onServiceDisconnected(name: ComponentName) {
_autoTunnelService.value = null
Timber.d("AutoTunnelService disconnected")
}
}
fun hasVpnPermission(): Boolean { fun hasVpnPermission(): Boolean {
return VpnService.prepare(context) == null return VpnService.prepare(context) == null
@@ -63,20 +77,13 @@ constructor(
autoTunnelMutex.withLock { autoTunnelMutex.withLock {
val settings = appDataRepository.settings.get() val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true)) appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
if (autoTunnelService.isCompleted) { if (_autoTunnelService.value != null) return
_autoTunnelActive.update { true } withContext(ioDispatcher) {
return val intent = Intent(context, AutoTunnelService::class.java)
context.startForegroundService(intent)
context.bindService(intent, autoTunnelServiceConnection, Context.BIND_AUTO_CREATE)
withContext(mainDispatcher) { updateAutoTunnelTile() }
} }
runCatching {
autoTunnelService = CompletableDeferred()
startService(AutoTunnelService::class.java, !WireGuardAutoTunnel.isForeground())
_autoTunnelActive.update { true }
}
.onFailure {
Timber.e(it)
_autoTunnelActive.update { false }
}
withContext(mainDispatcher) { updateAutoTunnelTile() }
} }
} }
@@ -84,43 +91,44 @@ constructor(
autoTunnelMutex.withLock { autoTunnelMutex.withLock {
val settings = appDataRepository.settings.get() val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false)) appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
if (!autoTunnelService.isCompleted) return if (_autoTunnelService.value == null) return
runCatching { _autoTunnelService.value?.let { service ->
val service = autoTunnelService.await() service.stop()
service.stop() try {
_autoTunnelActive.update { false } context.unbindService(autoTunnelServiceConnection)
autoTunnelService = CompletableDeferred() } finally {
_tunnelService.value = null
} }
.onFailure { Timber.e(it) } }
withContext(mainDispatcher) { updateAutoTunnelTile() } withContext(mainDispatcher) { updateAutoTunnelTile() }
} }
} }
fun startTunnelForegroundService() { suspend fun startTunnelForegroundService() {
if (backgroundService.isCompleted) return if (_tunnelService.value != null) return
runCatching { withContext(ioDispatcher) {
backgroundService = CompletableDeferred() applicationScope.launch(ioDispatcher) {
startService( val intent = Intent(context, TunnelForegroundService::class.java)
TunnelForegroundService::class.java, context.startForegroundService(intent)
!WireGuardAutoTunnel.isForeground(), context.bindService(intent, tunnelServiceConnection, Context.BIND_AUTO_CREATE)
)
} }
.onFailure { Timber.e(it) } }
} }
suspend fun stopTunnelForegroundService() { fun stopTunnelForegroundService() {
if (!backgroundService.isCompleted) return _tunnelService.value?.let { service ->
runCatching { service.stop()
val service = backgroundService.await() try {
service.stop() context.unbindService(tunnelServiceConnection)
backgroundService = CompletableDeferred() } finally {
_tunnelService.value = null
} }
.onFailure { Timber.e(it) } }
} }
fun toggleAutoTunnel() { fun toggleAutoTunnel() {
applicationScope.launch(ioDispatcher) { applicationScope.launch(ioDispatcher) {
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel() if (_autoTunnelService.value != null) stopAutoTunnel() else startAutoTunnel()
} }
} }
@@ -131,4 +139,12 @@ constructor(
fun updateTunnelTile() { fun updateTunnelTile() {
context.requestTunnelTileServiceStateUpdate() context.requestTunnelTileServiceStateUpdate()
} }
fun handleTunnelServiceDestroy() {
_tunnelService.update { null }
}
fun handleAutoTunnelServiceDestroy() {
_autoTunnelService.update { null }
}
} }
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Notification import android.app.Notification
import android.content.Intent import android.content.Intent
import android.os.Binder
import android.os.IBinder import android.os.IBinder
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
@@ -23,7 +24,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
@@ -64,9 +64,12 @@ class TunnelForegroundService : LifecycleService() {
private val jobsMutex = Mutex() private val jobsMutex = Mutex()
class LocalBinder(val service: TunnelForegroundService) : Binder()
private val binder = LocalBinder(this)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
serviceManager.backgroundService.complete(this)
ServiceCompat.startForeground( ServiceCompat.startForeground(
this@TunnelForegroundService, this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID, NotificationManager.VPN_NOTIFICATION_ID,
@@ -75,14 +78,13 @@ class TunnelForegroundService : LifecycleService() {
) )
} }
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder {
super.onBind(intent) super.onBind(intent)
return null return binder
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
serviceManager.backgroundService.complete(this)
ServiceCompat.startForeground( ServiceCompat.startForeground(
this@TunnelForegroundService, this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID, NotificationManager.VPN_NOTIFICATION_ID,
@@ -163,8 +165,12 @@ class TunnelForegroundService : LifecycleService() {
} else { } else {
pingJobs[tun]?.cancel() // Cancel any stale job pingJobs[tun]?.cancel() // Cancel any stale job
if (tun.isPingEnabled) { if (tun.isPingEnabled) {
pingJobs[tun] = startPingJob(tun) if (tun.isStaticallyConfigured()) {
Timber.d("Started ping job for ${tun.tunName}") Timber.d("Skipping ping for statically configured tunnel")
} else {
pingJobs[tun] = startPingJob(tun)
Timber.d("Started ping job for ${tun.tunName}")
}
} }
} }
} }
@@ -273,7 +279,7 @@ class TunnelForegroundService : LifecycleService() {
} }
override fun onDestroy() { override fun onDestroy() {
serviceManager.backgroundService = CompletableDeferred() serviceManager.handleTunnelServiceDestroy()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
super.onDestroy() super.onDestroy()
} }
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
import android.content.Intent import android.content.Intent
import android.os.Binder
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
@@ -28,7 +29,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
@@ -68,21 +68,23 @@ class AutoTunnelService : LifecycleService() {
private var killSwitchJob: Job? = null private var killSwitchJob: Job? = null
class LocalBinder(val service: AutoTunnelService) : Binder()
private val binder = LocalBinder(this)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
serviceManager.autoTunnelService.complete(this)
launchWatcherNotification() launchWatcherNotification()
} }
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder {
super.onBind(intent) super.onBind(intent)
return null return binder
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId") Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
start() start()
return START_STICKY return START_STICKY
} }
@@ -105,7 +107,7 @@ class AutoTunnelService : LifecycleService() {
} }
override fun onDestroy() { override fun onDestroy() {
serviceManager.autoTunnelService = CompletableDeferred() serviceManager.handleAutoTunnelServiceDestroy()
restoreVpnKillSwitch() restoreVpnKillSwitch()
super.onDestroy() super.onDestroy()
} }
@@ -38,8 +38,8 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for auto tunnel tile") Timber.d("Start listening called for auto tunnel tile")
lifecycleScope.launch { lifecycleScope.launch {
serviceManager.autoTunnelActive.collect { serviceManager.autoTunnelService.collect {
if (it) return@collect setActive() if (it != null) return@collect setActive()
setInactive() setInactive()
} }
} }
@@ -56,7 +56,7 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
lifecycleScope.launch { lifecycleScope.launch {
if (serviceManager.autoTunnelActive.value) { if (serviceManager.autoTunnelService.value != null) {
serviceManager.stopAutoTunnel() serviceManager.stopAutoTunnel()
setInactive() setInactive()
} else { } else {
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
@@ -11,14 +10,11 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@@ -31,6 +27,10 @@ abstract class BaseTunnel(
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
) : TunnelProvider { ) : TunnelProvider {
private val _errorEvents =
MutableSharedFlow<Pair<TunnelConf, BackendError>>(replay = 0, extraBufferCapacity = 1)
override val errorEvents = _errorEvents.asSharedFlow()
private val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap()) private val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap())
private val tunThreads = ConcurrentHashMap<Int, Thread>() private val tunThreads = ConcurrentHashMap<Int, Thread>()
override val activeTunnels = activeTuns.asStateFlow() override val activeTunnels = activeTuns.asStateFlow()
@@ -45,37 +45,34 @@ abstract class BaseTunnel(
abstract fun stopBackend(tunnel: TunnelConf) abstract fun stopBackend(tunnel: TunnelConf)
override suspend fun clearError(tunnelConf: TunnelConf) =
updateTunnelStatus(tunnelConf, TunnelStatus.Down)
override fun hasVpnPermission(): Boolean { override fun hasVpnPermission(): Boolean {
return serviceManager.hasVpnPermission() return serviceManager.hasVpnPermission()
} }
protected suspend fun updateTunnelStatus( protected suspend fun updateTunnelStatus(
tunnelConf: TunnelConf, tunnelConf: TunnelConf,
state: TunnelStatus? = null, status: TunnelStatus? = null,
stats: TunnelStatistics? = null, stats: TunnelStatistics? = null,
) { ) {
tunStatusMutex.withLock { tunStatusMutex.withLock {
activeTuns.update { current -> activeTuns.update { currentTuns ->
val originalConf = current.getKeyById(tunnelConf.id) ?: tunnelConf val originalConf = currentTuns.getKeyById(tunnelConf.id) ?: tunnelConf
val existingState = current.getValueById(tunnelConf.id) ?: TunnelState() val existingState = currentTuns.getValueById(tunnelConf.id) ?: TunnelState()
val newState = state ?: existingState.status val newState = status ?: existingState.status
if (newState == TunnelStatus.Down) { if (newState == TunnelStatus.Down) {
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN") Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
cleanUpTunThread(tunnelConf) cleanUpTunThread(tunnelConf)
current - originalConf currentTuns - originalConf
} else if (existingState.status == newState && stats == null) { } else if (existingState.status == newState && stats == null) {
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState") Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
current currentTuns
} else { } else {
val updated = val updated =
existingState.copy( existingState.copy(
status = newState, status = newState,
statistics = stats ?: existingState.statistics, statistics = stats ?: existingState.statistics,
) )
current + (originalConf to updated) currentTuns + (originalConf to updated)
} }
} }
} }
@@ -117,23 +114,17 @@ abstract class BaseTunnel(
if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels() if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels()
tunMutex.withLock { tunMutex.withLock {
tunThreads[tunnelConf.id] = thread { tunThreads[tunnelConf.id] = thread {
runCatching { runBlocking {
runBlocking { try {
try { Timber.d("Starting tunnel ${tunnelConf.id}...")
Timber.d("Starting tunnel ${tunnelConf.id}...") startTunnelInner(tunnelConf)
startTunnelInner(tunnelConf) Timber.d("Started complete for tunnel ${tunnelConf.name}...")
Timber.d("Started complete for tunnel ${tunnelConf.name}...") } catch (e: InterruptedException) {
} catch (e: BackendError) { Timber.w(
Timber.e(e, "Failed to start tunnel ${tunnelConf.name} userspace") "Tunnel start has been interrupted as ${tunnelConf.name} failed to start"
updateTunnelStatus(tunnelConf, TunnelStatus.Error(e)) )
} catch (e: InterruptedException) {
Timber.w(
"Tunnel start has been interrupted as ${tunnelConf.name} failed to start"
)
}
}
} }
.onFailure { Timber.w("Tunnel start has been interrupted") } }
} }
} }
} }
@@ -147,11 +138,10 @@ abstract class BaseTunnel(
Timber.d("Started for tun ${tunnelConf.id}...") Timber.d("Started for tun ${tunnelConf.id}...")
saveTunnelActiveState(tunnelConf, true) saveTunnelActiveState(tunnelConf, true)
serviceManager.startTunnelForegroundService() serviceManager.startTunnelForegroundService()
} catch (e: BackendException) { } catch (e: BackendError) {
Timber.e(e, "Failed to start backend for ${tunnelConf.name}") Timber.e(e, "Failed to start backend for ${tunnelConf.name}")
val backendError = e.toBackendError() _errorEvents.emit(tunnelConf to e)
updateTunnelStatus(tunnelConf, TunnelStatus.Error(backendError)) updateTunnelStatus(tunnelConf, TunnelStatus.Down)
throw backendError
} }
} }
@@ -163,26 +153,27 @@ abstract class BaseTunnel(
override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) { override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) {
if (tunnelConf == null) return stopActiveTunnels() if (tunnelConf == null) return stopActiveTunnels()
tunMutex.withLock { tunMutex.withLock {
try { if (activeTuns.isStarting(tunnelConf.id))
if (activeTuns.isStarting(tunnelConf.id)) return handleStuckStartingTunnelShutdown(tunnelConf)
return handleStuckStartingTunnelShutdown(tunnelConf) updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason))
updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason)) stopTunnelInner(tunnelConf)
stopTunnelInner(tunnelConf)
} catch (e: BackendError) {
Timber.e(e, "Failed to stop tunnel ${tunnelConf.id}")
updateTunnelStatus(tunnelConf, TunnelStatus.Error(e))
}
} }
} }
private suspend fun stopTunnelInner(tunnelConf: TunnelConf) { private suspend fun stopTunnelInner(tunnelConf: TunnelConf) {
val tunnel = activeTuns.findTunnel(tunnelConf.id) ?: return try {
stopBackend(tunnel) val tunnel = activeTuns.findTunnel(tunnelConf.id) ?: return
saveTunnelActiveState(tunnelConf, false) stopBackend(tunnel)
removeActiveTunnel(tunnel) saveTunnelActiveState(tunnelConf, false)
removeActiveTunnel(tunnel)
} catch (e: BackendError) {
Timber.e(e, "Failed to stop tunnel ${tunnelConf.id}")
_errorEvents.emit(tunnelConf to e)
updateTunnelStatus(tunnelConf, TunnelStatus.Down)
}
} }
private suspend fun handleServiceStateOnChange() { private fun handleServiceStateOnChange() {
if (activeTuns.value.isEmpty() && bouncingTunnelIds.isEmpty()) if (activeTuns.value.isEmpty() && bouncingTunnelIds.isEmpty())
serviceManager.stopTunnelForegroundService() serviceManager.stopTunnelForegroundService()
} }
@@ -193,15 +184,15 @@ abstract class BaseTunnel(
tunThreads[tunnel.id]?.let { tunThreads[tunnel.id]?.let {
if (it.state != Thread.State.TERMINATED) { if (it.state != Thread.State.TERMINATED) {
it.interrupt() it.interrupt()
updateTunnelStatus(tunnel, TunnelStatus.Down)
} else { } else {
Timber.d("Thread already terminated") Timber.d("Thread already terminated")
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}") Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}")
} finally {
updateTunnelStatus(tunnel, TunnelStatus.Down)
} }
cleanUpTunThread(tunnel)
} }
private fun cleanUpTunThread(tunnel: TunnelConf) { private fun cleanUpTunThread(tunnel: TunnelConf) {
@@ -221,7 +212,7 @@ abstract class BaseTunnel(
bouncingTunnelIds[tunnelConf.id] = reason bouncingTunnelIds[tunnelConf.id] = reason
try { try {
stopTunnel(tunnelConf, reason) stopTunnel(tunnelConf, reason)
delay(300L) delay(BOUNCE_DELAY)
startTunnel(tunnelConf) startTunnel(tunnelConf)
} finally { } finally {
bouncingTunnelIds.remove(tunnelConf.id) bouncingTunnelIds.remove(tunnelConf.id)
@@ -235,4 +226,8 @@ abstract class BaseTunnel(
override suspend fun runningTunnelNames(): Set<String> = override suspend fun runningTunnelNames(): Set<String> =
activeTuns.value.keys.map { it.tunName }.toSet() activeTuns.value.keys.map { it.tunName }.toSet()
companion object {
const val BOUNCE_DELAY = 300L
}
} }
@@ -5,6 +5,7 @@ import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.di.Userspace import com.zaneschepke.wireguardautotunnel.di.Userspace
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
@@ -15,6 +16,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
@@ -62,6 +64,9 @@ constructor(
initialValue = emptyMap(), initialValue = emptyMap(),
) )
override val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>>
get() = tunnelProviderFlow.value.errorEvents
override val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason> = override val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason> =
tunnelProviderFlow.value.bouncingTunnelIds tunnelProviderFlow.value.bouncingTunnelIds
@@ -69,10 +74,6 @@ constructor(
return userspaceTunnel.hasVpnPermission() return userspaceTunnel.hasVpnPermission()
} }
override suspend fun clearError(tunnelConf: TunnelConf) {
tunnelProviderFlow.value.clearError(tunnelConf)
}
override suspend fun updateTunnelStatistics(tunnel: TunnelConf) { override suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
tunnelProviderFlow.value.updateTunnelStatistics(tunnel) tunnelProviderFlow.value.updateTunnelStatistics(tunnel)
} }
@@ -1,11 +1,13 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider { interface TunnelProvider {
@@ -46,11 +48,11 @@ interface TunnelProvider {
val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>> val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>>
val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>>
val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason> val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason>
fun hasVpnPermission(): Boolean fun hasVpnPermission(): Boolean
suspend fun clearError(tunnelConf: TunnelConf)
suspend fun updateTunnelStatistics(tunnel: TunnelConf) suspend fun updateTunnelStatistics(tunnel: TunnelConf)
} }
@@ -50,8 +50,9 @@ constructor(
} catch (e: BackendException) { } catch (e: BackendException) {
Timber.e(e, "Failed to stop tunnel ${tunnel.id}") Timber.e(e, "Failed to stop tunnel ${tunnel.id}")
throw e.toBackendError() throw e.toBackendError()
} finally {
handlePreviouslyEnabledVpnKillSwitch()
} }
handlePreviouslyEnabledVpnKillSwitch()
} }
// stop vpn kill switch if we need to resolve DNS for peer endpoints // stop vpn kill switch if we need to resolve DNS for peer endpoints
@@ -69,7 +70,7 @@ constructor(
// restore vpn kill switch if needed // restore vpn kill switch if needed
private fun handlePreviouslyEnabledVpnKillSwitch() { private fun handlePreviouslyEnabledVpnKillSwitch() {
// let auto tunnel handle this if it is active // let auto tunnel handle this if it is active
if (!serviceManager.autoTunnelActive.value) { if (serviceManager.autoTunnelService.value == null) {
previousBackendState?.let { (state, lanEnabled) -> previousBackendState?.let { (state, lanEnabled) ->
Timber.d("Restoring kill switch configuration") Timber.d("Restoring kill switch configuration")
val lan = if (lanEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList() val lan = if (lanEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
@@ -57,7 +57,7 @@ constructor(
withContext(ioDispatcher) { withContext(ioDispatcher) {
Timber.i("Service worker started") Timber.i("Service worker started")
with(appDataRepository.settings.get()) { with(appDataRepository.settings.get()) {
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null)
return@with serviceManager.startAutoTunnel() return@with serviceManager.startAutoTunnel()
if (tunnelManager.activeTunnels.value.isEmpty()) if (tunnelManager.activeTunnels.value.isEmpty())
tunnelManager.restorePreviousState() tunnelManager.restorePreviousState()
@@ -26,7 +26,6 @@ data class TunnelConf(
val pingIp: String? = null, val pingIp: String? = null,
val isEthernetTunnel: Boolean = false, val isEthernetTunnel: Boolean = false,
val isIpv4Preferred: Boolean = true, val isIpv4Preferred: Boolean = true,
val useCache: Boolean = false,
@Transient private var stateChangeCallback: ((Any) -> Unit)? = null, @Transient private var stateChangeCallback: ((Any) -> Unit)? = null,
) : Tunnel, org.amnezia.awg.backend.Tunnel { ) : Tunnel, org.amnezia.awg.backend.Tunnel {
@@ -60,6 +59,10 @@ data class TunnelConf(
return result return result
} }
fun isStaticallyConfigured(): Boolean {
return toAmConfig().peers.all { it.endpoint.get().host.isValidIpv4orIpv6Address() }
}
fun copyWithCallback( fun copyWithCallback(
id: Int = this.id, id: Int = this.id,
tunName: String = this.tunName, tunName: String = this.tunName,
@@ -107,8 +110,6 @@ data class TunnelConf(
override fun isIpv4ResolutionPreferred(): Boolean = isIpv4Preferred override fun isIpv4ResolutionPreferred(): Boolean = isIpv4Preferred
override fun useCache(): Boolean = useCache
override fun onStateChange(newState: org.amnezia.awg.backend.Tunnel.State) { override fun onStateChange(newState: org.amnezia.awg.backend.Tunnel.State) {
stateChangeCallback?.invoke(newState) stateChangeCallback?.invoke(newState)
} }
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.enums package com.zaneschepke.wireguardautotunnel.domain.enums
enum class ConfigType { enum class ConfigType {
AMNEZIA, AM,
WG, WG,
} }
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.enums package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class TunnelStatus { sealed class TunnelStatus {
data class Error(val error: BackendError) : TunnelStatus()
data object Up : TunnelStatus() data object Up : TunnelStatus()
@@ -12,6 +12,7 @@ class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics()
rxBytes = stats.rxBytes, rxBytes = stats.rxBytes,
txBytes = stats.txBytes, txBytes = stats.txBytes,
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis, latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis,
resolvedEndpoint = stats.resolvedEndpoint,
) )
} }
} }
@@ -8,6 +8,7 @@ abstract class TunnelStatistics {
val rxBytes: Long, val rxBytes: Long,
val txBytes: Long, val txBytes: Long,
val latestHandshakeEpochMillis: Long, val latestHandshakeEpochMillis: Long,
val resolvedEndpoint: String,
) )
abstract fun peerStats(peer: Key): PeerStats? abstract fun peerStats(peer: Key): PeerStats?
@@ -12,6 +12,7 @@ class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics
txBytes = peerStats.txBytes, txBytes = peerStats.txBytes,
rxBytes = peerStats.rxBytes, rxBytes = peerStats.rxBytes,
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis, latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis,
resolvedEndpoint = peerStats.resolvedEndpoint,
) )
} }
} }
@@ -29,8 +29,6 @@ sealed class Route {
@Serializable data object Lock : Route() @Serializable data object Lock : Route()
@Serializable data object Scanner : Route()
@Serializable data object License : Route() @Serializable data object License : Route()
@Serializable data class Config(val id: Int) : Route() @Serializable data class Config(val id: Int) : Route()
@@ -2,22 +2,21 @@ package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.indication import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -44,32 +43,25 @@ fun ExpandingRowListItem(
modifier = modifier =
Modifier.animateContentSize() Modifier.animateContentSize()
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(
if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
else Color.Transparent
)
.then( .then(
if (!isTv) { if (!isTv) {
Modifier.combinedClickable( Modifier.combinedClickable(
onClick = onClick, interactionSource = interactionSource,
onLongClick = { indication = ripple(),
haptic.performHapticFeedback(HapticFeedbackType.LongPress) onClick = onClick,
onHold() onLongClick = {
}, haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onDoubleClick = onDoubleClick, onHold()
) },
.indication( onDoubleClick = onDoubleClick,
interactionSource = interactionSource, )
indication = ripple(),
)
} else Modifier } else Modifier
) )
) { ) {
LaunchedEffect(isSelected) {
if (isSelected) {
interactionSource.emit(PressInteraction.Press(Offset.Zero))
} else {
interactionSource.emit(
PressInteraction.Release(PressInteraction.Press(Offset.Zero))
)
}
}
Column { Column {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
@@ -0,0 +1,42 @@
package com.zaneschepke.wireguardautotunnel.ui.common.functions
import android.content.ClipData
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ClipboardHelper(
private val clipboard: Clipboard,
private val coroutineScope: CoroutineScope,
private val dispatcher: CoroutineDispatcher = Dispatchers.Main,
) {
fun copy(text: String, label: String = "") {
coroutineScope.launch(dispatcher) {
val clipData = ClipData.newPlainText(label, text)
clipboard.setClipEntry(ClipEntry(clipData))
}
}
fun paste(onResult: (String?) -> Unit) {
coroutineScope.launch(dispatcher) {
val entry = clipboard.getClipEntry()
val text = entry?.clipData?.getItemAt(0)?.text?.toString()
onResult(text)
}
}
}
@Composable
fun rememberClipboardHelper(
coroutineScope: CoroutineScope = rememberCoroutineScope()
): ClipboardHelper {
val clipboard = LocalClipboard.current
return remember(clipboard, coroutineScope) { ClipboardHelper(clipboard, coroutineScope) }
}
@@ -4,16 +4,7 @@ import android.os.Build
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.*
import androidx.compose.material.icons.rounded.CopyAll
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Download
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.material.icons.rounded.Save
import androidx.compose.material.icons.rounded.SelectAll
import androidx.compose.material.icons.rounded.Stop
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -240,8 +231,17 @@ fun currentNavBackStackEntryAsNavBarState(
showBottom = true, showBottom = true,
topTitle = { tunnel?.name?.let { Text(it) } }, topTitle = { tunnel?.name?.let { Text(it) } },
topTrailing = { topTrailing = {
ActionIconButton(Icons.Rounded.Edit, R.string.edit_tunnel) { Row {
tunnel?.id?.let { navController.navigate(Route.Config(it)) } ActionIconButton(Icons.Rounded.QrCode2, R.string.show_qr) {
tunnel?.id?.let {
viewModel.handleEvent(
AppEvent.SetShowModal(AppViewState.ModalType.QR)
)
}
}
ActionIconButton(Icons.Rounded.Edit, R.string.edit_tunnel) {
tunnel?.id?.let { navController.navigate(Route.Config(it)) }
}
} }
}, },
route = args?.let { Route.TunnelOptions(it.id) }, route = args?.let { Route.TunnelOptions(it.id) },
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AirplanemodeActive import androidx.compose.material.icons.outlined.PublicOff
import androidx.compose.material.icons.outlined.SettingsEthernet import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.SignalCellular4Bar import androidx.compose.material.icons.outlined.SignalCellular4Bar
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -95,7 +95,7 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnEthernet) }, onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnEthernet) },
), ),
SelectionItem( SelectionItem(
leadingIcon = Icons.Outlined.AirplanemodeActive, leadingIcon = Icons.Outlined.PublicOff,
title = { title = {
Text( Text(
stringResource(R.string.stop_on_no_internet), stringResource(R.string.stop_on_no_internet),
@@ -22,16 +22,15 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.networkmonitor.NetworkStatus import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@@ -48,7 +47,7 @@ fun WifiTunnelingItems(
isWifiNameReadable: () -> Boolean, isWifiNameReadable: () -> Boolean,
): List<SelectionItem> { ): List<SelectionItem> {
val context = LocalContext.current val context = LocalContext.current
val clipboard = LocalClipboardManager.current val clipboardHelper = rememberClipboardHelper()
val baseItems = val baseItems =
listOf( listOf(
@@ -71,29 +70,41 @@ fun WifiTunnelingItems(
) )
}, },
description = { description = {
val wifiName by val wifiInfo by
remember(uiState.networkStatus) { remember(uiState.networkStatus) {
derivedStateOf { derivedStateOf {
(uiState.networkStatus as? NetworkStatus.Connected) (uiState.networkStatus as? NetworkStatus.Connected)
?.takeIf { it.wifiConnected } ?.takeIf { it.wifiConnected }
?.wifiSsid .let { Pair(it?.wifiSsid, it?.securityType) }
} }
} }
Text( val (wifiName, securityType) = wifiInfo
text = Column {
wifiName?.let { stringResource(R.string.wifi_name_template, it) } Text(
?: stringResource(R.string.inactive), text =
style = wifiName?.let { stringResource(R.string.wifi_name_template, it) }
MaterialTheme.typography.bodySmall.copy( ?: stringResource(R.string.inactive),
color = MaterialTheme.colorScheme.outline style =
), MaterialTheme.typography.bodySmall.copy(
maxLines = 1, color = MaterialTheme.colorScheme.outline
overflow = TextOverflow.Ellipsis, ),
modifier = maxLines = 1,
Modifier.clickable { overflow = TextOverflow.Ellipsis,
wifiName?.let { clipboard.setText(AnnotatedString(it)) } modifier =
}, Modifier.clickable { wifiName?.let { clipboardHelper.copy(it) } },
) )
securityType?.let {
Text(
text = stringResource(R.string.security_template, it.name),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}, },
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) }, onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) },
), ),
@@ -7,12 +7,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ExportTunnelsBottomSheet import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ExportTunnelsBottomSheet
@@ -29,7 +31,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable @Composable
fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: AppViewModel) { fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: AppViewModel) {
val navController = LocalNavController.current val navController = LocalNavController.current
val clipboard = LocalClipboardManager.current val clipboard = rememberClipboardHelper()
var showUrlImportDialog by remember { mutableStateOf(false) } var showUrlImportDialog by remember { mutableStateOf(false) }
@@ -45,6 +47,17 @@ fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: Ap
onData = { data -> viewModel.handleEvent(AppEvent.ImportTunnelFromFile(data)) }, onData = { data -> viewModel.handleEvent(AppEvent.ImportTunnelFromFile(data)) },
) )
val scanLauncher =
rememberLauncherForActivityResult(
contract = ScanContract(),
onResult = { result ->
{
if (result != null && result.contents.isNotEmpty())
viewModel.handleEvent(AppEvent.ImportTunnelFromQrCode(result.contents))
}
},
)
val requestPermissionLauncher = val requestPermissionLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted
-> ->
@@ -56,7 +69,7 @@ fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: Ap
) )
return@rememberLauncherForActivityResult return@rememberLauncherForActivityResult
} }
navController.navigate(Route.Scanner) scanLauncher.launch(ScanOptions().setDesiredBarcodeFormats(ScanOptions.QR_CODE))
} }
if (appViewState.showModal == AppViewState.ModalType.DELETE) { if (appViewState.showModal == AppViewState.ModalType.DELETE) {
@@ -90,8 +103,9 @@ fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: Ap
requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) requestPermissionLauncher.launch(android.Manifest.permission.CAMERA)
}, },
onClipboardClick = { onClipboardClick = {
clipboard.getText()?.text?.let { clipboard.paste { result ->
viewModel.handleEvent(AppEvent.ImportTunnelFromClipboard(it)) if (result != null)
viewModel.handleEvent(AppEvent.ImportTunnelFromClipboard(result))
} }
}, },
onManualImportClick = { onManualImportClick = {
@@ -93,7 +93,7 @@ fun ExportTunnelsBottomSheet(viewModel: AppViewModel) {
ExportOptionRow( ExportOptionRow(
label = stringResource(R.string.export_tunnels_amnezia), label = stringResource(R.string.export_tunnels_amnezia),
onClick = { onClick = {
exportConfigType = ConfigType.AMNEZIA exportConfigType = ConfigType.AM
if (!isAuthorized && !isTv) { if (!isAuthorized && !isTv) {
showAuthPrompt = true showAuthPrompt = true
} else { } else {
@@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -18,6 +19,7 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.getValueById
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@@ -35,12 +37,19 @@ fun TunnelList(
onToggleTunnel: (TunnelConf, Boolean) -> Unit, onToggleTunnel: (TunnelConf, Boolean) -> Unit,
viewModel: AppViewModel, viewModel: AppViewModel,
) { ) {
val isTv = LocalIsAndroidTV.current
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val collator = Collator.getInstance(Locale.getDefault()) val collator = Collator.getInstance(Locale.getDefault())
val sortedTunnels = val sortedTunnels =
remember(appUiState.tunnels) { remember(appUiState.tunnels) {
appUiState.tunnels.sortedWith(compareBy(collator) { it.tunName }) appUiState.tunnels.sortedWith(
compareBy(
// primary tunnel first
{ !it.isPrimaryTunnel },
{ collator.compare(it.tunName, "") },
)
)
} }
LazyColumn( LazyColumn(
@@ -49,7 +58,7 @@ fun TunnelList(
modifier = modifier =
modifier modifier
.pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput } .pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput }
.overscroll(ScrollableDefaults.overscrollEffect()), .overscroll(rememberOverscrollEffect()),
state = rememberLazyListState(0, appUiState.tunnels.count()), state = rememberLazyListState(0, appUiState.tunnels.count()),
userScrollEnabled = true, userScrollEnabled = true,
reverseLayout = false, reverseLayout = false,
@@ -71,8 +80,12 @@ fun TunnelList(
tunnel = tunnel, tunnel = tunnel,
tunnelState = tunnelState, tunnelState = tunnelState,
onClick = { onClick = {
navController.navigate(Route.TunnelOptions(tunnel.id)) if (selectedTunnels.isNotEmpty() && !isTv) {
viewModel.handleEvent(AppEvent.ClearSelectedTunnels) viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
} else {
navController.navigate(Route.TunnelOptions(tunnel.id))
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
}
}, },
onDoubleClick = { onDoubleClick = {
viewModel.handleEvent(AppEvent.ToggleTunnelStatsExpanded(tunnel.id)) viewModel.handleEvent(AppEvent.ToggleTunnelStatsExpanded(tunnel.id))
@@ -81,6 +94,7 @@ fun TunnelList(
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(it)) viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(it))
}, },
onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) }, onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) },
isTv = isTv,
) )
} }
} }
@@ -26,7 +26,6 @@ import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.util.extensions.asColor import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
@Composable @Composable
@@ -40,9 +39,8 @@ fun TunnelRowItem(
onDoubleClick: () -> Unit, onDoubleClick: () -> Unit,
onToggleSelectedTunnel: (TunnelConf) -> Unit, onToggleSelectedTunnel: (TunnelConf) -> Unit,
onSwitchClick: (Boolean) -> Unit, onSwitchClick: (Boolean) -> Unit,
isTv: Boolean,
) { ) {
val isTv = LocalIsAndroidTV.current
val leadingIconColor = val leadingIconColor =
remember(state) { remember(state) {
if (state.status.isUp()) tunnelState.statistics.asColor() else Color.Gray if (state.status.isUp()) tunnelState.statistics.asColor() else Color.Gray
@@ -8,6 +8,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -21,49 +24,90 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceSt
@Composable @Composable
fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConf: TunnelConf) { fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConf: TunnelConf) {
val config = TunnelConf.configFromAmQuick(tunnelConf.wgQuick) val config = TunnelConf.configFromAmQuick(tunnelConf.wgQuick)
config.peers.forEach { peer -> Column(
Row( modifier = Modifier.fillMaxWidth().padding(start = 45.dp, bottom = 10.dp, end = 10.dp),
modifier = Modifier.fillMaxWidth().padding(end = 10.dp, bottom = 10.dp, start = 45.dp), verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically),
verticalAlignment = Alignment.CenterVertically, horizontalAlignment = Alignment.Start,
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), ) {
) { config.peers.forEach { peer ->
val peerId = peer.publicKey.toBase64().subSequence(0, 3).toString() + "***" val peerId = remember { peer.publicKey.toBase64().subSequence(0, 3).toString() + "***" }
val peerRx = statistics?.peerStats(peer.publicKey)?.rxBytes ?: 0 val endpoint =
val peerTx = statistics?.peerStats(peer.publicKey)?.txBytes ?: 0 remember(statistics) { statistics?.peerStats(peer.publicKey)?.resolvedEndpoint }
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString() val peerRxMB by
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString() remember(statistics) {
val handshake = derivedStateOf {
statistics?.peerStats(peer.publicKey)?.latestHandshakeEpochMillis?.let { statistics
if (it == 0L) { ?.peerStats(peer.publicKey)
stringResource(R.string.never) ?.rxBytes
} else { ?.let { NumberUtils.bytesToMB(it) }
"${NumberUtils.getSecondsBetweenTimestampAndNow(it)} ${stringResource(R.string.sec)}" ?.toThreeDecimalPlaceString()
} }
} ?: stringResource(R.string.never) }
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { val peerTxMB by
remember(statistics) {
derivedStateOf {
statistics
?.peerStats(peer.publicKey)
?.txBytes
?.let { NumberUtils.bytesToMB(it) }
?.toThreeDecimalPlaceString()
}
}
val handshake by
remember(statistics) {
derivedStateOf {
statistics?.peerStats(peer.publicKey)?.latestHandshakeEpochMillis?.let {
if (it == 0L) {
null
} else {
"${NumberUtils.getSecondsBetweenTimestampAndNow(it)}"
}
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
) {
Text( Text(
stringResource(R.string.peer).lowercase() + ": $peerId", stringResource(R.string.peer).lowercase() + ": $peerId",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
) )
Text( Text(
"tx: $peerTxMB MB", stringResource(R.string.handshake) +
": ${if(handshake == null) stringResource(R.string.never) else handshake + " " + stringResource(R.string.sec)}",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
) )
} }
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
) {
Text( Text(
stringResource(R.string.handshake) + ": $handshake", "rx: ${peerRxMB ?: 0.00} MB",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
) )
Text( Text(
"rx: $peerRxMB MB", "tx: ${peerTxMB ?: 0.00} MB",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
) )
} }
if (endpoint != null) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
) {
Text(
"endpoint: $endpoint",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
}
}
} }
} }
} }
@@ -16,16 +16,15 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
@Composable @Composable
@@ -38,7 +37,7 @@ fun InterfaceFields(
onInterfaceChange: (InterfaceProxy) -> Unit, onInterfaceChange: (InterfaceProxy) -> Unit,
) { ) {
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val clipboardManager = LocalClipboardManager.current val clipboardManager = rememberClipboardHelper()
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
@@ -53,8 +52,9 @@ fun InterfaceFields(
if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(), if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = { trailingIcon = {
IconButton( IconButton(
enabled = isAuthenticated, enabled = true,
onClick = { onClick = {
if (!isAuthenticated) return@IconButton showAuthPrompt()
val keypair = com.wireguard.crypto.KeyPair() val keypair = com.wireguard.crypto.KeyPair()
onInterfaceChange( onInterfaceChange(
interfaceState.copy( interfaceState.copy(
@@ -88,9 +88,7 @@ fun InterfaceFields(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
trailingIcon = { trailingIcon = {
IconButton( IconButton(onClick = { clipboardManager.copy(interfaceState.publicKey) }) {
onClick = { clipboardManager.setText(AnnotatedString(interfaceState.publicKey)) }
) {
Icon(Icons.Rounded.ContentCopy, stringResource(R.string.copy_public_key)) Icon(Icons.Rounded.ContentCopy, stringResource(R.string.copy_public_key))
} }
}, },
@@ -1,33 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.scanner
import android.app.Activity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import com.journeyapps.barcodescanner.CompoundBarcodeView
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun ScannerScreen(viewModel: AppViewModel) {
val context = LocalContext.current
val barcodeView = remember {
CompoundBarcodeView(context).apply {
this.initializeFromIntent((context as Activity).intent)
this.setStatusText("")
this.decodeSingle { result ->
result.text?.let { barCodeOrQr ->
viewModel.handleEvent(AppEvent.ImportTunnelFromQrCode(barCodeOrQr))
}
}
}
}
AndroidView(factory = { barcodeView })
DisposableEffect(Unit) {
barcodeView.resume()
onDispose { barcodeView.pause() }
}
}
@@ -1,12 +1,15 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel
import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Box
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@@ -34,21 +37,16 @@ fun SplitTunnelScreen(
appViewModel.handleEvent(AppEvent.PopBackStack(true)) appViewModel.handleEvent(AppEvent.PopBackStack(true))
} }
} }
if (uiState.loading) {
Crossfade( Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
targetState = uiState.loading, CircularProgressIndicator(modifier = Modifier.size(30.dp), strokeWidth = 5.dp)
animationSpec = tween(200),
modifier = Modifier.fillMaxSize(),
) { isLoading ->
if (isLoading) {
SplitTunnelSkeleton()
} else {
SplitTunnelContent(
uiState = uiState,
onSplitOptionChange = viewModel::updateSplitOption,
onAppSelectionToggle = viewModel::toggleAppSelection,
onQueryChange = viewModel::onSearchQuery,
)
} }
} else {
SplitTunnelContent(
uiState = uiState,
onSplitOptionChange = viewModel::updateSplitOption,
onAppSelectionToggle = viewModel::toggleAppSelection,
onQueryChange = viewModel::onSearchQuery,
)
} }
} }
@@ -1,92 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.animation.ShimmerEffect
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@Composable
fun SplitTunnelSkeleton() {
val shimmerBrush = ShimmerEffect()
Column(
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth().padding(top = 24.dp),
) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp).height(45.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
repeat(3) {
Box(
modifier =
Modifier.weight(1f)
.height(45.dp)
.clip(RoundedCornerShape(8.dp))
.background(shimmerBrush)
)
}
}
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp).height(45.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Box(
modifier =
Modifier.height(45.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(shimmerBrush)
)
}
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
contentPadding = PaddingValues(top = 10.dp),
modifier = Modifier.fillMaxWidth(),
) {
items(20) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier =
Modifier.size(iconSize).clip(CircleShape).background(shimmerBrush)
)
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier =
Modifier.height(20.dp)
.weight(1f)
.clip(RoundedCornerShape(4.dp))
.background(shimmerBrush)
)
Spacer(modifier = Modifier.width(16.dp))
Box(modifier = Modifier.size(24.dp).clip(CircleShape).background(shimmerBrush))
}
}
}
}
}
@@ -18,7 +18,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import java.text.Collator import java.text.Collator
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -50,7 +49,6 @@ constructor(
tunnelId?.let { loadInitialState(it) } tunnelId?.let { loadInitialState(it) }
} }
// TODO improve this loading experience
private fun loadInitialState(tunnelId: Int) = private fun loadInitialState(tunnelId: Int) =
viewModelScope.launch { viewModelScope.launch {
val tunnel = tunnelRepository.getById(tunnelId) ?: return@launch val tunnel = tunnelRepository.getById(tunnelId) ?: return@launch
@@ -66,7 +64,7 @@ constructor(
val installedPackages = packages.map { it.packageName }.toSet() val installedPackages = packages.map { it.packageName }.toSet()
// remove uninstalled apps // Remove uninstalled apps
proxyInterface.includedApplications.retainAll { it in installedPackages } proxyInterface.includedApplications.retainAll { it in installedPackages }
proxyInterface.excludedApplications.retainAll { it in installedPackages } proxyInterface.excludedApplications.retainAll { it in installedPackages }
@@ -98,12 +96,13 @@ constructor(
selected, selected,
) )
} }
.sortedWith(compareBy(collator) { it.first.name }) .sortedWith(
compareByDescending<Pair<TunnelApp, Boolean>> { it.second }
.thenBy(collator) { it.first.name }
)
allTunneledApps = tunneledApps allTunneledApps = tunneledApps
delay(500)
_uiState.update { _uiState.update {
SplitTunnelUiState( SplitTunnelUiState(
loading = false, loading = false,
@@ -6,18 +6,53 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.components.* import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.components.*
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AuthorizationPromptWrapper
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable @Composable
fun TunnelOptionsScreen(tunnelConf: TunnelConf, viewModel: AppViewModel) { fun TunnelOptionsScreen(
tunnelConf: TunnelConf,
viewModel: AppViewModel,
appViewState: AppViewState,
) {
val isTv = LocalIsAndroidTV.current
var showAuthPrompt by remember { mutableStateOf(!isTv) }
var isAuthorized by remember { mutableStateOf(isTv) }
if (appViewState.showModal == AppViewState.ModalType.QR) {
// Show authorization prompt if needed
if (showAuthPrompt) {
AuthorizationPromptWrapper(
onDismiss = { showAuthPrompt = false },
onSuccess = {
showAuthPrompt = false
isAuthorized = true
},
viewModel = viewModel,
)
}
if (isAuthorized) {
QrCodeDialog(
tunnelConf = tunnelConf,
onDismiss = {
viewModel.handleEvent(AppEvent.SetShowModal(AppViewState.ModalType.NONE))
},
)
}
}
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
@@ -0,0 +1,190 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.VpnKey
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
import com.zaneschepke.wireguardautotunnel.util.extensions.setScreenBrightness
import io.github.alexzhirkevich.qrose.options.QrBallShape
import io.github.alexzhirkevich.qrose.options.QrBrush
import io.github.alexzhirkevich.qrose.options.QrErrorCorrectionLevel
import io.github.alexzhirkevich.qrose.options.QrFrameShape
import io.github.alexzhirkevich.qrose.options.QrOptions
import io.github.alexzhirkevich.qrose.options.QrPixelShape
import io.github.alexzhirkevich.qrose.options.circle
import io.github.alexzhirkevich.qrose.options.roundCorners
import io.github.alexzhirkevich.qrose.options.solid
import io.github.alexzhirkevich.qrose.rememberQrCodePainter
@Composable
fun QrCodeDialog(tunnelConf: TunnelConf, onDismiss: () -> Unit) {
val context = LocalContext.current
val activity = context as? MainActivity
// Handle screen brightness
DisposableEffect(Unit) {
activity?.setScreenBrightness(1.0f)
onDispose { activity?.setScreenBrightness(-1f) }
}
QrCodeAlertDialog(tunnelConf = tunnelConf, onDismiss = onDismiss)
}
@Composable
private fun QrCodeAlertDialog(tunnelConf: TunnelConf, onDismiss: () -> Unit) {
Surface(color = Color.White, tonalElevation = 0.dp) {
AlertDialog(
containerColor = Color.White,
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.done), color = MaterialTheme.colorScheme.surface)
}
},
title = {
Text(
text = tunnelConf.name,
color = Color.Black,
style = MaterialTheme.typography.titleLarge,
)
},
text = { QrCodeContent(tunnelConf = tunnelConf) },
properties = DialogProperties(usePlatformDefaultWidth = true),
)
}
}
@Composable
private fun QrCodeContent(tunnelConf: TunnelConf) {
var selectedOption by remember { mutableStateOf(ConfigType.WG) }
val qrCodeText =
when (selectedOption) {
ConfigType.AM -> tunnelConf.toAmConfig().toAwgQuickString(true)
ConfigType.WG -> tunnelConf.toWgConfig().toWgQuickString(true)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
) {
val qrCodePainter = rememberQrCodePainter(data = qrCodeText, options = createQrOptions())
Image(
painter = qrCodePainter,
contentDescription = stringResource(R.string.show_qr),
modifier =
Modifier.size(300.dp)
.align(Alignment.CenterHorizontally)
.padding(16.dp)
.background(Color.White),
)
ConfigTypeSelector(
selectedOption = selectedOption,
onOptionSelected = { selectedOption = it },
)
}
}
@Composable
private fun ConfigTypeSelector(selectedOption: ConfigType, onOptionSelected: (ConfigType) -> Unit) {
MultiChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)) {
ConfigType.entries.sortedDescending().forEachIndexed { index, entry ->
val isActive = selectedOption == entry
val typeName =
stringResource(
when (entry) {
ConfigType.AM -> R.string.amnezia
ConfigType.WG -> R.string.wireguard
}
)
SegmentedButton(
shape =
SegmentedButtonDefaults.itemShape(
index = index,
count = ConfigType.entries.size,
baseShape = RoundedCornerShape(8.dp),
),
icon = {
SegmentedButtonDefaults.Icon(
active = isActive,
activeContent = {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = stringResource(R.string.select),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(SegmentedButtonDefaults.IconSize),
)
},
) {
Icon(
imageVector = Icons.Outlined.VpnKey,
contentDescription = typeName,
tint = Color.Black,
modifier = Modifier.size(SegmentedButtonDefaults.IconSize),
)
}
},
colors =
SegmentedButtonDefaults.colors()
.copy(
activeContainerColor = Color.White,
inactiveContainerColor = Color.White,
),
onCheckedChange = { onOptionSelected(entry) },
checked = isActive,
) {
Text(
text = typeName,
color = Color.Black,
style = MaterialTheme.typography.labelMedium,
)
}
}
}
}
private fun createQrOptions(): QrOptions = QrOptions {
shapes {
darkPixel = QrPixelShape.circle()
ball = QrBallShape.circle()
frame = QrFrameShape.roundCorners(0.2f)
}
colors {
dark = QrBrush.solid(Color.Black)
frame = QrBrush.solid(Color.Black)
ball = QrBrush.solid(Color.Black)
}
errorCorrectionLevel = QrErrorCorrectionLevel.Medium
}
@@ -8,20 +8,19 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable @Composable
fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem { fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
val clipboardManager = LocalClipboardManager.current val clipboardManager = rememberClipboardHelper()
return SelectionItem( return SelectionItem(
leadingIcon = Icons.Filled.SmartToy, leadingIcon = Icons.Filled.SmartToy,
@@ -42,8 +41,7 @@ fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionIt
), ),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = modifier = Modifier.clickable { clipboardManager.copy(key) },
Modifier.clickable { clipboardManager.setText(AnnotatedString(key)) },
) )
} }
} }
@@ -23,25 +23,21 @@ fun DisplayScreen(appUiState: AppUiState, viewModel: AppViewModel) {
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().padding(top = 24.dp).padding(horizontal = 24.dp), modifier = Modifier.fillMaxSize().padding(top = 24.dp).padding(horizontal = 24.dp),
) { ) {
IconSurfaceButton( enumValues<Theme>().forEach {
title = stringResource(R.string.automatic), val title =
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.AUTOMATIC)) }, when (it) {
selected = appUiState.appState.theme == Theme.AUTOMATIC, Theme.DARK -> stringResource(R.string.dark)
) Theme.LIGHT -> stringResource(R.string.light)
IconSurfaceButton( Theme.AUTOMATIC -> stringResource(R.string.automatic)
title = stringResource(R.string.light), Theme.DYNAMIC -> stringResource(R.string.dynamic)
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.LIGHT)) }, Theme.DARKER -> stringResource(R.string.darker)
selected = appUiState.appState.theme == Theme.LIGHT, Theme.AMOLED -> stringResource(R.string.amoled)
) }
IconSurfaceButton( IconSurfaceButton(
title = stringResource(R.string.dark), title = title,
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DARK)) }, onClick = { viewModel.handleEvent(AppEvent.SetTheme(it)) },
selected = appUiState.appState.theme == Theme.DARK, selected = appUiState.appState.theme == it,
) )
IconSurfaceButton( }
title = stringResource(R.string.dynamic),
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DYNAMIC)) },
selected = appUiState.appState.theme == Theme.DYNAMIC,
)
} }
} }
@@ -11,17 +11,16 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.zaneschepke.logcatter.model.LogMessage import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
@Composable @Composable
fun LogItem(log: LogMessage) { fun LogItem(log: LogMessage) {
val clipboardManager = LocalClipboardManager.current val clipboardManager = rememberClipboardHelper()
val fontSize = 10.sp val fontSize = 10.sp
Row( Row(
@@ -32,7 +31,7 @@ fun LogItem(log: LogMessage) {
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = null, indication = null,
onClick = { clipboardManager.setText(AnnotatedString(log.toString())) }, onClick = { clipboardManager.copy(log.toString()) },
), ),
) { ) {
Text(text = log.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize) Text(text = log.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize)
@@ -23,6 +23,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.ContactSupportOptions import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.ContactSupportOptions
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.GeneralSupportOptions import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.GeneralSupportOptions
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateSection import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateSection
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.canInstallPackages import com.zaneschepke.wireguardautotunnel.util.extensions.canInstallPackages
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.requestInstallPackagesPermission import com.zaneschepke.wireguardautotunnel.util.extensions.requestInstallPackagesPermission
@@ -54,7 +55,7 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
InfoDialog( InfoDialog(
onDismiss = { viewModel.handleUpdateShown() }, onDismiss = { viewModel.handleUpdateShown() },
onAttest = { onAttest = {
if (BuildConfig.FLAVOR != "full") { if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR) {
uiState.appUpdate?.apkUrl?.let { context.openWebUrl(it) } uiState.appUpdate?.apkUrl?.let { context.openWebUrl(it) }
return@InfoDialog return@InfoDialog
} }
@@ -86,7 +87,8 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
}, },
confirmText = { confirmText = {
Text( Text(
if (BuildConfig.FLAVOR != "full") stringResource(R.string.download) if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR)
stringResource(R.string.download)
else stringResource(R.string.download_and_install) else stringResource(R.string.download_and_install)
) )
}, },
@@ -121,7 +123,11 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
) )
UpdateSection( UpdateSection(
onUpdateCheck = { onUpdateCheck = {
if (BuildConfig.DEBUG || BuildConfig.VERSION_NAME.contains("beta")) if (
BuildConfig.DEBUG ||
BuildConfig.VERSION_NAME.contains("beta") ||
BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR
)
return@UpdateSection context.showToast(R.string.update_check_unsupported) return@UpdateSection context.showToast(R.string.update_check_unsupported)
context.showToast(R.string.checking_for_update) context.showToast(R.string.checking_for_update)
viewModel.handleUpdateCheck() viewModel.handleUpdateCheck()
@@ -85,7 +85,7 @@ fun ContactSupportOptions(context: android.content.Context) {
), ),
) )
) )
if (BuildConfig.FLAVOR == Constants.FDROID_FLAVOR) { if (BuildConfig.FLAVOR != Constants.GOOGLE_PLAY_FLAVOR) {
add( add(
SelectionItem( SelectionItem(
leadingIcon = Icons.Filled.Favorite, leadingIcon = Icons.Filled.Favorite,
@@ -18,6 +18,7 @@ data class AppViewState(
NONE, NONE,
DELETE, DELETE,
INFO, INFO,
QR,
} }
enum class BottomSheet { enum class BottomSheet {
@@ -10,6 +10,9 @@ val Plantation = Color(0xFF264A49)
val Shark = Color(0xFF21272A) val Shark = Color(0xFF21272A)
val BalticSea = Color(0xFF1C1B1F) val BalticSea = Color(0xFF1C1B1F)
// amoled
val ElectricTeal = Color(0xFF4DD0E1)
// Status colors // Status colors
val SilverTree = Color(0xFF6DB58B) val SilverTree = Color(0xFF6DB58B)
val Brick = Color(0xFFCE4257) val Brick = Color(0xFFCE4257)
@@ -44,6 +44,8 @@ enum class Theme {
AUTOMATIC, AUTOMATIC,
LIGHT, LIGHT,
DARK, DARK,
DARKER,
AMOLED,
DYNAMIC, DYNAMIC,
} }
@@ -59,6 +61,18 @@ fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composabl
isDark = true isDark = true
DarkColorScheme DarkColorScheme
} }
Theme.DARKER -> {
isDark = true
DarkColorScheme.copy(surface = BalticSea, background = BalticSea)
}
Theme.AMOLED -> {
isDark = true
DarkColorScheme.copy(
surface = Color.Black,
background = Color.Black,
primary = ElectricTeal,
)
}
Theme.LIGHT -> { Theme.LIGHT -> {
isDark = false isDark = false
LightColorScheme LightColorScheme
@@ -35,5 +35,7 @@ object Constants {
const val QR_CODE_NAME_PROPERTY = "# Name =" const val QR_CODE_NAME_PROPERTY = "# Name ="
const val FDROID_FLAVOR = "fdroid" const val FDROID_FLAVOR = "fdroid"
const val GOOGLE_PLAY_FLAVOR = "google"
const val STANDALONE_FLAVOR = "standalone"
const val RELEASE = "release" const val RELEASE = "release"
} }
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.util.extensions package com.zaneschepke.wireguardautotunnel.util.extensions
import android.Manifest import android.Manifest
import android.app.Activity
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Context.POWER_SERVICE import android.content.Context.POWER_SERVICE
@@ -17,6 +18,9 @@ import android.widget.Toast
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.location.LocationManagerCompat import androidx.core.location.LocationManagerCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
@@ -225,3 +229,22 @@ fun Context.installApk(apkFile: File) {
} }
startActivity(intent) startActivity(intent)
} }
fun Activity.setScreenBrightness(brightness: Float) {
window.attributes = window.attributes.apply { screenBrightness = brightness }
}
fun Activity.enableImmersiveMode() {
WindowCompat.setDecorFitsSystemWindows(window, false)
val controller = WindowCompat.getInsetsController(window, window.decorView)
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
controller.hide(WindowInsetsCompat.Type.systemBars())
}
fun Activity.disableImmersiveMode() {
WindowCompat.setDecorFitsSystemWindows(window, true)
val controller = WindowCompat.getInsetsController(window, window.decorView)
controller.show(WindowInsetsCompat.Type.systemBars())
window.statusBarColor = android.graphics.Color.TRANSPARENT
}
@@ -1,14 +1,29 @@
package com.zaneschepke.wireguardautotunnel.util.extensions package com.zaneschepke.wireguardautotunnel.util.extensions
import java.util.regex.Pattern
import timber.log.Timber import timber.log.Timber
val hasNumberInParentheses = """^(.+?)\((\d+)\)$""".toRegex() val hasNumberInParentheses = """^(.+?)\((\d+)\)$""".toRegex()
fun String.isValidIpv4orIpv6Address(): Boolean { fun String.isValidIpv4orIpv6Address(): Boolean {
val ipv4Pattern = Pattern.compile("^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\$") val sanitized = removeSurrounding("[", "]")
val ipv6Pattern = Pattern.compile("^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}\$") val ipv6Pattern =
return ipv4Pattern.matcher(this).matches() || ipv6Pattern.matcher(this).matches() Regex(
"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:)" +
"{1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]" +
"{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:" +
"[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4})" +
"{1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}" +
":((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]" +
"{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}" +
"[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:)" +
"{1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"
)
val ipv4Pattern =
Regex(
"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" +
"(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
)
return ipv4Pattern.matches(sanitized) || ipv6Pattern.matches(sanitized)
} }
fun String.hasNumberInParentheses(): Boolean { fun String.hasNumberInParentheses(): Boolean {
@@ -56,7 +56,7 @@ constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@MainDispatcher private val mainDispatcher: CoroutineDispatcher, @MainDispatcher private val mainDispatcher: CoroutineDispatcher,
@AppShell private val rootShell: Provider<RootShell>, @AppShell private val rootShell: Provider<RootShell>,
private val tunnelManager: TunnelManager, val tunnelManager: TunnelManager,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val logReader: LogReader, private val logReader: LogReader,
private val fileUtils: FileUtils, private val fileUtils: FileUtils,
@@ -86,7 +86,7 @@ constructor(
appDataRepository.tunnels.flow, appDataRepository.tunnels.flow,
appDataRepository.appState.flow, appDataRepository.appState.flow,
tunnelManager.activeTunnels, tunnelManager.activeTunnels,
serviceManager.autoTunnelActive, serviceManager.autoTunnelService.map { it != null },
networkMonitor.networkStatusFlow, networkMonitor.networkStatusFlow,
) { array -> ) { array ->
val settings = array[0] as AppSettings val settings = array[0] as AppSettings
@@ -206,7 +206,6 @@ constructor(
is AppEvent.ShowMessage -> handleShowMessage(event.message) is AppEvent.ShowMessage -> handleShowMessage(event.message)
is AppEvent.PopBackStack -> is AppEvent.PopBackStack ->
_appViewState.update { it.copy(popBackStack = event.pop) } _appViewState.update { it.copy(popBackStack = event.pop) }
is AppEvent.ClearTunnelError -> tunnelManager.clearError(event.tunnel)
AppEvent.ToggleRemoteControl -> handleToggleRemoteControl(state.appState) AppEvent.ToggleRemoteControl -> handleToggleRemoteControl(state.appState)
AppEvent.ClearSelectedTunnels -> clearSelectedTunnels() AppEvent.ClearSelectedTunnels -> clearSelectedTunnels()
is AppEvent.SetShowModal -> is AppEvent.SetShowModal ->
@@ -265,6 +264,9 @@ constructor(
} }
} }
private fun handleTunnelErrors() =
viewModelScope.launch { tunnelManager.errorEvents.collect { errorEvent -> } }
private suspend fun handleAppReadyCheck(tunnels: List<TunnelConf>) { private suspend fun handleAppReadyCheck(tunnels: List<TunnelConf>) {
if (tunnels.size == appDataRepository.tunnels.count()) { if (tunnels.size == appDataRepository.tunnels.count()) {
_appViewState.update { it.copy(isAppReady = true) } _appViewState.update { it.copy(isAppReady = true) }
@@ -398,6 +400,7 @@ constructor(
private suspend fun handleClipboardImport(config: String, tunnels: List<TunnelConf>) { private suspend fun handleClipboardImport(config: String, tunnels: List<TunnelConf>) {
runCatching { runCatching {
Timber.d("Config: $config")
val amConfig = TunnelConf.configFromAmQuick(config) val amConfig = TunnelConf.configFromAmQuick(config)
val tunnelConf = TunnelConf.tunnelConfigFromAmConfig(amConfig) val tunnelConf = TunnelConf.tunnelConfigFromAmConfig(amConfig)
saveTunnel( saveTunnel(
@@ -686,7 +689,7 @@ constructor(
if (tunnels.isEmpty()) return if (tunnels.isEmpty()) return
val (files, shareFileName) = val (files, shareFileName) =
when (configType) { when (configType) {
ConfigType.AMNEZIA -> { ConfigType.AM -> {
val amFiles = fileUtils.createAmFiles(tunnels) val amFiles = fileUtils.createAmFiles(tunnels)
if (amFiles.isEmpty()) { if (amFiles.isEmpty()) {
throw IOException("No valid Amnezia config files created") throw IOException("No valid Amnezia config files created")
@@ -106,8 +106,6 @@ sealed class AppEvent {
data class ShowMessage(val message: StringValue) : AppEvent() data class ShowMessage(val message: StringValue) : AppEvent()
data class ClearTunnelError(val tunnel: TunnelConf) : AppEvent()
data class PopBackStack(val pop: Boolean) : AppEvent() data class PopBackStack(val pop: Boolean) : AppEvent()
data class SetBottomSheet(val showSheet: AppViewState.BottomSheet) : AppEvent() data class SetBottomSheet(val showSheet: AppViewState.BottomSheet) : AppEvent()
+71 -1
View File
@@ -82,7 +82,7 @@
<string name="no_browser_detected">Žádný prohlížeč nebyl nalezen</string> <string name="no_browser_detected">Žádný prohlížeč nebyl nalezen</string>
<string name="read_logs">Přečíst si logy</string> <string name="read_logs">Přečíst si logy</string>
<string name="pin_created">PIN úspěšně vytvořen</string> <string name="pin_created">PIN úspěšně vytvořen</string>
<string name="enter_pin">Vložte Váš PIN</string> <string name="enter_pin">Zadejte Váš PIN</string>
<string name="enable_app_lock">Zapnout zámek aplikace</string> <string name="enable_app_lock">Zapnout zámek aplikace</string>
<string name="restart_on_ping">Restartovat při selhání pingu</string> <string name="restart_on_ping">Restartovat při selhání pingu</string>
<string name="mobile_data_tunnel">Nastavit jako tunel pro mobilní data</string> <string name="mobile_data_tunnel">Nastavit jako tunel pro mobilní data</string>
@@ -163,4 +163,74 @@
<string name="learn_more">Zjistit více</string> <string name="learn_more">Zjistit více</string>
<string name="stop">zastavit</string> <string name="stop">zastavit</string>
<string name="server_ipv4">Překlad názvu hostitele IPv4</string> <string name="server_ipv4">Překlad názvu hostitele IPv4</string>
<string name="select_all">Vybrat vše</string>
<string name="share">Sdílet</string>
<string name="trusted_ssid_value_description">Odeslat SSID</string>
<string name="app_settings">nastavení aplikace</string>
<string name="debounce_delay">Debounce zpoždění</string>
<string name="always_on_message">Autorizace připojení VPN byla zamítnuta. Zkontrolujte prosím</string>
<string name="bio_not_created">Biometrické údaje nebyly vytvořeny</string>
<string name="bio_not_supported">Biometrie není podporována</string>
<string name="bio_subtitle">Přihlášení pomocí biometrických údajů</string>
<string name="config_error">Chybná konfigurace</string>
<string name="prominent_background_location_title">Zpřístupnění stávající polohy na pozadí</string>
<string name="vpn_denied_dialog_title">Povolení zamítnuto</string>
<string name="app_permission_title">Řídicí most pro WG tunely</string>
<string name="app_permission_description">Ovládání funkcí tunelu a automatického tunelu.</string>
<string name="enable_remote_app_control">Povolit vzdálené ovládání aplikace</string>
<string name="tunnel_starting">Spuštění tunelu</string>
<string name="bio_auth_title">Biometrické ověření</string>
<string name="nothing_here_yet">Zatím zde nic není!</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>
<string name="update_check_failed">Kontrola aktualizace se nezdařila.</string>
<string name="version_template">Verze: %1$s</string>
<string name="update_download_failed">Stažení aktualizace se nezdařilo.</string>
<string name="update_available">Dostupná aktualizace!</string>
<string name="download_and_install">Stáhnout a nainstalovat</string>
<string name="allow">Povolit</string>
<string name="permission_required">Je vyžadováno oprávnění</string>
<string name="licenses">Licence</string>
<string name="latest_installed">Již používáte nejnovější verzi.</string>
<string name="install_updated_permission">Tato aplikace potřebuje oprávnění k instalaci aktualizací.</string>
<string name="checking_for_update">Kontrola aktualizací</string>
<string name="add_from_url">Přidat z adresy URL</string>
<string name="inactive">Neaktivní</string>
<string name="auth_error">Chyba: neautorizováno</string>
<string name="kernel_name_error">Chyba názvu modulu jádra</string>
<string name="export_failed">Export se nezdařil</string>
<string name="delete">Smazat</string>
<string name="export_tunnels_wireguard">Exportovat tunely jako WireGuard</string>
<string name="export_tunnels_amnezia">Exportovat tunely jako Amnezia</string>
<string name="remote_key_template">Klíč: %1$s</string>
<string name="active">Aktivní</string>
<string name="service_running_error">Chyba: Služba není spuštěna</string>
<string name="wifi_name_template">Aktivní: %1$s</string>
<string name="tunnel_error_template">Tunel selhal s: %1$s</string>
<string name="camera_permission_required">Vyžadováno oprávnění k použití fotoaparátu</string>
<string name="info">Informace</string>
<string name="copy">Kopírovat</string>
<string name="status">Stav</string>
<string name="launch_app_settings">Spustit nastavení aplikace</string>
<string name="tunnel_running">Tunel je v provozu</string>
<string name="wildcards_active">Wildcards aktivní</string>
<string name="root_accepted">Root shell přijata</string>
<string name="background_location_message">Autorizace povolit vždy polohu a/nebo přesná poloha je vyžadováno pro tuto funkci. Viz</string>
<string name="update_check_unsupported">Kontrola aktualizací není u tohoto typu sestavení podporována.</string>
<string name="background_location_message2">abyste se ujistili, že jsou tato oprávnění povolena</string>
<string name="darker">Tmavší</string>
<string name="amoled">AMOLED</string>
<string name="default_ping_ip">(nepovinné, výchozí hodnota je peers)</string>
<string name="monitoring_state_changes">Monitorování změn stavu</string>
<string name="pre_up">Před aktivací</string>
<string name="pre_down">Před deaktivací</string>
<string name="post_up">Po aktivaci</string>
<string name="optional_default">"nepovinné, výchozí: "</string>
<string name="flavor_template">Varianta: %1$s</string>
<string name="security_template">Zabezpečení: %1$s</string>
<string name="done">Hotovo</string>
<string name="wireguard">WireGuard</string>
<string name="amnezia">Amnezia</string>
<string name="show_qr">Zobrazit QR kód</string>
</resources> </resources>
+27 -2
View File
@@ -12,7 +12,7 @@
<string name="turn_on_tunnel">Für diese Aktion muss ein aktiver Tunnel bestehen</string> <string name="turn_on_tunnel">Für diese Aktion muss ein aktiver Tunnel bestehen</string>
<string name="prominent_background_location_message">Diese Funktion erfordert die Erlaubnis zur Standortbestimmung im Hintergrund, um die Überwachung der WLAN SSID zu ermöglichen, auch wenn die Anwendung geschlossen ist. Weitere Einzelheiten in den Datenschutzbestimmungen, die auf dem Support-Bildschirm verlinkt sind.</string> <string name="prominent_background_location_message">Diese Funktion erfordert die Erlaubnis zur Standortbestimmung im Hintergrund, um die Überwachung der WLAN SSID zu ermöglichen, auch wenn die Anwendung geschlossen ist. Weitere Einzelheiten in den Datenschutzbestimmungen, die auf dem Support-Bildschirm verlinkt sind.</string>
<string name="prominent_background_location_title">Vereinbarung der Standortberechtigung im Hintergrund</string> <string name="prominent_background_location_title">Vereinbarung der Standortberechtigung im Hintergrund</string>
<string name="thank_you">Danke fürs Benutzen von WG Tunnel!</string> <string name="thank_you">Danke für die Nutzung von WG Tunnel!</string>
<string name="trusted_ssid_value_description">SSID übermitteln</string> <string name="trusted_ssid_value_description">SSID übermitteln</string>
<string name="add_tunnels_text">Von Datei oder ZIP hinzufügen</string> <string name="add_tunnels_text">Von Datei oder ZIP hinzufügen</string>
<string name="open_file">Datei öffnen</string> <string name="open_file">Datei öffnen</string>
@@ -140,7 +140,7 @@
<string name="local_logging">Lokales Logging</string> <string name="local_logging">Lokales Logging</string>
<string name="enable_local_logging">Lokales Logging aktivieren</string> <string name="enable_local_logging">Lokales Logging aktivieren</string>
<string name="add_from_clipboard">Aus Zwischenablage einfügen</string> <string name="add_from_clipboard">Aus Zwischenablage einfügen</string>
<string name="kill_switch">Kill Switch</string> <string name="kill_switch">Notschalter</string>
<string name="automatic">Automatisch</string> <string name="automatic">Automatisch</string>
<string name="language">Sprache</string> <string name="language">Sprache</string>
<string name="display_theme">Anzeigetheme</string> <string name="display_theme">Anzeigetheme</string>
@@ -227,4 +227,29 @@
<string name="wifi_name_template">Aktiv: %1$s</string> <string name="wifi_name_template">Aktiv: %1$s</string>
<string name="delete">Löschen</string> <string name="delete">Löschen</string>
<string name="nothing_here_yet">Noch nix hier!</string> <string name="nothing_here_yet">Noch nix hier!</string>
<string name="share">Teilen</string>
<string name="select_all">Alles auswählen</string>
<string name="version_template">Version: %1$s</string>
<string name="export_success">Export Erfolg</string>
<string name="download">Download</string>
<string name="check_for_update">Auf Update prüfen</string>
<string name="update_check_failed">Updateprüfung fehlgeschlagen.</string>
<string name="checking_for_update">Überpüfe auf Updates</string>
<string name="update_download_failed">Updatedownload fehlgeschlagen.</string>
<string name="update_available">Update verfügbar!</string>
<string name="download_and_install">Herunterladen und installieren</string>
<string name="permission_required">Berechtigung erforderlich</string>
<string name="licenses">Lizenzen</string>
<string name="allow">Erlauben</string>
<string name="install_updated_permission">Diese App benötigt die Berechtigung, um Updates zu installieren.</string>
<string name="latest_installed">Du verwendesz bereits die neueste Version.</string>
<string name="security_template">Sicherheit: %1$s</string>
<string name="amoled">AMOLED</string>
<string name="flavor_template">Variante: %1$s</string>
<string name="darker">Dunkler</string>
<string name="update_check_unsupported">Updateprüfung wird bei diesem Build-Typ nicht unterstützt.</string>
<string name="wireguard">WireGuard</string>
<string name="done">Erledigt</string>
<string name="show_qr">QR anzeigen</string>
<string name="amnezia">Amnezia</string>
</resources> </resources>
+16
View File
@@ -171,4 +171,20 @@
<string name="quick_actions">Actions rapides</string> <string name="quick_actions">Actions rapides</string>
<string name="enable_amnezia_compatibility">Activer la prise en charge d\'Amnezia</string> <string name="enable_amnezia_compatibility">Activer la prise en charge d\'Amnezia</string>
<string name="include_lan">Inclure le LAN</string> <string name="include_lan">Inclure le LAN</string>
<string name="select">Sélectionner</string>
<string name="join_telegram">Rejoindre la communauté Telegram</string>
<string name="join_matrix">Rejoindre la communauté Matrix</string>
<string name="auto_tunnel_channel_description">Un canal pour les notifications de l\'état du tunnel automatique</string>
<string name="tunnel_control">Contrôle du tunnel</string>
<string name="auto_tunnel">Tunnel automatique</string>
<string name="add_tunnel">Ajouter un tunnel</string>
<string name="error_download_failed">Le téléchargement de la configuration a échouée</string>
<string name="multiple">Multiple</string>
<string name="add_from_url">Ajouter depuis un URL</string>
<string name="enter_config_url">Saisissez l\'URL de configuration</string>
<string name="search">Rechercher</string>
<string name="save">Sauvegarder</string>
<string name="copy">Copier</string>
<string name="info">Informations</string>
<string name="prefer_ipv4">Préférer une connexion IPv4</string>
</resources> </resources>
+3 -1
View File
@@ -1,2 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources></resources> <resources>
<string name="app_name">WG Tunnel</string>
</resources>
+135 -6
View File
@@ -10,7 +10,7 @@
<string name="comma_separated_list">elenco separato da virgola</string> <string name="comma_separated_list">elenco separato da virgola</string>
<string name="listen_port">Porta d\'ascolto</string> <string name="listen_port">Porta d\'ascolto</string>
<string name="error_authorization_failed">Autorizzazione fallita</string> <string name="error_authorization_failed">Autorizzazione fallita</string>
<string name="error_no_file_explorer">Nessun esploratore di file installato</string> <string name="error_no_file_explorer">Nessun gestore file installato</string>
<string name="app_name">Tunnel WG</string> <string name="app_name">Tunnel WG</string>
<string name="vpn_channel_name">Canale di notifica VPN</string> <string name="vpn_channel_name">Canale di notifica VPN</string>
<string name="turn_off_tunnel">L\'operaz. richiede la disatt. del tunnel</string> <string name="turn_off_tunnel">L\'operaz. richiede la disatt. del tunnel</string>
@@ -20,7 +20,7 @@
<string name="enabled_app_shortcuts">Abilita le scorciatoie da app</string> <string name="enabled_app_shortcuts">Abilita le scorciatoie da app</string>
<string name="email_subject">Supporto di Tunnel WG</string> <string name="email_subject">Supporto di Tunnel WG</string>
<string name="email_chooser">Invia un email…</string> <string name="email_chooser">Invia un email…</string>
<string name="docs_description">Read the docs</string> <string name="docs_description">Leggi la documentazione</string>
<string name="email_description">Mandami un\'email (in inglese)</string> <string name="email_description">Mandami un\'email (in inglese)</string>
<string name="no_tunnels">Ancora nessun tunnel aggiunto!</string> <string name="no_tunnels">Ancora nessun tunnel aggiunto!</string>
<string name="peer">Peer</string> <string name="peer">Peer</string>
@@ -53,11 +53,11 @@
<string name="error_ssid_exists">L\'SSID esiste già</string> <string name="error_ssid_exists">L\'SSID esiste già</string>
<string name="error_root_denied">Shell di root negata</string> <string name="error_root_denied">Shell di root negata</string>
<string name="prominent_background_location_title">Permesso localizzazione in background</string> <string name="prominent_background_location_title">Permesso localizzazione in background</string>
<string name="location_services_missing_message">Questa app non rileva nessun servizio di localizzazione attiva sul tuo dispositivo. Dipendentemente dal dispositivo, questo potrebbe causare il fallimento a leggere il nome wifi da parte della funzione di wifi non fidate. Vuoi continuare comunque?</string> <string name="location_services_missing_message">Questa app non rileva alcun servizio di localizzazione attivo sul tuo dispositivo. Indipendentemente dal dispositivo, questo potrebbe causare il fallimento nel recuperare il nome wifi da parte della funzione wifi non fidate. Vuoi continuare comunque?</string>
<string name="read_logs">Leggi i log</string> <string name="read_logs">Leggi i log</string>
<string name="auto">(auto)</string> <string name="auto">(auto)</string>
<string name="underload_packet_magic_header">Magic header pacchetto sottocarico</string> <string name="underload_packet_magic_header">Magic header pacchetto sottocarico</string>
<string name="unsure_how">se non sei sicuro di come procedere</string> <string name="unsure_how">se non sei sicuro su come procedere</string>
<string name="vpn_settings">Impostazioni sistema VPN</string> <string name="vpn_settings">Impostazioni sistema VPN</string>
<string name="always_on_message">Permessi connessione VPN negati. Verifica la</string> <string name="always_on_message">Permessi connessione VPN negati. Verifica la</string>
<string name="junk_packet_maximum_size">Dimensione massima pacchetti indesiderati</string> <string name="junk_packet_maximum_size">Dimensione massima pacchetti indesiderati</string>
@@ -115,7 +115,136 @@
<string name="enter_pin">Inserisci il tuo PIN</string> <string name="enter_pin">Inserisci il tuo PIN</string>
<string name="create_pin">Crea PIN</string> <string name="create_pin">Crea PIN</string>
<string name="transport_packet_magic_header">Magic header pacchetto trasporto</string> <string name="transport_packet_magic_header">Magic header pacchetto trasporto</string>
<string name="kill_switch_off">Ferma interruttore di spegnimento su fidate</string> <string name="kill_switch_off">Arresta kill switch su fidate</string>
<string name="prominent_background_location_message">Questa caratteristica richiede il permesso di localizzazione in background per abilitare il monitoraggio dell\'SSID Wi-fi anche quando l\'applicazione è chiusa. Per più dettagli, verifica la Privacy Policy linkata nella schermata di supporto.</string> <string name="prominent_background_location_message">Questa caratteristica richiede il permesso di localizzazione in background per abilitare il monitoraggio dell\'SSID Wi-fi anche quando l\'applicazione è chiusa. Per più dettagli, verifica la Privacy Policy linkata nella schermata di supporto.</string>
<string name="auto_tunnel_title">Servizio auto-tunnel</string> <string name="auto_tunnel_title">Servizio tunnel automatico</string>
<string name="mobile_tunnel">Tunnel dati mobili</string>
<string name="wifi_name_via_shell">Nome wifi via shell</string>
<string name="use_root_shell_for_wifi">Usa una shell di robot per recuperare il nome wifi</string>
<string name="stop_on_internet_loss">Ferma il tunnel in caso di perdita di collegamento internet</string>
<string name="ethernet_tunnel">Tunnel ethernet</string>
<string name="vpn_kill_switch">Kill switch VPN</string>
<string name="kill_switch_options">Opzioni kill switch</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>
<string name="appearance">Aspetto</string>
<string name="skip">Salta</string>
<string name="launch_app_settings">Opzioni di avvio della app</string>
<string name="learn_more">Approfondisci</string>
<string name="wildcards_active">Caratteri jolly attivi</string>
<string name="donate">Dona al progetto</string>
<string name="enable_local_logging">Abilita logging locale</string>
<string name="monitoring_state_changes">Monitora i cambi di stato</string>
<string name="stop">arrestato</string>
<string name="tunnel_specific_settings">Configurazioni specifiche del tunnel</string>
<string name="show_scripts">Mostra gli script</string>
<string name="pre_up">Prima di avviarsi</string>
<string name="splt_tunneling">Split tunneling</string>
<string name="quick_actions">Azioni rapide</string>
<string name="debounce_delay">Ritardo debounce</string>
<string name="hide_amnezia_properties">Nascondi le proprietà di Amnezia</string>
<string name="advanced_settings">Configurazioni avanzate</string>
<string name="enable_amnezia_compatibility">Abilita compatibilità Amnezia</string>
<string name="remove_amnezia_compatibility">Disabilita compatibilità Amnezia</string>
<string name="exclude_lan">Escludi la LAN</string>
<string name="share">Condividi</string>
<string name="select_all">Seleziona tutto</string>
<string name="never">mai</string>
<string name="use_wildcards">Usa nomi jolly</string>
<string name="add_from_clipboard">Aggiungi dagli appunti</string>
<string name="trusted_wifi_names">Nomi wifi di fiducia</string>
<string name="add_wifi_name">Aggiungi un nome wifi</string>
<string name="primary_tunnel">Tunnel primario</string>
<string name="sec">sec</string>
<string name="post_down">Dopo essersi arrestato</string>
<string name="pre_down">Prima di arrestarsi</string>
<string name="light">Chiaro</string>
<string name="dark">Scuro</string>
<string name="bio_not_created">Biometria non creata</string>
<string name="search">Cerca</string>
<string name="app_permission_description">Controllo tunnel e funzionalità automatiche.</string>
<string name="enter_config_url">Inserisci un URL di configurazione</string>
<string name="error_download_failed">Download della configurazione fallito</string>
<string name="save">Salva</string>
<string name="active">Attivo</string>
<string name="dropdown">A discesa</string>
<string name="local_logging">Logging locale</string>
<string name="handshake">handshake</string>
<string name="logs">Logs</string>
<string name="notifications">Notifiche</string>
<string name="automatic">Automatico</string>
<string name="kill_switch">Kill switch</string>
<string name="dynamic">Dinamico</string>
<string name="language">Lingua</string>
<string name="display_theme">Tema schermo</string>
<string name="kernel_not_supported">Kernel non supportato</string>
<string name="tunnel_running">Tunnel avviato</string>
<string name="stop_on_no_internet">Ferma se non c\'è internet</string>
<string name="start_auto">Avvia tunnel automatico</string>
<string name="stop_auto">Ferma tunnel automatico</string>
<string name="native_kill_switch">Kill switch nativo</string>
<string name="set_ethernet_tunnel">Configura come tunnel ethernet</string>
<string name="bypass_lan_for_kill_switch">Bypass LAN per kill switch</string>
<string name="vpn_channel_description">Un canale per le notifiche di stato della VPN</string>
<string name="auto_tunnel_channel_name">Canale di notifica del tunnel automatico</string>
<string name="auto_tunnel_channel_description">Un canale per le notifiche di stato del tunnel automatico</string>
<string name="post_up">Dopo essersi avviato</string>
<string name="multiple">Molteplici</string>
<string name="add_from_url">Aggiungi da URL</string>
<string name="join_telegram">Unisciti alla comunità Telegram</string>
<string name="join_matrix">Unisciti alla comunità Matrix</string>
<string name="app_permission_title">WG Tunnel Control Bridge</string>
<string name="select">Seleziona</string>
<string name="hide_scripts">Nascondi gli script</string>
<string name="auto_tunnel">Tunnel automatico</string>
<string name="add_tunnel">Aggiungi tunnel</string>
<string name="export_logs">Esporta i log salvati</string>
<string name="delete_logs">Cancella e pulisci i logs</string>
<string name="copy">Copia</string>
<string name="info">Informazioni</string>
<string name="export_tunnels_amnezia">Esporta i tunnel in modalità Amnezia</string>
<string name="export_tunnels_wireguard">Esporta i tunnel in modalità WireGuard</string>
<string name="delete">Cancella</string>
<string name="camera_permission_required">Permessi telecamera richiesti</string>
<string name="export_failed">Esportazione fallita</string>
<string name="tunnel_error_template">Tunnel interrotto: %1$s</string>
<string name="wifi_name_template">Attivo: %1$s</string>
<string name="auth_error">errore non autorizzato</string>
<string name="service_running_error">errore servizio non avviato</string>
<string name="inactive">Inattivo</string>
<string name="remote_key_template">Chiave: %1$s</string>
<string name="config_error">errore di configurazione</string>
<string name="dns_resolve_error">errore risoluzione dns</string>
<string name="invalid_config_error">invalid_config_error</string>
<string name="bio_not_supported">Biometria non supportata</string>
<string name="bio_subtitle">Il log sta usando la tua autenticazione biometrica</string>
<string name="enable_remote_app_control">Abilita controllo remoto app</string>
<string name="version_template">Versione: %1$s</string>
<string name="flavor_template">Caratteristica: %1$s</string>
<string name="kernel_name_error">nome errato modulo kernel</string>
<string name="status">Stato</string>
<string name="bio_auth_title">Autenticazione Biometrica</string>
<string name="bio_update_required">Aggiornamento richiesto sicurezza biometrica</string>
<string name="tunnel_starting">Il tunnel si sta avviando</string>
<string name="update_available">Aggiornamento disponibile!</string>
<string name="permission_required">Permessi Richiesti</string>
<string name="download_and_install">Scarica e Installa</string>
<string name="latest_installed">Sta già usando la versione più recente.</string>
<string name="update_download_failed">Download aggiornamento fallito.</string>
<string name="install_updated_permission">Questa applicazione richiede i permessi per installare gli aggiornamenti.</string>
<string name="allow">Autorizza</string>
<string name="licenses">Licenze</string>
<string name="update_check_unsupported">Controllo degli aggiornamenti non supportato per questo tipo di build.</string>
<string name="darker">Più scuro</string>
<string name="security_template">Sicurezza: %1$s</string>
<string name="export_success">Esportazione riuscita</string>
<string name="download">Download</string>
<string name="check_for_update">Controlla gli aggiornamenti</string>
<string name="update_check_failed">Controllo aggiornamenti fallito.</string>
<string name="checking_for_update">Controllo gli aggiornamenti</string>
<string name="amoled">AMOLED</string>
<string name="server_ipv4">Risoluzione nomi IPv4</string>
<string name="prefer_ipv4">Preferisci connessioni IPv4</string>
<string name="nothing_here_yet">Non c\'è ancora nulla qui!</string>
</resources> </resources>
+32
View File
@@ -54,4 +54,36 @@
<string name="turn_on_tunnel">実行に有効なVPNトンネルが必要です</string> <string name="turn_on_tunnel">実行に有効なVPNトンネルが必要です</string>
<string name="add_peer">ピアの追加</string> <string name="add_peer">ピアの追加</string>
<string name="delete_tunnel">トンネルの削除</string> <string name="delete_tunnel">トンネルの削除</string>
<string name="unsure_how">操作方法がわからない場合</string>
<string name="background_location_message">この機能には『常に許可』の位置情報権限および/または正確な位置情報が必要です。以下をご覧ください</string>
<string name="tunnel_on_ethernet">Ethernetでのトンネルの有効化</string>
<string name="open_issue">問題を報告する</string>
<string name="incorrect_pin">PINが無効です</string>
<string name="read_logs">ログを確認する</string>
<string name="pin_created">PINの登録が完了しました</string>
<string name="settings">設定</string>
<string name="set_custom_ping_internal">Ping間隔 (秒)</string>
<string name="tunnel_mobile_data">モバイルデータでのトンネルの有効化</string>
<string name="tunnel_on_wifi">信頼していないWifiでの有効化</string>
<string name="enter_pin">PINコードを入力</string>
<string name="create_import">最初から作成する</string>
<string name="auto_tunnel_title">自動トンネルサービス</string>
<string name="edit_tunnel">トンネルの編集</string>
<string name="create_pin">新規PINを作成</string>
<string name="use_tunnel_on_wifi_name">指定WiFiでトンネルを適用</string>
<string name="version">バージョン</string>
<string name="mobile_data_tunnel">モバイルデータトンネルに設定</string>
<string name="enable_app_lock">アプリロックを有効にする</string>
<string name="always_on_message">VPN接続の許可が拒否されました。</string>
<string name="always_on_message2">他のすべてのアプリで常時接続VPNがオフになっていることを確認して、再度お試しください</string>
<string name="getting_started_guide">スタートガイド</string>
<string name="error_file_format">無効なトンネル設定フォーマット</string>
<string name="kill_switch">キルスイッチ</string>
<string name="notifications">通知</string>
<string name="logs">ログ</string>
<string name="add_from_clipboard">クリップボードからの追加</string>
<string name="automatic">自動</string>
<string name="add_wifi_name">Wifi名の追加</string>
<string name="language">設定言語</string>
<string name="trusted_wifi_names">信頼されたWifi名</string>
</resources> </resources>
+21
View File
@@ -150,4 +150,25 @@
<string name="add_from_url">Toevoegen met link</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_no_internet">Stoppen wanneer geen internet</string>
<string name="stop_on_internet_loss">Stop tunnel bij verlies van internet</string> <string name="stop_on_internet_loss">Stop tunnel bij verlies van internet</string>
<string name="mobile_tunnel">Mobiele datatunnel</string>
<string name="wildcards_active">Wildcards actief</string>
<string name="wifi_name_via_shell">Wifi naam via shell</string>
<string name="vpn_kill_switch">VPN kill switch</string>
<string name="kill_switch_options">Kill switch opties</string>
<string name="use_root_shell_for_wifi">Gebruik root shell om wifi naam te bepalen</string>
<string name="kernel_not_supported">Kernel niet ondersteund</string>
<string name="start_auto">Start auto-tunnel</string>
<string name="stop_auto">Stop auto-tunnel</string>
<string name="tunnel_running">Tunnel actief</string>
<string name="donate">Doneer aan project</string>
<string name="local_logging">Lokale logboeken</string>
<string name="monitoring_state_changes">Monitor statuswijzigingen</string>
<string name="trusted_wifi_names">Vertrouwde Wifi namen</string>
<string name="add_wifi_name">Wifi naam toevoegen</string>
<string name="launch_app_settings">Open applicatie-instellingen</string>
<string name="use_wildcards">Gebruik wildcards in naam</string>
<string name="learn_more">Lees meer</string>
<string name="app_permission_description">Configureer tunnels een auto-tunnel instellingen.</string>
<string name="skip">Overslaan</string>
<string name="primary_tunnel">Primaire tunnel</string>
</resources> </resources>
+30 -5
View File
@@ -81,12 +81,12 @@
<string name="unknown_error">Wystąpił nieznany błąd</string> <string name="unknown_error">Wystąpił nieznany błąd</string>
<string name="start_auto">Uruchom autotunel</string> <string name="start_auto">Uruchom autotunel</string>
<string name="location_services_not_detected">Usługi lokalizacyjne nie zostały wykryte</string> <string name="location_services_not_detected">Usługi lokalizacyjne nie zostały wykryte</string>
<string name="background_location_message">Ta funkcja wymaga pozwolenia na określenie lokalizacji w dowolnym momencie i/lub dokładnej lokalizacji. Sprawdź</string> <string name="background_location_message">Ta funkcja wymaga zezwolenia na określenie lokalizacji w dowolnym momencie i/lub dokładnej lokalizacji. Sprawdź</string>
<string name="auto_tunnel_title">Usługa autotunelu</string> <string name="auto_tunnel_title">Usługa autotunelu</string>
<string name="donate">Przekaż darowiznę na rzecz projektu</string> <string name="donate">Przekaż darowiznę na rzecz projektu</string>
<string name="trusted_ssid_value_description">Prześlij SSID</string> <string name="trusted_ssid_value_description">Prześlij SSID</string>
<string name="all">wszystkie</string> <string name="all">wszystkie</string>
<string name="no_email_detected">Nie wykryto aplikacji e-mail</string> <string name="no_email_detected">Nie wykryto aplikacji pocztowej</string>
<string name="turn_off_tunnel">Czynność wymaga wyłączenia tunelu</string> <string name="turn_off_tunnel">Czynność wymaga wyłączenia tunelu</string>
<string name="add_from_qr">Dodaj z kodu QR</string> <string name="add_from_qr">Dodaj z kodu QR</string>
<string name="qr_scan">Skanuj kod QR</string> <string name="qr_scan">Skanuj kod QR</string>
@@ -140,14 +140,14 @@
<string name="stop_auto">Zatrzymaj autotunel</string> <string name="stop_auto">Zatrzymaj autotunel</string>
<string name="tunnel_running">Tunel jest uruchomiony</string> <string name="tunnel_running">Tunel jest uruchomiony</string>
<string name="enable_local_logging">Włącz lokalne rejestrowanie</string> <string name="enable_local_logging">Włącz lokalne rejestrowanie</string>
<string name="email_chooser">Wyślij e-mail…</string> <string name="email_chooser">Wyślij wiadomość e-mail…</string>
<string name="set_custom_ping_cooldown">Czas odnowienia pingowania (sek.)</string> <string name="set_custom_ping_cooldown">Czas odnowienia pingowania (sek.)</string>
<string name="open_file">Otwórz plik</string> <string name="open_file">Otwórz plik</string>
<string name="okay">OK</string> <string name="okay">OK</string>
<string name="no_tunnels">Nie dodano jeszcze żadnych tuneli!</string> <string name="no_tunnels">Nie dodano jeszcze żadnych tuneli!</string>
<string name="exclude">Wyklucz</string> <string name="exclude">Wyklucz</string>
<string name="docs_description">Przeczytaj dokumentację</string> <string name="docs_description">Przeczytaj dokumentację</string>
<string name="email_description">Wyślij mi e-mail</string> <string name="email_description">Wyślij mi wiadomość e-mail</string>
<string name="set_primary_tunnel">Ustaw jako tunel podstawowy</string> <string name="set_primary_tunnel">Ustaw jako tunel podstawowy</string>
<string name="mobile_data_tunnel">Ustaw jako tunel mobilnej transmisji danych</string> <string name="mobile_data_tunnel">Ustaw jako tunel mobilnej transmisji danych</string>
<string name="vpn_channel_id">Kanał VPN</string> <string name="vpn_channel_id">Kanał VPN</string>
@@ -210,7 +210,7 @@
<string name="info">Informacje</string> <string name="info">Informacje</string>
<string name="export_tunnels_wireguard">Eksportuj tunele jako WireGuard</string> <string name="export_tunnels_wireguard">Eksportuj tunele jako WireGuard</string>
<string name="delete">Usuń</string> <string name="delete">Usuń</string>
<string name="camera_permission_required">Wymagane pozwolenie na dostęp do aparatu</string> <string name="camera_permission_required">Wymagane zezwolenie na dostęp do aparatu</string>
<string name="tunnel_error_template">Tunel nie powiódł się z powodu: %1$s</string> <string name="tunnel_error_template">Tunel nie powiódł się z powodu: %1$s</string>
<string name="remote_key_template">Klucz: %1$s</string> <string name="remote_key_template">Klucz: %1$s</string>
<string name="config_error">błąd konfiguracji</string> <string name="config_error">błąd konfiguracji</string>
@@ -228,4 +228,29 @@
<string name="invalid_config_error">błąd nieprawidłowej konfiguracji</string> <string name="invalid_config_error">błąd nieprawidłowej konfiguracji</string>
<string name="dns_resolve_error">błąd rozwiązywania DNS</string> <string name="dns_resolve_error">błąd rozwiązywania DNS</string>
<string name="nothing_here_yet">Jeszcze nic tu nie ma!</string> <string name="nothing_here_yet">Jeszcze nic tu nie ma!</string>
<string name="select_all">Wybierz wszystkie</string>
<string name="share">Udostępnij</string>
<string name="download">Pobierz</string>
<string name="version_template">Wersja: %1$s</string>
<string name="check_for_update">Sprawdź aktualizację</string>
<string name="update_check_failed">Sprawdzanie aktualizacji zakończyło się niepowodzeniem.</string>
<string name="checking_for_update">Sprawdzanie aktualizacji</string>
<string name="latest_installed">Używasz już najnowszej wersji.</string>
<string name="update_download_failed">Nie udało się pobrać aktualizacji.</string>
<string name="update_available">Aktualizacja dostępna!</string>
<string name="download_and_install">Pobierz i zainstaluj</string>
<string name="permission_required">Wymagane zezwolenie</string>
<string name="install_updated_permission">Ta aplikacja wymaga zezwolenia na instalowanie aktualizacji.</string>
<string name="allow">Zezwól</string>
<string name="export_success">Eksport zakończony powodzeniem</string>
<string name="licenses">Licencje</string>
<string name="update_check_unsupported">Sprawdzanie aktualizacji nie jest obsługiwane w tym typie kompilacji.</string>
<string name="flavor_template">Wariant: %1$s</string>
<string name="darker">Ciemniejszy</string>
<string name="amoled">AMOLED</string>
<string name="security_template">Zabezpieczenia: %1$s</string>
<string name="show_qr">Pokaż kod QR</string>
<string name="amnezia">Amnezia</string>
<string name="wireguard">WireGuard</string>
<string name="done">Gotowe</string>
</resources> </resources>
+30 -5
View File
@@ -112,7 +112,7 @@
<string name="always_on_message2">, чтобы убедиться, что функция «Постоянный VPN» отключена для всех других приложений, и повторите попытку</string> <string name="always_on_message2">, чтобы убедиться, что функция «Постоянный VPN» отключена для всех других приложений, и повторите попытку</string>
<string name="background_location_message">Разрешать всё время, пока для работы этой функции требуется доступ на определение местоположения и/или точное местоположение. Смотрите</string> <string name="background_location_message">Разрешать всё время, пока для работы этой функции требуется доступ на определение местоположения и/или точное местоположение. Смотрите</string>
<string name="vpn_settings">Системные настройки VPN</string> <string name="vpn_settings">Системные настройки VPN</string>
<string name="vpn_denied_dialog_title">Нет разрешения</string> <string name="vpn_denied_dialog_title">Отказано в доступе</string>
<string name="set_custom_ping_internal">Интервал пинга (сек.)</string> <string name="set_custom_ping_internal">Интервал пинга (сек.)</string>
<string name="optional_default">"необязательно, по умолчанию: "</string> <string name="optional_default">"необязательно, по умолчанию: "</string>
<string name="set_custom_ping_ip">Назначить свой IP для пинга</string> <string name="set_custom_ping_ip">Назначить свой IP для пинга</string>
@@ -146,7 +146,7 @@
<string name="use_root_shell_for_wifi">Использовать root-доступ для получения имени сети Wi-Fi</string> <string name="use_root_shell_for_wifi">Использовать root-доступ для получения имени сети Wi-Fi</string>
<string name="wifi_name_via_shell">Имя Wi-Fi через root</string> <string name="wifi_name_via_shell">Имя Wi-Fi через root</string>
<string name="start_auto">Запустить автотуннель</string> <string name="start_auto">Запустить автотуннель</string>
<string name="tunnel_running">Туннель работает</string> <string name="tunnel_running">Работающий туннель</string>
<string name="monitoring_state_changes">Отслеживание изменений состояния</string> <string name="monitoring_state_changes">Отслеживание изменений состояния</string>
<string name="enable_local_logging">Включить ведение журнала</string> <string name="enable_local_logging">Включить ведение журнала</string>
<string name="add_from_clipboard">Добавить из буфера обмена</string> <string name="add_from_clipboard">Добавить из буфера обмена</string>
@@ -171,7 +171,7 @@
<string name="post_up">После активации</string> <string name="post_up">После активации</string>
<string name="pre_down">До деактивации</string> <string name="pre_down">До деактивации</string>
<string name="post_down">После деактивации</string> <string name="post_down">После деактивации</string>
<string name="debounce_delay">Задержка отбоя</string> <string name="debounce_delay">Задержка изменения сети</string>
<string name="remove_amnezia_compatibility">Отключить совместимость с Amnezia</string> <string name="remove_amnezia_compatibility">Отключить совместимость с Amnezia</string>
<string name="exclude_lan">Исключить LAN</string> <string name="exclude_lan">Исключить LAN</string>
<string name="hide_scripts">Скрыть сценарии</string> <string name="hide_scripts">Скрыть сценарии</string>
@@ -179,8 +179,8 @@
<string name="advanced_settings">Дополнительные настройки</string> <string name="advanced_settings">Дополнительные настройки</string>
<string name="enable_amnezia_compatibility">Включить совместимость с Amnezia</string> <string name="enable_amnezia_compatibility">Включить совместимость с Amnezia</string>
<string name="include_lan">Включить LAN</string> <string name="include_lan">Включить LAN</string>
<string name="auto_tunnel">Автотуннель</string> <string name="auto_tunnel">Авто-туннелирование</string>
<string name="tunnel_control">Управление туннелем</string> <string name="tunnel_control">Управление туннелями</string>
<string name="kill_switch_off">Без экстренного отключения в доверенных</string> <string name="kill_switch_off">Без экстренного отключения в доверенных</string>
<string name="prefer_ipv4">Предпочитать соединение IPv4</string> <string name="prefer_ipv4">Предпочитать соединение IPv4</string>
<string name="server_ipv4">Получать имя узла IPv4</string> <string name="server_ipv4">Получать имя узла IPv4</string>
@@ -227,4 +227,29 @@
<string name="enable_remote_app_control">Удалённое управление приложением</string> <string name="enable_remote_app_control">Удалённое управление приложением</string>
<string name="error_download_failed">Невозможно скачать конфигурацию</string> <string name="error_download_failed">Невозможно скачать конфигурацию</string>
<string name="nothing_here_yet">Здесь пока ничего нет!</string> <string name="nothing_here_yet">Здесь пока ничего нет!</string>
<string name="select_all">Выбрать все</string>
<string name="share">Поделиться</string>
<string name="export_success">Экспорт успешно выполнен</string>
<string name="check_for_update">Проверить обновление</string>
<string name="update_check_failed">Проверка обновлений не выполнена.</string>
<string name="checking_for_update">Проверка обновления</string>
<string name="latest_installed">Вы уже используете последнюю версию.</string>
<string name="update_available">Доступно обновление!</string>
<string name="install_updated_permission">Этому приложению нужно разрешение на установку обновлений.</string>
<string name="allow">Разрешить</string>
<string name="download">Скачать</string>
<string name="download_and_install">Скачать и установить</string>
<string name="update_download_failed">Невозможно скачать обновление.</string>
<string name="version_template">Версия: %1$s</string>
<string name="licenses">Лицензии</string>
<string name="permission_required">Требуется разрешение</string>
<string name="update_check_unsupported">Проверка обновлений не поддерживается для этого типа сборок.</string>
<string name="flavor_template">Сборка: %1$s</string>
<string name="amoled">AMOLED</string>
<string name="darker">Ночная</string>
<string name="security_template">Защита: %1$s</string>
<string name="wireguard">WireGuard</string>
<string name="done">Готово</string>
<string name="amnezia">Amnezia</string>
<string name="show_qr">Показать QR-код</string>
</resources> </resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+63 -4
View File
@@ -136,10 +136,10 @@
<string name="splt_tunneling">பிளவு சுரங்கப்பாதை</string> <string name="splt_tunneling">பிளவு சுரங்கப்பாதை</string>
<string name="tunnel_specific_settings">சுரங்கப்பாதை குறிப்பிட்ட அமைப்புகள்</string> <string name="tunnel_specific_settings">சுரங்கப்பாதை குறிப்பிட்ட அமைப்புகள்</string>
<string name="show_scripts">ச்கிரிப்ட்களைக் காட்டு</string> <string name="show_scripts">ச்கிரிப்ட்களைக் காட்டு</string>
<string name="pre_up">முன்</string> <string name="pre_up">முன் மேல்</string>
<string name="post_up">இடுகை</string> <string name="post_up">பின் மேல்</string>
<string name="pre_down">முன்</string> <string name="pre_down">முன் கீழ்</string>
<string name="post_down">இடுகை</string> <string name="post_down">பின் கீழ்</string>
<string name="hide_scripts">ச்கிரிப்ட்களை மறைக்கவும்</string> <string name="hide_scripts">ச்கிரிப்ட்களை மறைக்கவும்</string>
<string name="enable_amnezia_compatibility">அம்னேசியா பொருந்தக்கூடிய தன்மையை இயக்கவும்</string> <string name="enable_amnezia_compatibility">அம்னேசியா பொருந்தக்கூடிய தன்மையை இயக்கவும்</string>
<string name="debounce_delay">நேரந்தவறுகை</string> <string name="debounce_delay">நேரந்தவறுகை</string>
@@ -185,4 +185,63 @@
<string name="prefer_ipv4">ஐபிவி 4 இணைப்பை விரும்புங்கள்</string> <string name="prefer_ipv4">ஐபிவி 4 இணைப்பை விரும்புங்கள்</string>
<string name="multiple">நான்கு</string> <string name="multiple">நான்கு</string>
<string name="kill_switch_off">நம்பகமானதைக் கொலை செய்வதை நிறுத்துங்கள்</string> <string name="kill_switch_off">நம்பகமானதைக் கொலை செய்வதை நிறுத்துங்கள்</string>
<string name="share">பங்கு</string>
<string name="select_all">அனைத்தையும் தெரிவுசெய்</string>
<string name="version_template">பதிப்பு: %1$s</string>
<string name="update_download_failed">புதுப்பிப்பு பதிவிறக்கம் தோல்வியடைந்தது.</string>
<string name="update_available">புதுப்பிப்பு கிடைக்கிறது!</string>
<string name="allow">இசைவு</string>
<string name="inactive">செயலற்றது</string>
<string name="tunnel_error_template">சுரங்கப்பாதை தோல்வியுற்றது: %1$s</string>
<string name="export_tunnels_wireguard">சுரங்கங்களை வயர் கார்டாக ஏற்றுமதி செய்யுங்கள்</string>
<string name="invalid_config_error">Invalitir_config_error</string>
<string name="app_permission_title">WG சுரங்கப்பாதை கட்டுப்பாட்டு பாலம்</string>
<string name="add_tunnel">சுரங்கப்பாதை சேர்க்கவும்</string>
<string name="app_permission_description">சுரங்கங்கள் மற்றும் ஆட்டோ-டன்னல் அம்சங்களைக் கட்டுப்படுத்தவும்.</string>
<string name="enter_config_url">கட்டமைப்பு முகவரி ஐ உள்ளிடவும்</string>
<string name="error_download_failed">கட்டமைப்பைப் பதிவிறக்குவதில் தோல்வி</string>
<string name="search">தேடல்</string>
<string name="select">தேர்ந்தெடு</string>
<string name="join_telegram">தந்தி சமூகத்தில் சேரவும்</string>
<string name="join_matrix">மேட்ரிக்ச் சமூகத்தில் சேரவும்</string>
<string name="dropdown">கீழ்தோன்றும்</string>
<string name="export_logs">சேமிக்கப்பட்ட பதிவுகளை ஏற்றுமதி செய்யுங்கள்</string>
<string name="info">தகவல்</string>
<string name="delete">நீக்கு</string>
<string name="wifi_name_template">செயலில்: %1$s</string>
<string name="export_failed">ஏற்றுமதி தோல்வியடைந்தது</string>
<string name="remote_key_template">விசை: %1$s</string>
<string name="config_error">கட்டமைப்பு பிழை</string>
<string name="dns_resolve_error">டிஎன்எச் தீர்மானம் பிழை</string>
<string name="kernel_name_error">கர்னல் தொகுதி பெயர் பிழை</string>
<string name="auth_error">அங்கீகரிக்கப்பட்ட பிழை இல்லை</string>
<string name="service_running_error">பணி இயங்கும் பிழை</string>
<string name="active">செயலில்</string>
<string name="bio_not_supported">பயோமெட்ரிக்ச் ஆதரிக்கப்படவில்லை</string>
<string name="bio_subtitle">உங்கள் பயோமெட்ரிக் நற்சான்றிதழைப் பயன்படுத்தி உள்நுழைக</string>
<string name="tunnel_starting">சுரங்கப்பாதை தொடங்குகிறது</string>
<string name="enable_remote_app_control">தொலைநிலை பயன்பாட்டுக் கட்டுப்பாட்டை இயக்கவும்</string>
<string name="bio_not_created">பயோமெட்ரிக்ச் உருவாக்கப்படவில்லை</string>
<string name="bio_update_required">பயோமெட்ரிக் பாதுகாப்பு புதுப்பிப்பு தேவை</string>
<string name="flavor_template">சுவை: %1$s</string>
<string name="add_from_url">முகவரி இலிருந்து சேர்க்கவும்</string>
<string name="delete_logs">பதிவுகளை நீக்கவும் அழிக்கவும்</string>
<string name="copy">நகலெடு</string>
<string name="save">சேமி</string>
<string name="export_tunnels_amnezia">அம்னேசியாவாக சுரங்கங்களை ஏற்றுமதி செய்யுங்கள்</string>
<string name="camera_permission_required">கேமரா இசைவு தேவை</string>
<string name="status">நிலை</string>
<string name="bio_auth_title">பயோமெட்ரிக் ஏற்பு</string>
<string name="nothing_here_yet">இன்னும் இங்கே எதுவும் இல்லை!</string>
<string name="export_success">ஏற்றுமதி செய்</string>
<string name="download">பதிவிறக்கம்</string>
<string name="check_for_update">புதுப்பிப்புக்கு சரிபார்க்கவும்</string>
<string name="update_check_failed">புதுப்பிப்பு காசோலை தோல்வியடைந்தது.</string>
<string name="checking_for_update">புதுப்பிப்புகளைச் சரிபார்க்கிறது</string>
<string name="latest_installed">நீங்கள் ஏற்கனவே அண்மைக் கால பதிப்பை இயக்குகிறீர்கள்.</string>
<string name="download_and_install">பதிவிறக்கம் செய்து நிறுவவும்</string>
<string name="permission_required">இசைவு தேவை</string>
<string name="install_updated_permission">புதுப்பிப்புகளை நிறுவ இந்த பயன்பாட்டிற்கு இசைவு தேவை.</string>
<string name="licenses">உரிமங்கள்</string>
<string name="update_check_unsupported">புதுப்பிப்பு காசோலை இந்த உருவாக்க வகையை ஆதரிக்கவில்லை.</string>
</resources> </resources>
+53 -32
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="peer">پیر</string> <string name="peer">پیر</string>
<string name="add_from_qr">QR کوڈ سے شامل کریں۔</string> <string name="add_from_qr">کیو آر کوڈ سے شامل کریں</string>
<string name="always_on_vpn_support">ہمیشہ آن VPN کی اجازت دیں۔</string> <string name="always_on_vpn_support">ہمیشہ آن VPN کی اجازت دیں۔</string>
<string name="add_peer">ساتھی شامل کریں۔</string> <string name="add_peer">ساتھی شامل کریں۔</string>
<string name="turn_off_tunnel">کارروائی کے لیے سرنگ بند کی ضرورت ہے۔</string> <string name="turn_off_tunnel">کارروائی کے لیے سرنگ بند کی ضرورت ہے۔</string>
@@ -16,11 +16,11 @@
<string name="app_name">ڈبلیو جی ٹنل</string> <string name="app_name">ڈبلیو جی ٹنل</string>
<string name="error_file_extension">فائل conf یا zip نہیں ہے۔</string> <string name="error_file_extension">فائل conf یا zip نہیں ہے۔</string>
<string name="no_tunnels">ابھی تک کوئی سرنگیں شامل نہیں کی گئیں!</string> <string name="no_tunnels">ابھی تک کوئی سرنگیں شامل نہیں کی گئیں!</string>
<string name="tunnels">سرنگیں۔</string> <string name="tunnels">ٹنلز</string>
<string name="tunnel_mobile_data">موبائل ڈیٹا پر سرنگ</string> <string name="tunnel_mobile_data">موبائل ڈیٹا پر ٹنل</string>
<string name="privacy_policy">رازداری کی پالیسی دیکھیں</string> <string name="privacy_policy">رازداری کی پالیسی دیکھیں</string>
<string name="okay">ٹھیک ہے۔</string> <string name="okay">ٹھیک ہے۔</string>
<string name="tunnel_on_ethernet">ایتھرنیٹ پر سرنگ</string> <string name="tunnel_on_ethernet">ایتھرنیٹ پر ٹنل</string>
<string name="thank_you">ڈبلیو جی ٹنل استعمال کرنے کا شکریہ!</string> <string name="thank_you">ڈبلیو جی ٹنل استعمال کرنے کا شکریہ!</string>
<string name="trusted_ssid_value_description">SSID جمع کروائیں۔</string> <string name="trusted_ssid_value_description">SSID جمع کروائیں۔</string>
<string name="open_file">فائل کھولیں۔</string> <string name="open_file">فائل کھولیں۔</string>
@@ -37,7 +37,7 @@
<string name="location_services_not_detected">مقام کی خدمات کا پتہ نہیں چلا</string> <string name="location_services_not_detected">مقام کی خدمات کا پتہ نہیں چلا</string>
<string name="vpn_on">VPN آن</string> <string name="vpn_on">VPN آن</string>
<string name="vpn_off">VPN آف ہے۔</string> <string name="vpn_off">VPN آف ہے۔</string>
<string name="turn_on_tunnel">کارروائی کے لیے فعال سرنگ کی ضرورت ہے۔</string> <string name="turn_on_tunnel">کارروائی کے لیے فعال ٹنل کی ضرورت ہے</string>
<string name="interface_">انٹرفیس</string> <string name="interface_">انٹرفیس</string>
<string name="private_key">نجی کلید</string> <string name="private_key">نجی کلید</string>
<string name="base64_key">بیس 64 کلید</string> <string name="base64_key">بیس 64 کلید</string>
@@ -48,7 +48,7 @@
<string name="error_authentication_failed">تصدیق ناکام ہوگئی</string> <string name="error_authentication_failed">تصدیق ناکام ہوگئی</string>
<string name="error_authorization_failed">اجازت دینے میں ناکام</string> <string name="error_authorization_failed">اجازت دینے میں ناکام</string>
<string name="enabled_app_shortcuts">ایپ شارٹ کٹس کو فعال کریں۔</string> <string name="enabled_app_shortcuts">ایپ شارٹ کٹس کو فعال کریں۔</string>
<string name="tunnel_on_wifi">ناقابل اعتماد وائی فائی پر سرنگ</string> <string name="tunnel_on_wifi">ناقابل اعتماد وائی فائی پر ٹنل</string>
<string name="email_subject">ڈبلیو جی ٹنل سپورٹ</string> <string name="email_subject">ڈبلیو جی ٹنل سپورٹ</string>
<string name="docs_description">دستاویزات پڑھیں</string> <string name="docs_description">دستاویزات پڑھیں</string>
<string name="email_description">مجھے ایک ای میل بھیجیں۔</string> <string name="email_description">مجھے ایک ای میل بھیجیں۔</string>
@@ -56,9 +56,9 @@
<string name="error_ssid_exists">SSID پہلے سے موجود ہے۔</string> <string name="error_ssid_exists">SSID پہلے سے موجود ہے۔</string>
<string name="error_root_denied">روٹ شیل سے انکار کر دیا گیا۔</string> <string name="error_root_denied">روٹ شیل سے انکار کر دیا گیا۔</string>
<string name="error_no_file_explorer">کوئی فائل ایکسپلورر انسٹال نہیں ہے۔</string> <string name="error_no_file_explorer">کوئی فائل ایکسپلورر انسٹال نہیں ہے۔</string>
<string name="auto_tunnel_title">آٹو ٹنل سروس</string> <string name="auto_tunnel_title">خودکار ٹنل سروس</string>
<string name="delete_tunnel">سرنگ کو حذف کریں۔</string> <string name="delete_tunnel">ٹنل کو حذف کریں</string>
<string name="delete_tunnel_message">کیا آپ واقعی اس سرنگ کو حذف کرنا چاہیں گے؟</string> <string name="delete_tunnel_message">کیا آپ واقعی اس ٹنل کو حذف کرنا چاہیں گے؟</string>
<string name="yes">جی ہاں</string> <string name="yes">جی ہاں</string>
<string name="all">تمام</string> <string name="all">تمام</string>
<string name="no_email_detected">کوئی ای میل ایپ نہیں ملی</string> <string name="no_email_detected">کوئی ای میل ایپ نہیں ملی</string>
@@ -70,14 +70,14 @@
<string name="pin_created">پن کامیابی کے ساتھ بن گیا۔</string> <string name="pin_created">پن کامیابی کے ساتھ بن گیا۔</string>
<string name="create_pin">پن بنائیں</string> <string name="create_pin">پن بنائیں</string>
<string name="restart_on_ping">پنگ فیل پر دوبارہ شروع کریں (بی ٹا)</string> <string name="restart_on_ping">پنگ فیل پر دوبارہ شروع کریں (بی ٹا)</string>
<string name="set_primary_tunnel">بنیادی سرنگ کے طور پر سیٹ کریں۔</string> <string name="set_primary_tunnel">بنیادی ٹنل کے طور پر سیٹ کریں</string>
<string name="version">ورژن</string> <string name="version">ورژن</string>
<string name="junk_packet_count">ردی کے پیکٹ کی گنتی</string> <string name="junk_packet_count">ردی کے پیکٹ کی گنتی</string>
<string name="init_packet_junk_size">Init پیکٹ ردی کا سائز</string> <string name="init_packet_junk_size">Init پیکٹ ردی کا سائز</string>
<string name="init_packet_magic_header">Init پیکٹ میجک ہیڈر</string> <string name="init_packet_magic_header">Init پیکٹ میجک ہیڈر</string>
<string name="transport_packet_magic_header">ٹرانسپورٹ پیکٹ میجک ہیڈر</string> <string name="transport_packet_magic_header">ٹرانسپورٹ پیکٹ میجک ہیڈر</string>
<string name="see_the">دیکھیں</string> <string name="see_the">دیکھیں</string>
<string name="error_file_format">غلط سرنگ کنفیگریشن فارمیٹ</string> <string name="error_file_format">غلط ٹنل کنفیگریشن فارمیٹ</string>
<string name="vpn_denied_dialog_title">اجازت نامنظور</string> <string name="vpn_denied_dialog_title">اجازت نامنظور</string>
<string name="vpn_settings">VPN سسٹم کی ترتیبات</string> <string name="vpn_settings">VPN سسٹم کی ترتیبات</string>
<string name="always_on_message">VPN کنکشن کی اجازت مسترد کر دی گئی ہے۔ براہ کرم چیک کریں۔</string> <string name="always_on_message">VPN کنکشن کی اجازت مسترد کر دی گئی ہے۔ براہ کرم چیک کریں۔</string>
@@ -95,17 +95,17 @@
<string name="language">زبان</string> <string name="language">زبان</string>
<string name="display_theme">ڈسپلے تھیم</string> <string name="display_theme">ڈسپلے تھیم</string>
<string name="trusted_wifi_names">قابل اعتماد وائی فائی نام</string> <string name="trusted_wifi_names">قابل اعتماد وائی فائی نام</string>
<string name="primary_tunnel">بنیادی سرنگ</string> <string name="primary_tunnel">بنیادی ٹنل</string>
<string name="mobile_tunnel">موبائل ڈیٹا ٹنل</string> <string name="mobile_tunnel">موبائل ڈیٹا ٹنل</string>
<string name="skip">چھوڑیں۔</string> <string name="skip">چھوڑیں۔</string>
<string name="learn_more">مزید جانیں</string> <string name="learn_more">مزید جانیں</string>
<string name="kernel_not_supported">کرنل تعاون یافتہ نہیں ہے۔</string> <string name="kernel_not_supported">کرنل تعاون یافتہ نہیں ہے۔</string>
<string name="start_auto">آٹو ٹنل شروع کریں۔</string> <string name="start_auto">خودکار ٹنل شروع کریں</string>
<string name="donate">پروجیکٹ کے لیے عطیہ کریں۔</string> <string name="donate">پروجیکٹ کے لیے عطیہ کریں۔</string>
<string name="local_logging">مقامی لاگنگ</string> <string name="local_logging">مقامی لاگنگ</string>
<string name="enable_local_logging">مقامی لاگنگ کو فعال کریں۔</string> <string name="enable_local_logging">مقامی لاگنگ کو فعال کریں۔</string>
<string name="stop_on_no_internet">بغیر انٹرنیٹ پر رکیں۔</string> <string name="stop_on_no_internet">بغیر انٹرنیٹ پر رکیں۔</string>
<string name="stop_on_internet_loss">انٹرنیٹ کے نقصان پر سرنگ کو روکیں۔</string> <string name="stop_on_internet_loss">انٹرنیٹ بند ہونے پر ٹنل روکیں</string>
<string name="vpn_kill_switch">VPN کِل سوئچ</string> <string name="vpn_kill_switch">VPN کِل سوئچ</string>
<string name="kill_switch_options">کِل سوئچ کے اختیارات</string> <string name="kill_switch_options">کِل سوئچ کے اختیارات</string>
<string name="allow_lan_traffic">LAN ٹریفک کی اجازت دیں۔</string> <string name="allow_lan_traffic">LAN ٹریفک کی اجازت دیں۔</string>
@@ -122,22 +122,22 @@
<string name="hide_amnezia_properties">Amnezia کی خصوصیات کو چھپائیں۔</string> <string name="hide_amnezia_properties">Amnezia کی خصوصیات کو چھپائیں۔</string>
<string name="exclude_lan">LAN کو خارج کریں۔</string> <string name="exclude_lan">LAN کو خارج کریں۔</string>
<string name="include_lan">LAN شامل کریں۔</string> <string name="include_lan">LAN شامل کریں۔</string>
<string name="auto_tunnel">خودکار سرنگ</string> <string name="auto_tunnel">خودکار ٹنل</string>
<string name="kill_switch_off">قابل اعتماد پر کِل سوئچ کو بند کریں۔</string> <string name="kill_switch_off">قابل اعتماد پر کِل سوئچ کو بند کریں۔</string>
<string name="server_ipv4">IPv4 میزبان نام کی قرارداد</string> <string name="server_ipv4">IPv4 میزبان نام کی قرارداد</string>
<string name="multiple">متعدد</string> <string name="multiple">متعدد</string>
<string name="prominent_background_location_message">اس فیچر کو ایپلیکیشن بند ہونے کے باوجود بھی Wi-Fi SSID مانیٹرنگ کو فعال کرنے کے لیے پس منظر کی جگہ کی اجازت درکار ہے۔ مزید تفصیلات کے لیے، براہ کرم سپورٹ اسکرین پر منسلک رازداری کی پالیسی دیکھیں۔</string> <string name="prominent_background_location_message">اس فیچر کو ایپلیکیشن بند ہونے کے باوجود بھی Wi-Fi SSID مانیٹرنگ کو فعال کرنے کے لیے پس منظر کی جگہ کی اجازت درکار ہے۔ مزید تفصیلات کے لیے، براہ کرم سپورٹ اسکرین پر منسلک رازداری کی پالیسی دیکھیں۔</string>
<string name="email_chooser">ای میل بھیجیں…</string> <string name="email_chooser">ای میل بھیجیں…</string>
<string name="add_tunnels_text">فائل یا زپ سے شامل کریں۔</string> <string name="add_tunnels_text">فائل یا زپ سے شامل کریں</string>
<string name="config_changes_saved">کنفیگریشن تبدیلیاں محفوظ ہو گئیں۔</string> <string name="config_changes_saved">کنفیگریشن تبدیلیاں محفوظ ہو گئیں۔</string>
<string name="auto_tunneling">خودکار سرنگ</string> <string name="auto_tunneling">خودکار ٹنلنگ</string>
<string name="create_import">شروع سے تخلیق کریں۔</string> <string name="create_import">شروع سے تخلیق کریں۔</string>
<string name="unknown_error">نامعلوم خرابی پیش آگئی</string> <string name="unknown_error">نامعلوم خرابی پیش آگئی</string>
<string name="seconds">سیکنڈ</string> <string name="seconds">سیکنڈ</string>
<string name="persistent_keepalive">مسلسل زندہ رہنا</string> <string name="persistent_keepalive">مسلسل زندہ رہنا</string>
<string name="remove_amnezia_compatibility">Amnezia مطابقت کو ہٹا دیں۔</string> <string name="remove_amnezia_compatibility">Amnezia مطابقت کو ہٹا دیں۔</string>
<string name="light">روشنی</string> <string name="light">روشنی</string>
<string name="set_ethernet_tunnel">ایتھرنیٹ ٹنل کے طور پر سیٹ کریں۔</string> <string name="set_ethernet_tunnel">ایتھرنیٹ ٹنل کے طور پر سیٹ کریں</string>
<string name="launch_app_settings">ایپ کی ترتیبات شروع کریں۔</string> <string name="launch_app_settings">ایپ کی ترتیبات شروع کریں۔</string>
<string name="monitoring_state_changes">ریاستی تبدیلیوں کی نگرانی</string> <string name="monitoring_state_changes">ریاستی تبدیلیوں کی نگرانی</string>
<string name="set_custom_ping_internal">پنگ وقفہ (سیکنڈ)</string> <string name="set_custom_ping_internal">پنگ وقفہ (سیکنڈ)</string>
@@ -148,13 +148,13 @@
<string name="handshake">مصافحہ</string> <string name="handshake">مصافحہ</string>
<string name="response_packet_magic_header">رسپانس پیکٹ میجک ہیڈر</string> <string name="response_packet_magic_header">رسپانس پیکٹ میجک ہیڈر</string>
<string name="unsure_how">اگر آپ کو یقین نہیں ہے کہ کیسے آگے بڑھیں۔</string> <string name="unsure_how">اگر آپ کو یقین نہیں ہے کہ کیسے آگے بڑھیں۔</string>
<string name="use_tunnel_on_wifi_name">وائی فائی نام پر ٹنل استعمال کریں۔</string> <string name="use_tunnel_on_wifi_name">وائی فائی نام پر ٹنل استعمال کریں</string>
<string name="optional_default">"اختیاری، ڈیفالٹ: "</string> <string name="optional_default">"اختیاری، ڈیفالٹ: "</string>
<string name="use_root_shell_for_wifi">وائی فائی کا نام حاصل کرنے کے لیے روٹ شیل کا استعمال کریں۔</string> <string name="use_root_shell_for_wifi">وائی فائی کا نام حاصل کرنے کے لیے روٹ شیل کا استعمال کریں۔</string>
<string name="response_packet_junk_size">رسپانس پیکٹ ردی کا سائز</string> <string name="response_packet_junk_size">رسپانس پیکٹ ردی کا سائز</string>
<string name="junk_packet_minimum_size">جنک پیکٹ کا کم از کم سائز</string> <string name="junk_packet_minimum_size">جنک پیکٹ کا کم از کم سائز</string>
<string name="mobile_data_tunnel">موبائل ڈیٹا ٹنل کے طور پر سیٹ کریں۔</string> <string name="mobile_data_tunnel">موبائل ڈیٹا ٹنل کے طور پر سیٹ کریں</string>
<string name="edit_tunnel">سرنگ میں ترمیم کریں۔</string> <string name="edit_tunnel">ٹنل میں ترمیم کریں</string>
<string name="enable_app_lock">ایپ لاک کو فعال کریں۔</string> <string name="enable_app_lock">ایپ لاک کو فعال کریں۔</string>
<string name="logs">نوشتہ جات</string> <string name="logs">نوشتہ جات</string>
<string name="kernel">کرنل</string> <string name="kernel">کرنل</string>
@@ -167,9 +167,9 @@
<string name="kill_switch">کِل سوئچ</string> <string name="kill_switch">کِل سوئچ</string>
<string name="prefer_ipv4">IPv4 کنکشن کو ترجیح دیں۔</string> <string name="prefer_ipv4">IPv4 کنکشن کو ترجیح دیں۔</string>
<string name="native_kill_switch">مقامی قتل سوئچ</string> <string name="native_kill_switch">مقامی قتل سوئچ</string>
<string name="auto_tunnel_channel_description">خودکار سرنگ ریاست کی اطلاعات کے لیے ایک چینل</string> <string name="auto_tunnel_channel_description">خودکار ٹنل اسٹیٹ کی اطلاعات کے لیے ایک چینل</string>
<string name="use_wildcards">نام وائلڈ کارڈ استعمال کریں۔</string> <string name="use_wildcards">نام وائلڈ کارڈ استعمال کریں۔</string>
<string name="stop_auto">آٹو ٹنل بند کرو</string> <string name="stop_auto">خودکار ٹنل روکیں</string>
<string name="add_wifi_name">وائی فائی کا نام شامل کریں۔</string> <string name="add_wifi_name">وائی فائی کا نام شامل کریں۔</string>
<string name="wildcards_active">وائلڈ کارڈز فعال</string> <string name="wildcards_active">وائلڈ کارڈز فعال</string>
<string name="bypass_lan_for_kill_switch">کِل سوئچ کے لیے LAN کو بائی پاس کریں۔</string> <string name="bypass_lan_for_kill_switch">کِل سوئچ کے لیے LAN کو بائی پاس کریں۔</string>
@@ -178,9 +178,9 @@
<string name="underload_packet_magic_header">پیکٹ میجک ہیڈر کو انڈر لوڈ کریں۔</string> <string name="underload_packet_magic_header">پیکٹ میجک ہیڈر کو انڈر لوڈ کریں۔</string>
<string name="background_location_message">اس خصوصیت کے لیے ہر وقت مقام کی اجازت اور/یا درست مقام کی ضرورت ہے۔ ملاحظہ فرمائیں</string> <string name="background_location_message">اس خصوصیت کے لیے ہر وقت مقام کی اجازت اور/یا درست مقام کی ضرورت ہے۔ ملاحظہ فرمائیں</string>
<string name="always_on_message2">یہ یقینی بنانے کے لیے کہ ہمیشہ آن VPN دیگر تمام ایپس کے لیے بند ہے اور دوبارہ کوشش کریں۔</string> <string name="always_on_message2">یہ یقینی بنانے کے لیے کہ ہمیشہ آن VPN دیگر تمام ایپس کے لیے بند ہے اور دوبارہ کوشش کریں۔</string>
<string name="ethernet_tunnel">ایتھرنیٹ سرنگ</string> <string name="ethernet_tunnel">ایتھرنیٹ ٹنل</string>
<string name="auto_tunnel_channel_name">آٹو ٹنل نوٹیفکیشن چینل</string> <string name="auto_tunnel_channel_name">خودکار ٹنل نوٹیفکیشن چینل</string>
<string name="tunnel_running">ٹنل چل رہا ہے۔</string> <string name="tunnel_running">ٹنل چل رہا ہے</string>
<string name="tunnel_control">ٹنل کنٹرول</string> <string name="tunnel_control">ٹنل کنٹرول</string>
<string name="vpn_channel_description">VPN ریاستی اطلاعات کے لیے ایک چینل</string> <string name="vpn_channel_description">VPN ریاستی اطلاعات کے لیے ایک چینل</string>
<string name="hide_scripts">اسکرپٹ چھپائیں۔</string> <string name="hide_scripts">اسکرپٹ چھپائیں۔</string>
@@ -189,7 +189,7 @@
<string name="enable_remote_app_control">ریموٹ ایپ کنٹرول کو فعال کریں۔</string> <string name="enable_remote_app_control">ریموٹ ایپ کنٹرول کو فعال کریں۔</string>
<string name="kernel_name_error">کرنل ماڈیول نام کی خرابی۔</string> <string name="kernel_name_error">کرنل ماڈیول نام کی خرابی۔</string>
<string name="app_permission_title">ڈبلیو جی ٹنل کنٹرول برج</string> <string name="app_permission_title">ڈبلیو جی ٹنل کنٹرول برج</string>
<string name="app_permission_description">سرنگوں اور خودکار سرنگ کی خصوصیات کو کنٹرول کریں۔</string> <string name="app_permission_description">ٹنل اور خودکار ٹنل کی خصوصیات کو کنٹرول کریں۔</string>
<string name="add_from_url">یو آر ایل سے شامل کریں۔</string> <string name="add_from_url">یو آر ایل سے شامل کریں۔</string>
<string name="enter_config_url">ترتیب یو آر ایل درج کریں۔</string> <string name="enter_config_url">ترتیب یو آر ایل درج کریں۔</string>
<string name="error_download_failed">کنفیگریشن ڈاؤن لوڈ کرنے میں ناکام</string> <string name="error_download_failed">کنفیگریشن ڈاؤن لوڈ کرنے میں ناکام</string>
@@ -199,15 +199,15 @@
<string name="join_telegram">ٹیلیگرام کمیونٹی میں شامل ہوں۔</string> <string name="join_telegram">ٹیلیگرام کمیونٹی میں شامل ہوں۔</string>
<string name="matrix_url">https://matrix.to/#/#wg-tunnel-space:matrix.org</string> <string name="matrix_url">https://matrix.to/#/#wg-tunnel-space:matrix.org</string>
<string name="dropdown">ڈراپ ڈاؤن</string> <string name="dropdown">ڈراپ ڈاؤن</string>
<string name="add_tunnel">سرنگ شامل کریں۔</string> <string name="add_tunnel">ٹنل شامل کریں</string>
<string name="export_logs">ذخیرہ شدہ نوشتہ جات برآمد کریں۔</string> <string name="export_logs">ذخیرہ شدہ نوشتہ جات برآمد کریں۔</string>
<string name="copy">نقل</string> <string name="copy">نقل</string>
<string name="info">معلومات</string> <string name="info">معلومات</string>
<string name="export_tunnels_wireguard">وائر گارڈ کے بطور سرنگیں برآمد کریں۔</string> <string name="export_tunnels_wireguard">وائر گارڈ کے بطور ٹنلز برآمد کریں</string>
<string name="delete">حذف کریں۔</string> <string name="delete">حذف کریں۔</string>
<string name="camera_permission_required">کیمرے کی اجازت درکار ہے۔</string> <string name="camera_permission_required">کیمرے کی اجازت درکار ہے۔</string>
<string name="export_failed">ایکسپورٹ ناکام ہو گیا۔</string> <string name="export_failed">ایکسپورٹ ناکام ہو گیا۔</string>
<string name="tunnel_error_template">سرنگ اس کے ساتھ ناکام ہوگئی: %1$s</string> <string name="tunnel_error_template">ٹنل اس کے ساتھ ناکام ہوگئی: %1$s</string>
<string name="wifi_name_template">فعال: %1$s</string> <string name="wifi_name_template">فعال: %1$s</string>
<string name="config_error">کنفگریشن کی خرابی</string> <string name="config_error">کنفگریشن کی خرابی</string>
<string name="dns_resolve_error">ڈی این ایس ریزولوشن کی خرابی۔</string> <string name="dns_resolve_error">ڈی این ایس ریزولوشن کی خرابی۔</string>
@@ -220,11 +220,32 @@
<string name="bio_not_supported">بایومیٹرکس تعاون یافتہ نہیں ہیں۔</string> <string name="bio_not_supported">بایومیٹرکس تعاون یافتہ نہیں ہیں۔</string>
<string name="bio_not_created">بائیو میٹرکس نہیں بنائے گئے۔</string> <string name="bio_not_created">بائیو میٹرکس نہیں بنائے گئے۔</string>
<string name="bio_update_required">بائیو میٹرک سیکیورٹی اپ ڈیٹ درکار ہے۔</string> <string name="bio_update_required">بائیو میٹرک سیکیورٹی اپ ڈیٹ درکار ہے۔</string>
<string name="tunnel_starting">سرنگ شروع ہو رہی ہے۔</string> <string name="tunnel_starting">ٹنل چل رہی ہے</string>
<string name="nothing_here_yet">ابھی تک یہاں کچھ نہیں!</string> <string name="nothing_here_yet">ابھی تک یہاں کچھ نہیں!</string>
<string name="join_matrix">میٹرکس کمیونٹی میں شامل ہوں۔</string> <string name="join_matrix">میٹرکس کمیونٹی میں شامل ہوں۔</string>
<string name="delete_logs">نوشتہ جات کو حذف اور صاف کریں۔</string> <string name="delete_logs">نوشتہ جات کو حذف اور صاف کریں۔</string>
<string name="export_tunnels_amnezia">ایمنیزیا کے طور پر سرنگیں برآمد کریں۔</string> <string name="export_tunnels_amnezia">ایمنیزیا کے طور پر ٹنلز برآمد کریں</string>
<string name="bio_subtitle">اپنی بائیو میٹرک اسناد کا استعمال کرتے ہوئے لاگ ان کریں۔</string> <string name="bio_subtitle">اپنی بائیو میٹرک اسناد کا استعمال کرتے ہوئے لاگ ان کریں۔</string>
<string name="bio_auth_title">بائیو میٹرک تصدیق</string> <string name="bio_auth_title">بائیو میٹرک تصدیق</string>
<string name="share">شیئر کریں۔</string>
<string name="select_all">سبھی کو منتخب کریں۔</string>
<string name="check_for_update">اپ ڈیٹ کے لیے چیک کریں۔</string>
<string name="update_check_failed">اپ ڈیٹ چیک ناکام ہو گیا۔</string>
<string name="checking_for_update">اپ ڈیٹس کی جانچ ہو رہی ہے۔</string>
<string name="update_download_failed">اپ ڈیٹ ڈاؤن لوڈ ناکام ہو گیا۔</string>
<string name="update_available">اپ ڈیٹ دستیاب ہے!</string>
<string name="download_and_install">ڈاؤن لوڈ اور انسٹال کریں۔</string>
<string name="permission_required">اجازت درکار ہے۔</string>
<string name="install_updated_permission">اس ایپ کو اپ ڈیٹس انسٹال کرنے کے لیے اجازت درکار ہے۔</string>
<string name="allow">اجازت دیں۔</string>
<string name="licenses">لائسنس</string>
<string name="download">ڈاؤن لوڈ کریں۔</string>
<string name="version_template">ورژن: %1$s</string>
<string name="export_success">برآمد کی کامیابی</string>
<string name="latest_installed">آپ پہلے ہی تازہ ترین ورژن چلا رہے ہیں۔</string>
<string name="darker">گہرا</string>
<string name="amoled">ایمولیڈ</string>
<string name="security_template">سیکیورٹی: %1$s</string>
<string name="update_check_unsupported">اپ ڈیٹ چیک اس قسم کی تعمیر کی حمایت نہیں کرتا ہے۔</string>
<string name="flavor_template">ذائقہ: %1$s</string>
</resources> </resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources></resources> <resources>
<string name="no_tunnels">Chưa có đường nối nào!</string>
<string name="tunnels">Các đường nối</string>
<string name="error_file_extension">Tệp không phải .conf hay .zip</string>
</resources>
@@ -227,4 +227,29 @@
<string name="export_logs">导出已保存的日志</string> <string name="export_logs">导出已保存的日志</string>
<string name="bio_not_supported">生物特征不受支持</string> <string name="bio_not_supported">生物特征不受支持</string>
<string name="nothing_here_yet">此处还什么都没有!</string> <string name="nothing_here_yet">此处还什么都没有!</string>
<string name="select_all">全选</string>
<string name="share">分享</string>
<string name="version_template">当前版本:%1$s</string>
<string name="export_success">导出成功</string>
<string name="download">下载</string>
<string name="check_for_update">检查更新</string>
<string name="update_check_failed">更新检查失败了。</string>
<string name="checking_for_update">正在检查更新</string>
<string name="latest_installed">你已经在运行最新版本。</string>
<string name="update_download_failed">更新下载失败。</string>
<string name="update_available">有更新可用!</string>
<string name="download_and_install">下载并安装</string>
<string name="permission_required">需要权限</string>
<string name="licenses">许可证</string>
<string name="install_updated_permission">本应用需要权限来安装更新。</string>
<string name="allow">允许</string>
<string name="update_check_unsupported">更新检查不支持此构建类型。</string>
<string name="flavor_template">版本架构:%1$s</string>
<string name="security_template">安全性:%1$s</string>
<string name="darker">颜色更深</string>
<string name="amoled">AMOLED</string>
<string name="show_qr">显示二维码</string>
<string name="amnezia">Amnezia</string>
<string name="wireguard">WireGuard</string>
<string name="done">完成</string>
</resources> </resources>
+62 -11
View File
@@ -11,18 +11,18 @@
<string name="name">名稱</string> <string name="name">名稱</string>
<string name="public_key">公鑰</string> <string name="public_key">公鑰</string>
<string name="privacy_policy">查看隱私政策</string> <string name="privacy_policy">查看隱私政策</string>
<string name="tunnels">隧道列表</string> <string name="tunnels">隧道清單</string>
<string name="thank_you">感謝您使用 WG Tunnel!</string> <string name="thank_you">感謝您使用 WG Tunnel</string>
<string name="open_file">開啟檔案</string> <string name="open_file">開啟檔案</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>
<string name="okay"></string> <string name="okay">確認</string>
<string name="qr_scan">掃描 QR code</string> <string name="qr_scan">掃描 QR code</string>
<string name="dns_servers">DNS 伺服器</string> <string name="dns_servers">DNS 伺服器</string>
<string name="tunnel_name">隧道名稱</string> <string name="tunnel_name">隧道名稱</string>
<string name="config_changes_saved">組態變更已儲存。</string> <string name="config_changes_saved">組態變更已儲存。</string>
<string name="exclude">排除</string> <string name="exclude">排除</string>
<string name="include">包含</string> <string name="include">包含</string>
<string name="addresses"></string> <string name="addresses"></string>
<string name="add_tunnels_text">從檔案或 zip 壓縮檔新增</string> <string name="add_tunnels_text">從檔案或 zip 壓縮檔新增</string>
<string name="add_from_qr">從 QR code 新增</string> <string name="add_from_qr">從 QR code 新增</string>
<string name="copy_public_key">複製公鑰</string> <string name="copy_public_key">複製公鑰</string>
@@ -51,7 +51,7 @@
<string name="edit_tunnel">編輯隧道</string> <string name="edit_tunnel">編輯隧道</string>
<string name="kernel">核心</string> <string name="kernel">核心</string>
<string name="vpn_settings">系統 VPN 設定</string> <string name="vpn_settings">系統 VPN 設定</string>
<string name="support"></string> <string name="support"></string>
<string name="getting_started_guide">取得入門指南</string> <string name="getting_started_guide">取得入門指南</string>
<string name="settings">設定</string> <string name="settings">設定</string>
<string name="restart_at_boot">開機時重新啟動</string> <string name="restart_at_boot">開機時重新啟動</string>
@@ -59,22 +59,22 @@
<string name="set_custom_ping_internal">Ping 間隔 (秒)</string> <string name="set_custom_ping_internal">Ping 間隔 (秒)</string>
<string name="app_settings">應用程式設定</string> <string name="app_settings">應用程式設定</string>
<string name="logs">日誌</string> <string name="logs">日誌</string>
<string name="dark"></string> <string name="dark"></string>
<string name="light">亮色</string> <string name="light">亮色</string>
<string name="donate">贈給專案</string> <string name="donate"></string>
<string name="appearance">外觀</string> <string name="appearance">外觀</string>
<string name="display_theme">主題</string> <string name="display_theme">主題</string>
<string name="primary_tunnel">主要隧道</string> <string name="primary_tunnel">主要隧道</string>
<string name="learn_more">了解更多</string> <string name="learn_more">了解更多</string>
<string name="kernel_not_supported">核心不支</string> <string name="kernel_not_supported">核心不支</string>
<string name="notifications">通知</string> <string name="notifications">通知</string>
<string name="dynamic">動態</string> <string name="dynamic">動態</string>
<string name="never">從不</string> <string name="never">從不</string>
<string name="automatic">自動</string> <string name="automatic">自動</string>
<string name="add_wifi_name">新增WiFi SSID</string> <string name="add_wifi_name">新增 WiFi SSID</string>
<string name="allow_lan_traffic">允許 LAN 流量</string> <string name="allow_lan_traffic">允許 LAN 流量</string>
<string name="stop_on_no_internet">沒有連上網路時停止</string> <string name="stop_on_no_internet">沒有連上網路時停止</string>
<string name="stop_on_internet_loss">網路連線斷開時停止隧道</string> <string name="stop_on_internet_loss">網路斷線時停止隧道</string>
<string name="add_from_clipboard">從剪貼簿新增</string> <string name="add_from_clipboard">從剪貼簿新增</string>
<string name="stop">停止</string> <string name="stop">停止</string>
<string name="advanced_settings">進階設定</string> <string name="advanced_settings">進階設定</string>
@@ -92,7 +92,7 @@
<string name="unsure_how">如果您不確定如何進行</string> <string name="unsure_how">如果您不確定如何進行</string>
<string name="see_the">請參閱</string> <string name="see_the">請參閱</string>
<string name="read_logs">查看日誌</string> <string name="read_logs">查看日誌</string>
<string name="local_logging">本地日誌記錄</string> <string name="local_logging">日誌</string>
<string name="enable_local_logging">啟用本地日誌記錄</string> <string name="enable_local_logging">啟用本地日誌記錄</string>
<string name="email_description">給作者寄電子郵件</string> <string name="email_description">給作者寄電子郵件</string>
<string name="always_on_vpn_support">允許永久連線的 VPN</string> <string name="always_on_vpn_support">允許永久連線的 VPN</string>
@@ -125,4 +125,55 @@
<string name="nothing_here_yet">這裡目前還沒有任何東西!</string> <string name="nothing_here_yet">這裡目前還沒有任何東西!</string>
<string name="remove_amnezia_compatibility">移除 Amnezia 相容性</string> <string name="remove_amnezia_compatibility">移除 Amnezia 相容性</string>
<string name="enable_amnezia_compatibility">啟用 Amnezia 相容性</string> <string name="enable_amnezia_compatibility">啟用 Amnezia 相容性</string>
<string name="comma_separated_list">以逗號分隔的清單</string>
<string name="show_amnezia_properties">顯示 Amnezia 屬性</string>
<string name="background_location_message2">請確保啟用了這些權限</string>
<string name="set_custom_ping_ip">自訂 ping 目標 IP</string>
<string name="hide_amnezia_properties">隱藏 Amnezia 屬性</string>
<string name="active">使用中</string>
<string name="vpn_denied_dialog_title">拒絕存取</string>
<string name="always_on_message">VPN 連線所需權限被拒絕,請檢查</string>
<string name="wildcards_active">啟用萬用字元</string>
<string name="multiple">多個</string>
<string name="export_tunnels_amnezia">將隧道匯出為 Amnezia</string>
<string name="export_tunnels_wireguard">將隧道匯出為 WireGuard</string>
<string name="wifi_name_template">使用中: %1$s</string>
<string name="inactive">未使用</string>
<string name="trusted_wifi_names">信任的 WiFi SSID</string>
<string name="set_custom_ping_cooldown">Ping 重新開始冷卻時間 (秒)</string>
<string name="auto_tunnel">自動隧道管理</string>
<string name="auto_tunnel_title">自動隧道管理服務</string>
<string name="restart_on_ping">Ping 失敗後重新啟動隧道 (beta)</string>
<string name="select_all">全選</string>
<string name="share">分享</string>
<string name="start_auto">開始自動隧道管理</string>
<string name="auto_tunneling">自動隧道管理</string>
<string name="stop_auto">停止自動隧道管理</string>
<string name="debounce_delay">防抖延遲</string>
<string name="tunnel_control">隧道控制</string>
<string name="export_logs">匯出已儲存的日誌</string>
<string name="delete_logs">刪除並清理日誌</string>
<string name="monitoring_state_changes">監控狀態變化</string>
<string name="use_root_shell_for_wifi">透過 root 權限取得 WiFi SSID</string>
<string name="splt_tunneling">隧道拆分</string>
<string name="tunnel_specific_settings">隧道個別設定</string>
<string name="use_wildcards">SSID 使用萬用字元</string>
<string name="allow">允許</string>
<string name="check_for_update">檢查更新</string>
<string name="version_template">版本: %1$s</string>
<string name="update_available">有可用更新!</string>
<string name="update_download_failed">下載更新失敗。</string>
<string name="latest_installed">您正在執行最新版本。</string>
<string name="checking_for_update">正在檢查更新</string>
<string name="export_success">匯出成功</string>
<string name="install_updated_permission">此應用程式需要權限才能安裝更新。</string>
<string name="permission_required">需要權限</string>
<string name="download">下載</string>
<string name="licenses">授權條款</string>
<string name="update_check_failed">檢查更新失敗。</string>
<string name="download_and_install">下載並安裝</string>
<string name="app_permission_description">控制隧道和自動隧道管理功能。</string>
<string name="amoled">AMOLED</string>
<string name="update_check_unsupported">更新檢查不支援此建置類型。</string>
<string name="darker">黑色</string>
</resources> </resources>
+7
View File
@@ -221,6 +221,7 @@
<string name="wifi_name_template">Active: %1$s</string> <string name="wifi_name_template">Active: %1$s</string>
<string name="remote_key_template">Key: %1$s</string> <string name="remote_key_template">Key: %1$s</string>
<string name="version_template">Version: %1$s</string> <string name="version_template">Version: %1$s</string>
<string name="security_template">Security: %1$s</string>
<string name="flavor_template">Flavor: %1$s</string> <string name="flavor_template">Flavor: %1$s</string>
<string name="config_error">config error</string> <string name="config_error">config error</string>
<string name="dns_resolve_error">dns resolution error</string> <string name="dns_resolve_error">dns resolution error</string>
@@ -255,4 +256,10 @@
<string name="allow">Allow</string> <string name="allow">Allow</string>
<string name="licenses">Licenses</string> <string name="licenses">Licenses</string>
<string name="update_check_unsupported">Update check not supported this build type.</string> <string name="update_check_unsupported">Update check not supported this build type.</string>
<string name="darker">Darker</string>
<string name="amoled">AMOLED</string>
<string name="show_qr">Show QR</string>
<string name="amnezia">Amnezia</string>
<string name="wireguard">WireGuard</string>
<string name="done">Done</string>
</resources> </resources>
+2
View File
@@ -6,6 +6,8 @@
<item name="android:colorPrimary">@color/background</item> <item name="android:colorPrimary">@color/background</item>
<item name="android:windowAllowReturnTransitionOverlap">true</item> <item name="android:windowAllowReturnTransitionOverlap">true</item>
<item name="android:windowAllowEnterTransitionOverlap">true</item> <item name="android:windowAllowEnterTransitionOverlap">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style> </style>
<style name="Theme.App.Start" parent="@style/Theme.SplashScreen"> <style name="Theme.App.Start" parent="@style/Theme.SplashScreen">
+1 -1
View File
@@ -8,6 +8,6 @@ repositories {
} }
dependencies { dependencies {
implementation("org.semver4j:semver4j:5.6.0") implementation("org.semver4j:semver4j:5.7.0")
implementation("org.ajoberstar.grgit:grgit-core:5.3.0") implementation("org.ajoberstar.grgit:grgit-core:5.3.0")
} }
+2 -2
View File
@@ -1,7 +1,7 @@
object Constants { object Constants {
const val VERSION_NAME = "3.8.3" const val VERSION_NAME = "3.9.1"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 38300 const val VERSION_CODE = 39100
const val TARGET_SDK = 35 const val TARGET_SDK = 35
const val MIN_SDK = 26 const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_ID = "com.zaneschepke.wireguardautotunnel"
+2 -2
View File
@@ -80,8 +80,8 @@ fun Project.computeVersionName(): String {
// Bump minor for pre-release // Bump minor for pre-release
val preReleaseVersion = Semver.of( val preReleaseVersion = Semver.of(
baseVersion.major, baseVersion.major,
baseVersion.minor + 1, baseVersion.minor,
0 0 + 1,
) )
"${preReleaseVersion}-beta+git.${getGitCommitHash()}" "${preReleaseVersion}-beta+git.${getGitCommitHash()}"
} }
@@ -0,0 +1,6 @@
Co je nového:
- Podpora více tunelů pro režim jádra
- Přepsání WG výchozí preference DNS IPv4
- Zastavení funkce kill switch na důvěryhodných
- Omezení dotazů na polohu automatickým tunelem
- Různé opravy chyb a vylepšení
@@ -0,0 +1,5 @@
Co je nového:
- Oprava chyby statického koncového bodu IPv6 peer
- Oprava chyby dynamických zkratek
- Lokalizace
- Oprava chyby přidávání peerů
@@ -0,0 +1,5 @@
Co je nového:
- Oprava regrese automatického tunelu
- Vylepšení synchronizace dlaždic
- Optimalizace dotazování na název sítě Wi-Fi
- Vylení kontrol oprávnění pro monitorování sítě
@@ -0,0 +1,8 @@
Co je nového:
- Aktualizace uživatelského rozhraní
- Přidání tunelu prostřednictvím adresy URL
- Opravy chyb funkce VPN kill switch
- Opravy pádů služby tunelu
- Opravy pingu a restartu při změně konfigurace
- Informace o síti v uživatelském rozhraní
- Různé další opravy a vylepšení
@@ -0,0 +1,6 @@
Co je nového:
- Nová integrace záměrů vzdálených aplikací
- Kopírování názvu aktivní sítě Wi-Fi do schránky
- Oprava chyby při vypínání tunelu
- Oprava chyby výchozího serveru DNS, která upřednostňovala DNS telefonu
- Oprava konfigurací statických peer hostitelů IPv6
@@ -0,0 +1,8 @@
Co je nového:
- Nová ikona aplikace a drobná vylepšení uživatelského rozhraní
- Oprava chyby v přepínání místních záznamníků protokolu
- Oprava restartu AndroidTV při restartu počítače
- Oprava výběru jazyka AndroidTV
- Oprava funkce ping spuštěné ve výchozím nastavení
- Oprava chyby při vypínání režimu jádra
- Oprava chyby automatického tunelování kill switch
@@ -0,0 +1,6 @@
Co je nového:
- Oprava problémů s překladem DNS
- Vylepšení obnovení Android TV při uspání
- Opravena chyba s vyhledáváním děleného tunelu
- Aktualizace lokalizace
- Aktualizovány odkazy na dokumentaci k nové webové stránce aplikace
@@ -0,0 +1,6 @@
Co je nového:
- Opraveny bannery v Android TV
- Přidán vícenásobný výběr tunelů
- Přidána kontrola aktualizací v aplikaci
- Přidána obrazovka s licencí
- Opravy různých chyb
@@ -0,0 +1,6 @@
Co je nového:
- Opravena chyba DNS způsobující nefunkční připojení tunelu
- Opravena chyba zaseknutí tunelu při spuštění
- Přidán QR kód pro zobrazení tunelu
- Přidán přeložný koncový bod do statistik
- Různé další opravy chyb
@@ -0,0 +1,8 @@
Was ist neu?
- Neues App-Symbol und kleinere UI-Verbesserungen
- Behebung eines Fehlers beim Umschalten des lokalen Loggers
- Fix für den Neustart von AndroidTV beim Neustart
- Fix für die AndroidTV-Sprachauswahl
- Fix für die standardmäßig laufende Ping-Funktion
- Fix für Fehler beim Herunterfahren des Kernel-Modus
- Fix für den Fehler beim automatischen Tunneln des Notschalters
@@ -0,0 +1,6 @@
Was ist neu?
- Behebung von Problemen mit der DNS-Auflösung
- Verbesserte Wiederherstellung von Android TV im Ruhezustand
- Fehler bei der geteilten Tunnelsuche behoben
- Aktualisierungen der Lokalisierung
- Aktualisierung der Dokumentationslinks für die neue App-Website
@@ -0,0 +1,6 @@
Was ist neu?
- Android TV-Banner korrigiert
- Mehrfachauswahl für Tunnel hinzugefügt
- In-App-Update-Prüfung hinzugefügt
- Lizenzbildschirm hinzugefügt
- Verschiedene Fehlerbehebungen
@@ -0,0 +1,6 @@
Was ist neu?
- Behebung des DNS-Fehlers bei fehlender Tunnelkonnektivität
- Fehler beim Starten des Tunnels behoben
- Tunnel-QR-Code anzeigen hinzugefügt
- Aufgelösten Endpunkt zur Statistik hinzugefügt
- Verschiedene andere Fehlerkorrekturen
@@ -0,0 +1,6 @@
What's new:
- Fix Android TV Banners
- Add multi-select for tunnels
- Add in-app update checker
- Add license screen
- Various bug fixes
@@ -0,0 +1,6 @@
What's new:
- Fix tunnel no connectivity DNS bug
- Fix tunnel stuck starting bug
- Add view tunnel QR code
- Add resolved endpoint to statistics
- Various other bug fixes
@@ -0,0 +1 @@
Egy alternatív VPN kliens alkalmazás a WireGuard számára több funkcióval
@@ -0,0 +1 @@
WG Tunnel
@@ -0,0 +1,6 @@
Cosa c'è di nuovo:
- Supporto tunnel multipli in modalità kermel.
- Possibilità di ignorare le preferenze Ipv4 di default per WG.
- Disabilitare kill switch su dispositivi fidati.
- Limitazione richieste di localizzazione dalla funzione tunnel automatica.
- Vari bug fix e miglioramenti
@@ -0,0 +1,5 @@
Cosa c'è di nuovo:
- Correzione bug configurazione endpoint peer IPV6 statico
- Correzione bug scorciatoie dinamiche
- Localizzazione
- Correzione bug peer
@@ -0,0 +1,5 @@
Cosa c'è di nuovo:
- Correzione regressione tunnel automatico
- Miglioramenti nella sincronizzazione delle tile.
- Ottimizzazione nelle query dei nomi wifi.
- Miglioramento controllo dei permessi per il monitoraggio della rete
@@ -0,0 +1,8 @@
Cosa c'è di nuovo:
- Aggiornamento UI
- Aggiunta tunnel via URL
- Correzione bug kill switch VPN
- Correzione blocchi improvvisi nel sevizio di tunnel
- Correzioni ping e configurazione modifiche di riavvio
- Miglioramento UI informazioni di rete
- Vari miglioramenti e bug fix
@@ -0,0 +1,6 @@
Cosa c'è di nuovo:
- Nuova integrazione app per gestione remota.
- Copia il nome wifi attivo negli appunti
- Correzione problema interruzione tunnel
- Correzione problema server DNS di deault, il DNS del telefono preferenziale
- Correzione problema configurazione peer host statici IPV6
@@ -0,0 +1,8 @@
Cosa c'è di nuovo:
- Nuova icona app e miglioramenti UI
- Correzione problema attivazione log locale
- Correzione problema AndroidTV riavvio sevizio su riavvio dispositivo
- Correzione problema AndroidTV sulla selezione della lingua
- Correzione funzionalità ping attiva di default
- Correzione problema interruzione modalità kernel
- Correzione problema kill switch per il tunnel automatico
@@ -0,0 +1,6 @@
Cosa c'è di nuovo:
- Correzione problema risoluzione DNS
- Miglioramenti AndroidTV quando si esce dalla modalità sospensione
- Correzione bug ricerca split tunnel
- Aggiornamenti localizzazione
- Aggiornamento link documentazione e nuovo sito web app
@@ -0,0 +1,6 @@
Cosa c'è di nuovo:
- Corretto problema banner AndroidTV
- Aggiunta la selezione multipla per i tunnel
- Aggiunta la ricerca degli aggiornamenti
- Aggiunto la schermata di licenza
- Vari bug fix
@@ -0,0 +1,6 @@
Cosa c'è di nuovo:
- Correzione problema Tunnel di connettività DNS
- Correzione problema Tunnel bloccato in fase di avvio
- Aggiunta la visualizzazione QR code del tunnel
- Aggiunti gli endpoint risolti alle statistiche
- Vari bug fix

Some files were not shown because too many files have changed in this diff Show More