Compare commits
8 Commits
948f15d44a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fe92ccb734 | |||
| e0bc67b146 | |||
| 2f7ef55856 | |||
| 5af5042575 | |||
| 3067b42de4 | |||
| 0dd4869cbb | |||
| b8e1e925b3 | |||
| 24f3e5f454 |
@@ -14,8 +14,10 @@
|
|||||||
# Триггер — тег вида v1.2.3 (совпадает с полем "version" в tauri.conf.json).
|
# Триггер — тег вида v1.2.3 (совпадает с полем "version" в tauri.conf.json).
|
||||||
#
|
#
|
||||||
# Нужные секреты в Gitea: TAURI_SIGNING_PRIVATE_KEY(_PASSWORD) — как и раньше,
|
# Нужные секреты в Gitea: TAURI_SIGNING_PRIVATE_KEY(_PASSWORD) — как и раньше,
|
||||||
# плюс GITEA_SERVER_URL (напр. https://git.example.com, без слэша на конце) и
|
# плюс GITEA_TOKEN (личный токен с правом записи в этот репозиторий).
|
||||||
# GITEA_TOKEN (личный токен с правом записи в этот репозиторий).
|
# GITEA_SERVER_URL (напр. https://git.example.com, без слэша на конце) — это
|
||||||
|
# НЕ секрет (публичный URL сервера), задаётся как repo Variable (vars.*), не
|
||||||
|
# Actions secret.
|
||||||
#
|
#
|
||||||
# shell: bash на windows-latest требует Git for Windows/bash в PATH раннера —
|
# shell: bash на windows-latest требует Git for Windows/bash в PATH раннера —
|
||||||
# на GitHub-хостед раннерах это есть из коробки, на самохостед Gitea-раннере
|
# на GitHub-хостед раннерах это есть из коробки, на самохостед Gitea-раннере
|
||||||
@@ -121,7 +123,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish release to Gitea
|
- name: Publish release to Gitea
|
||||||
env:
|
env:
|
||||||
GITEA_SERVER_URL: ${{ secrets.GITEA_SERVER_URL }}
|
GITEA_SERVER_URL: ${{ vars.GITEA_SERVER_URL }}
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
GITEA_REPO: ${{ github.repository }}
|
GITEA_REPO: ${{ github.repository }}
|
||||||
RELEASE_VERSION: ${{ github.ref_name }}
|
RELEASE_VERSION: ${{ github.ref_name }}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
name: Release Desktop
|
||||||
|
|
||||||
|
# Только десктоп (Windows/macOS/Linux) — Android/iOS обновляются через сторы,
|
||||||
|
# самообновление там запрещено политиками платформ (см. src-tauri/src/lib.rs).
|
||||||
|
# Триггер — тег вида v1.2.3 (совпадает с полем "version" в tauri.conf.json).
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: "macos-latest"
|
||||||
|
args: "--target aarch64-apple-darwin"
|
||||||
|
- platform: "macos-latest"
|
||||||
|
args: "--target x86_64-apple-darwin"
|
||||||
|
- platform: "ubuntu-24.04"
|
||||||
|
args: ""
|
||||||
|
- platform: "windows-latest"
|
||||||
|
args: ""
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
|
- name: Install Linux system deps (webkit2gtk, etc.)
|
||||||
|
if: matrix.platform == 'ubuntu-24.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# Приватный ключ для подписи апдейтов — БЕЗ него downloadAndInstall()
|
||||||
|
# на клиентах будет отклонять пакет (сигнатура не совпадёт).
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
tagName: ${{ github.ref_name }}
|
||||||
|
releaseName: "Netrunner VPN ${{ github.ref_name }}"
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: false
|
||||||
|
includeUpdaterJson: true
|
||||||
|
args: ${{ matrix.args }}
|
||||||
@@ -20,7 +20,9 @@
|
|||||||
"@hugeicons/react": "^1.1.5",
|
"@hugeicons/react": "^1.1.5",
|
||||||
"@tabler/icons-react": "^3.40.0",
|
"@tabler/icons-react": "^3.40.0",
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.35.2",
|
"framer-motion": "^12.35.2",
|
||||||
|
|||||||
Generated
+20
@@ -26,9 +26,15 @@ importers:
|
|||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.10.1
|
version: 2.10.1
|
||||||
|
'@tauri-apps/plugin-process':
|
||||||
|
specifier: ^2.3.1
|
||||||
|
version: 2.3.1
|
||||||
'@tauri-apps/plugin-shell':
|
'@tauri-apps/plugin-shell':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.3.5
|
version: 2.3.5
|
||||||
|
'@tauri-apps/plugin-updater':
|
||||||
|
specifier: ^2.10.1
|
||||||
|
version: 2.10.1
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -1584,9 +1590,15 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-process@2.3.1':
|
||||||
|
resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-shell@2.3.5':
|
'@tauri-apps/plugin-shell@2.3.5':
|
||||||
resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==}
|
resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==}
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-updater@2.10.1':
|
||||||
|
resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==}
|
||||||
|
|
||||||
'@ts-morph/common@0.27.0':
|
'@ts-morph/common@0.27.0':
|
||||||
resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==}
|
resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==}
|
||||||
|
|
||||||
@@ -4518,10 +4530,18 @@ snapshots:
|
|||||||
'@tauri-apps/cli-win32-ia32-msvc': 2.10.1
|
'@tauri-apps/cli-win32-ia32-msvc': 2.10.1
|
||||||
'@tauri-apps/cli-win32-x64-msvc': 2.10.1
|
'@tauri-apps/cli-win32-x64-msvc': 2.10.1
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-process@2.3.1':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
'@tauri-apps/plugin-shell@2.3.5':
|
'@tauri-apps/plugin-shell@2.3.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.10.1
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-updater@2.10.1':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
'@ts-morph/common@0.27.0':
|
'@ts-morph/common@0.27.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
|
|||||||
Generated
+143
@@ -70,6 +70,15 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||||
|
dependencies = [
|
||||||
|
"derive_arbitrary",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
version = "1.9.2"
|
version = "1.9.2"
|
||||||
@@ -900,6 +909,17 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "0.99.20"
|
version = "0.99.20"
|
||||||
@@ -1183,6 +1203,16 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -2567,6 +2597,12 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minisign-verify"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -2648,9 +2684,12 @@ dependencies = [
|
|||||||
"ndk-context",
|
"ndk-context",
|
||||||
"netrunner-logger",
|
"netrunner-logger",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
|
"tauri-plugin-updater",
|
||||||
"tauri-plugin-vpn",
|
"tauri-plugin-vpn",
|
||||||
"winres",
|
"winres",
|
||||||
]
|
]
|
||||||
@@ -2871,6 +2910,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"block2",
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
@@ -2886,6 +2926,18 @@ dependencies = [
|
|||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-osa-kit"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"objc2",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-quartz-core"
|
name = "objc2-quartz-core"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -2973,6 +3025,20 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "osakit"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||||
|
dependencies = [
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
"objc2-osa-kit",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
@@ -3803,6 +3869,7 @@ checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
"subtle",
|
"subtle",
|
||||||
@@ -4620,6 +4687,17 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tar"
|
||||||
|
version = "0.4.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||||
|
dependencies = [
|
||||||
|
"filetime",
|
||||||
|
"libc",
|
||||||
|
"xattr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.12.16"
|
version = "0.12.16"
|
||||||
@@ -4757,6 +4835,16 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-process"
|
||||||
|
version = "2.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
|
||||||
|
dependencies = [
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-shell"
|
name = "tauri-plugin-shell"
|
||||||
version = "2.3.5"
|
version = "2.3.5"
|
||||||
@@ -4778,6 +4866,39 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-updater"
|
||||||
|
version = "2.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"dirs",
|
||||||
|
"flate2",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"infer",
|
||||||
|
"log",
|
||||||
|
"minisign-verify",
|
||||||
|
"osakit",
|
||||||
|
"percent-encoding",
|
||||||
|
"reqwest",
|
||||||
|
"rustls",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tar",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"time",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-vpn"
|
name = "tauri-plugin-vpn"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -6673,6 +6794,16 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xattr"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -6790,6 +6921,18 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
|
"crc32fast",
|
||||||
|
"indexmap 2.13.1",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ winres = "0.1"
|
|||||||
jni = "0.22.3"
|
jni = "0.22.3"
|
||||||
tauri = { version = "2.0.0", features = [] }
|
tauri = { version = "2.0.0", features = [] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
ndk-context = "0.1"
|
ndk-context = "0.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
||||||
@@ -22,6 +23,13 @@ tauri-plugin-vpn = { path = "./src/plugins/vpn-plugin" }
|
|||||||
tauri-plugin-shell = "2.0.0"
|
tauri-plugin-shell = "2.0.0"
|
||||||
netrunner-logger = { git = "ssh://git@github.com/nineAp/netrunner-core.git", branch = "main", features = ["desktop"] }
|
netrunner-logger = { git = "ssh://git@github.com/nineAp/netrunner-core.git", branch = "main", features = ["desktop"] }
|
||||||
|
|
||||||
|
# Автообновление есть только на десктопе — Android/iOS запрещают самообновление
|
||||||
|
# в обход стора, поэтому эти крейты не тянутся и не регистрируются на мобильных
|
||||||
|
# таргетах (см. src/lib.rs, #[cfg(desktop)]).
|
||||||
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
|
tauri-plugin-updater = "2.0.0"
|
||||||
|
tauri-plugin-process = "2.0.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "updater",
|
||||||
|
"description": "Автообновление — только десктоп (Windows/macOS/Linux). Мобильные сборки обновляются через сторы.",
|
||||||
|
"windows": ["main"],
|
||||||
|
"platforms": ["macOS", "windows", "linux"],
|
||||||
|
"permissions": [
|
||||||
|
"updater:default",
|
||||||
|
"process:allow-restart"
|
||||||
|
]
|
||||||
|
}
|
||||||
+11
-2
@@ -1,8 +1,17 @@
|
|||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
let builder = tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_vpn::init())
|
.plugin(tauri_plugin_vpn::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init());
|
||||||
|
|
||||||
|
// Android/iOS не разрешают самообновление в обход стора — плагин
|
||||||
|
// регистрируется только на десктопе (Windows/macOS/Linux).
|
||||||
|
#[cfg(desktop)]
|
||||||
|
let builder = builder
|
||||||
|
.plugin(tauri_plugin_process::init())
|
||||||
|
.plugin(tauri_plugin_updater::Builder::new().build());
|
||||||
|
|
||||||
|
builder
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
"https://github.com/nineAp/netrunner-app/releases/latest/download/latest.json",
|
"https://github.com/nineAp/netrunner-app/releases/latest/download/latest.json",
|
||||||
"https://CHANGE_ME_GITEA_SERVER/nineAp/netrunner-app/releases/download/latest/latest.json"
|
"https://CHANGE_ME_GITEA_SERVER/nineAp/netrunner-app/releases/download/latest/latest.json"
|
||||||
],
|
],
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDczRUU1OTM0MzM1N0M2MzkKUldRNXhsY3pORm51Yzl0YStpa1ZiWkgvWHByMTFBdlpCODJjWmt2UEEyMVI3WWg1MXZFZGt3ZmgK"
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDcxQTFBM0JEMTk5OEEwQTAKUldTZ29KZ1p2YU9oY1JzTlZZeTlzTGFLS0lyVEdsZDRmODAySWZFajNISlpxdHhibTRUV01UZGkK"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { NetrunnerMatrix } from "matrix-engine/NetrunnerMatrix";
|
|||||||
import { useSettingsStore } from "./store/useSettingsStore";
|
import { useSettingsStore } from "./store/useSettingsStore";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { TelegramWeb } from "./pages/TelegramWeb";
|
import { TelegramWeb } from "./pages/TelegramWeb";
|
||||||
|
import { UpdateChecker } from "./features/UpdateChecker";
|
||||||
|
import { ErrorBoundary } from "./features/ErrorBoundary";
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
@@ -102,6 +104,8 @@ function AppContent() {
|
|||||||
onClose={() => setIsMenuOpen(false)}
|
onClose={() => setIsMenuOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UpdateChecker />
|
||||||
|
|
||||||
<main className="flex-1 relative flex flex-col items-center justify-center overflow-hidden">
|
<main className="flex-1 relative flex flex-col items-center justify-center overflow-hidden">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={isLoggedIn ? <Home /> : <Login />} />
|
<Route path="/" element={isLoggedIn ? <Home /> : <Login />} />
|
||||||
@@ -118,8 +122,10 @@ function AppContent() {
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AppContent />
|
<AppContent />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||||
|
import { withTranslation, type WithTranslation } from "react-i18next";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface Props extends WithTranslation {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ловит непойманные ошибки рендера где угодно в дереве ниже — без неё любой
|
||||||
|
* баг в компоненте (например, неожиданный ответ бэкенда, на который никто не
|
||||||
|
* рассчитывал) валит всё приложение в белый экран без единого сообщения.
|
||||||
|
* React error boundary обязан быть классовым компонентом — хуки этого не умеют.
|
||||||
|
*/
|
||||||
|
class ErrorBoundaryImpl extends Component<Props, State> {
|
||||||
|
state: State = { error: null };
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||||
|
console.error("[ErrorBoundary] Непойманная ошибка рендера:", error, info.componentStack);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReload = () => {
|
||||||
|
this.setState({ error: null });
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { error } = this.state;
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
if (!error) return this.props.children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 flex flex-col items-center justify-center gap-4 bg-background p-6 text-center">
|
||||||
|
<h1 className="text-lg font-semibold">{t("error_boundary_title")}</h1>
|
||||||
|
<p className="max-w-sm text-sm text-muted-foreground">
|
||||||
|
{t("error_boundary_desc")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={this.handleReload}>{t("error_boundary_reload")}</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorBoundary = withTranslation()(ErrorBoundaryImpl);
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import type { Update } from "@tauri-apps/plugin-updater";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
type Status = "idle" | "downloading" | "error";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка обновлений — только десктоп (Windows/macOS/Linux). На мобильных
|
||||||
|
* сборках плагин не зарегистрирован (см. src-tauri/src/lib.rs, #[cfg(desktop)]),
|
||||||
|
* поэтому check() там просто недоступен — ошибка тихо проглатывается ниже,
|
||||||
|
* без падения остального приложения.
|
||||||
|
*/
|
||||||
|
export function UpdateChecker() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [update, setUpdate] = useState<Update | null>(null);
|
||||||
|
const [status, setStatus] = useState<Status>("idle");
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const installing = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { check } = await import("@tauri-apps/plugin-updater");
|
||||||
|
const result = await check();
|
||||||
|
if (!cancelled && result?.available) {
|
||||||
|
setUpdate(result);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Мобильная сборка или плагин недоступен в этом окружении — не критично.
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!update) return null;
|
||||||
|
|
||||||
|
const installUpdate = async () => {
|
||||||
|
if (installing.current) return;
|
||||||
|
installing.current = true;
|
||||||
|
setStatus("downloading");
|
||||||
|
try {
|
||||||
|
let downloaded = 0;
|
||||||
|
let total = 0;
|
||||||
|
await update.downloadAndInstall((event) => {
|
||||||
|
if (event.event === "Started") {
|
||||||
|
total = event.data.contentLength ?? 0;
|
||||||
|
} else if (event.event === "Progress") {
|
||||||
|
downloaded += event.data.chunkLength;
|
||||||
|
setProgress(total > 0 ? Math.round((downloaded / total) * 100) : 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { relaunch } = await import("@tauri-apps/plugin-process");
|
||||||
|
await relaunch();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[UpdateChecker] Установка обновления не удалась:", e);
|
||||||
|
setStatus("error");
|
||||||
|
installing.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open onOpenChange={() => {}}>
|
||||||
|
<DialogContent showCloseButton={status !== "downloading"}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("update_available_title")}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t("update_available_desc", { version: update.version })}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={installUpdate} disabled={status === "downloading"}>
|
||||||
|
{status === "downloading"
|
||||||
|
? t("update_downloading", { progress })
|
||||||
|
: status === "error"
|
||||||
|
? t("update_retry")
|
||||||
|
: t("update_install_now")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
+52
-6
@@ -15,6 +15,52 @@ const API_V1 = `${API_BASE}/api/v1`;
|
|||||||
const SEED_KEY = "nrxp_seed";
|
const SEED_KEY = "nrxp_seed";
|
||||||
const TOKEN_KEY = "nrxp_token";
|
const TOKEN_KEY = "nrxp_token";
|
||||||
|
|
||||||
|
// --- Ретраи с backoff для сетевых сбоев ---
|
||||||
|
// Раньше любой fetch() к бэкенду падал с первой же сетевой ошибки/5xx без
|
||||||
|
// единой попытки повторить — временная просадка сети/перезапуск бэкенда сразу
|
||||||
|
// приводили к нечитаемой ошибке в UI ("anon_failed" и т.п.). Ретраим только
|
||||||
|
// сетевые сбои (fetch throws) и 5xx (backend сам сломан) — 4xx-ответы (401 на
|
||||||
|
// просроченный код, 410 и т.д.) НЕ ретраим, это осмысленный ответ сервера,
|
||||||
|
// а не транзиентный сбой, повторять его бессмысленно.
|
||||||
|
const RETRY_ATTEMPTS = 2; // + первая попытка = до 3 всего
|
||||||
|
const RETRY_BASE_DELAY_MS = 400;
|
||||||
|
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NetworkUnavailableError extends Error {
|
||||||
|
constructor(cause?: unknown) {
|
||||||
|
super("NETWORK_UNAVAILABLE");
|
||||||
|
this.name = "NetworkUnavailableError";
|
||||||
|
if (cause) this.cause = cause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithRetry(
|
||||||
|
input: string,
|
||||||
|
init?: RequestInit,
|
||||||
|
): Promise<Response> {
|
||||||
|
let lastError: unknown;
|
||||||
|
for (let attempt = 0; attempt <= RETRY_ATTEMPTS; attempt++) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(input, init);
|
||||||
|
if (res.status >= 500 && attempt < RETRY_ATTEMPTS) {
|
||||||
|
await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
lastError = e;
|
||||||
|
if (attempt < RETRY_ATTEMPTS) {
|
||||||
|
await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NetworkUnavailableError(lastError);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Доменная модель сервера (камелкейс-зеркало VpnNode из бэкенда) ---
|
// --- Доменная модель сервера (камелкейс-зеркало VpnNode из бэкенда) ---
|
||||||
export interface ServerNode {
|
export interface ServerNode {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -89,7 +135,7 @@ function isTokenValid(token: string | null): token is string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function generateSeed(): Promise<string> {
|
async function generateSeed(): Promise<string> {
|
||||||
const res = await fetch(`${API_V1}/auth/seed/generate`, {
|
const res = await fetchWithRetry(`${API_V1}/auth/seed/generate`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
@@ -101,7 +147,7 @@ async function generateSeed(): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loginSeed(seed: string): Promise<string> {
|
async function loginSeed(seed: string): Promise<string> {
|
||||||
const res = await fetch(`${API_V1}/auth/seed/login`, {
|
const res = await fetchWithRetry(`${API_V1}/auth/seed/login`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ seed }),
|
body: JSON.stringify({ seed }),
|
||||||
@@ -133,7 +179,7 @@ async function ensureToken(): Promise<string> {
|
|||||||
|
|
||||||
export async function fetchNodes(): Promise<ServerNode[]> {
|
export async function fetchNodes(): Promise<ServerNode[]> {
|
||||||
const token = await ensureToken();
|
const token = await ensureToken();
|
||||||
const res = await fetch(`${API_V1}/nodes`, {
|
const res = await fetchWithRetry(`${API_V1}/nodes`, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`GET /nodes failed: ${res.status}`);
|
if (!res.ok) throw new Error(`GET /nodes failed: ${res.status}`);
|
||||||
@@ -172,7 +218,7 @@ export async function loginWithSeed(seed: string): Promise<void> {
|
|||||||
* поэтому это одноразовый обмен, а не поллинг: `true` при первом же успешном вызове.
|
* поэтому это одноразовый обмен, а не поллинг: `true` при первом же успешном вызове.
|
||||||
*/
|
*/
|
||||||
export async function pollTelegramLogin(authCode: string): Promise<boolean> {
|
export async function pollTelegramLogin(authCode: string): Promise<boolean> {
|
||||||
const res = await fetch(`${API_V1}/auth/telegram/session/${authCode}`);
|
const res = await fetchWithRetry(`${API_V1}/auth/telegram/session/${authCode}`);
|
||||||
if (res.status === 401 || res.status === 410) {
|
if (res.status === 401 || res.status === 410) {
|
||||||
throw new Error("TELEGRAM_LOGIN_EXPIRED");
|
throw new Error("TELEGRAM_LOGIN_EXPIRED");
|
||||||
}
|
}
|
||||||
@@ -194,7 +240,7 @@ export interface UsageInfo {
|
|||||||
/** Текущий расход/лимит трафика юзера — для индикатора на `VpnStats`. */
|
/** Текущий расход/лимит трафика юзера — для индикатора на `VpnStats`. */
|
||||||
export async function fetchUsage(): Promise<UsageInfo> {
|
export async function fetchUsage(): Promise<UsageInfo> {
|
||||||
const token = await ensureToken();
|
const token = await ensureToken();
|
||||||
const res = await fetch(`${API_V1}/users/me`, {
|
const res = await fetchWithRetry(`${API_V1}/users/me`, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`GET /users/me failed: ${res.status}`);
|
if (!res.ok) throw new Error(`GET /users/me failed: ${res.status}`);
|
||||||
@@ -212,7 +258,7 @@ let cachedBotUsername: string | null = null;
|
|||||||
export async function getBotUsername(): Promise<string | null> {
|
export async function getBotUsername(): Promise<string | null> {
|
||||||
if (cachedBotUsername) return cachedBotUsername;
|
if (cachedBotUsername) return cachedBotUsername;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_V1}/config`);
|
const res = await fetchWithRetry(`${API_V1}/config`);
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
const data = (await res.json()) as { bot_username: string };
|
const data = (await res.json()) as { bot_username: string };
|
||||||
cachedBotUsername = data.bot_username;
|
cachedBotUsername = data.bot_username;
|
||||||
|
|||||||
+9
-1
@@ -79,5 +79,13 @@
|
|||||||
"tg_reset_btn": "إعادة تعيين الاتصال",
|
"tg_reset_btn": "إعادة تعيين الاتصال",
|
||||||
"splash_tagline_1": "النظام يراقب الجميع",
|
"splash_tagline_1": "النظام يراقب الجميع",
|
||||||
"splash_tagline_2": "ما عداك",
|
"splash_tagline_2": "ما عداك",
|
||||||
"dialog_close": "إغلاق"
|
"dialog_close": "إغلاق",
|
||||||
|
"update_available_title": "يتوفر تحديث",
|
||||||
|
"update_available_desc": "الإصدار الجديد ({{version}}) جاهز للتثبيت.",
|
||||||
|
"update_downloading": "جارٍ التنزيل... {{progress}}%",
|
||||||
|
"update_install_now": "تثبيت وإعادة التشغيل",
|
||||||
|
"update_retry": "إعادة المحاولة",
|
||||||
|
"error_boundary_title": "حدث خطأ ما",
|
||||||
|
"error_boundary_desc": "واجه التطبيق خطأ غير متوقع. عادةً ما تحل إعادة التحميل المشكلة.",
|
||||||
|
"error_boundary_reload": "إعادة التحميل"
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -79,5 +79,13 @@
|
|||||||
"tg_reset_btn": "СКІНУЦЬ ЗЛУЧЭННЕ",
|
"tg_reset_btn": "СКІНУЦЬ ЗЛУЧЭННЕ",
|
||||||
"splash_tagline_1": "Сістэма сочыць за ўсімі",
|
"splash_tagline_1": "Сістэма сочыць за ўсімі",
|
||||||
"splash_tagline_2": "Акрамя цябе",
|
"splash_tagline_2": "Акрамя цябе",
|
||||||
"dialog_close": "Закрыць"
|
"dialog_close": "Закрыць",
|
||||||
|
"update_available_title": "Даступна абнаўленне",
|
||||||
|
"update_available_desc": "Новая версія ({{version}}) гатовая да ўсталявання.",
|
||||||
|
"update_downloading": "Загрузка... {{progress}}%",
|
||||||
|
"update_install_now": "Усталяваць і перазапусціць",
|
||||||
|
"update_retry": "Паўтарыць",
|
||||||
|
"error_boundary_title": "Нешта пайшло не так",
|
||||||
|
"error_boundary_desc": "Праграма сутыкнулася з нечаканай памылкай. Звычайна дапамагае перазагрузка.",
|
||||||
|
"error_boundary_reload": "Перазагрузіць"
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -79,5 +79,13 @@
|
|||||||
"tg_reset_btn": "RESET CONNECTION",
|
"tg_reset_btn": "RESET CONNECTION",
|
||||||
"splash_tagline_1": "The System Watches Everyone",
|
"splash_tagline_1": "The System Watches Everyone",
|
||||||
"splash_tagline_2": "Except You",
|
"splash_tagline_2": "Except You",
|
||||||
"dialog_close": "Close"
|
"dialog_close": "Close",
|
||||||
|
"update_available_title": "Update Available",
|
||||||
|
"update_available_desc": "A new version ({{version}}) is ready to install.",
|
||||||
|
"update_downloading": "Downloading... {{progress}}%",
|
||||||
|
"update_install_now": "Install & Restart",
|
||||||
|
"update_retry": "Retry",
|
||||||
|
"error_boundary_title": "Something went wrong",
|
||||||
|
"error_boundary_desc": "The app hit an unexpected error. Reloading usually fixes it.",
|
||||||
|
"error_boundary_reload": "Reload"
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -79,5 +79,13 @@
|
|||||||
"tg_reset_btn": "بازنشانی اتصال",
|
"tg_reset_btn": "بازنشانی اتصال",
|
||||||
"splash_tagline_1": "سیستم همه را زیر نظر دارد",
|
"splash_tagline_1": "سیستم همه را زیر نظر دارد",
|
||||||
"splash_tagline_2": "بهجز تو",
|
"splash_tagline_2": "بهجز تو",
|
||||||
"dialog_close": "بستن"
|
"dialog_close": "بستن",
|
||||||
|
"update_available_title": "بهروزرسانی موجود است",
|
||||||
|
"update_available_desc": "نسخه جدید ({{version}}) آماده نصب است.",
|
||||||
|
"update_downloading": "در حال دانلود... {{progress}}%",
|
||||||
|
"update_install_now": "نصب و راهاندازی مجدد",
|
||||||
|
"update_retry": "تلاش مجدد",
|
||||||
|
"error_boundary_title": "مشکلی پیش آمد",
|
||||||
|
"error_boundary_desc": "برنامه با خطای غیرمنتظرهای مواجه شد. معمولاً بارگذاری مجدد آن را حل میکند.",
|
||||||
|
"error_boundary_reload": "بارگذاری مجدد"
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -79,5 +79,13 @@
|
|||||||
"tg_reset_btn": "СБРОСИТЬ СОЕДИНЕНИЕ",
|
"tg_reset_btn": "СБРОСИТЬ СОЕДИНЕНИЕ",
|
||||||
"splash_tagline_1": "Система следит за всеми",
|
"splash_tagline_1": "Система следит за всеми",
|
||||||
"splash_tagline_2": "Кроме тебя",
|
"splash_tagline_2": "Кроме тебя",
|
||||||
"dialog_close": "Закрыть"
|
"dialog_close": "Закрыть",
|
||||||
|
"update_available_title": "Доступно обновление",
|
||||||
|
"update_available_desc": "Готова к установке новая версия ({{version}}).",
|
||||||
|
"update_downloading": "Загрузка... {{progress}}%",
|
||||||
|
"update_install_now": "Установить и перезапустить",
|
||||||
|
"update_retry": "Повторить",
|
||||||
|
"error_boundary_title": "Что-то пошло не так",
|
||||||
|
"error_boundary_desc": "Приложение столкнулось с неожиданной ошибкой. Обычно помогает перезагрузка.",
|
||||||
|
"error_boundary_reload": "Перезагрузить"
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -79,5 +79,13 @@
|
|||||||
"tg_reset_btn": "BAĞLANTIYI SIFIRLA",
|
"tg_reset_btn": "BAĞLANTIYI SIFIRLA",
|
||||||
"splash_tagline_1": "Sistem Herkesi İzliyor",
|
"splash_tagline_1": "Sistem Herkesi İzliyor",
|
||||||
"splash_tagline_2": "Senin Dışında",
|
"splash_tagline_2": "Senin Dışında",
|
||||||
"dialog_close": "Kapat"
|
"dialog_close": "Kapat",
|
||||||
|
"update_available_title": "Güncelleme mevcut",
|
||||||
|
"update_available_desc": "Yeni sürüm ({{version}}) yüklenmeye hazır.",
|
||||||
|
"update_downloading": "İndiriliyor... {{progress}}%",
|
||||||
|
"update_install_now": "Yükle ve yeniden başlat",
|
||||||
|
"update_retry": "Tekrar dene",
|
||||||
|
"error_boundary_title": "Bir şeyler ters gitti",
|
||||||
|
"error_boundary_desc": "Uygulama beklenmeyen bir hatayla karşılaştı. Genellikle yeniden yükleme sorunu çözer.",
|
||||||
|
"error_boundary_reload": "Yeniden yükle"
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -79,5 +79,13 @@
|
|||||||
"tg_reset_btn": "СКИНУТИ З'ЄДНАННЯ",
|
"tg_reset_btn": "СКИНУТИ З'ЄДНАННЯ",
|
||||||
"splash_tagline_1": "Система стежить за всіма",
|
"splash_tagline_1": "Система стежить за всіма",
|
||||||
"splash_tagline_2": "Крім тебе",
|
"splash_tagline_2": "Крім тебе",
|
||||||
"dialog_close": "Закрити"
|
"dialog_close": "Закрити",
|
||||||
|
"update_available_title": "Доступне оновлення",
|
||||||
|
"update_available_desc": "Нова версія ({{version}}) готова до встановлення.",
|
||||||
|
"update_downloading": "Завантаження... {{progress}}%",
|
||||||
|
"update_install_now": "Встановити й перезапустити",
|
||||||
|
"update_retry": "Повторити",
|
||||||
|
"error_boundary_title": "Щось пішло не так",
|
||||||
|
"error_boundary_desc": "Застосунок зіткнувся з неочікуваною помилкою. Зазвичай допомагає перезавантаження.",
|
||||||
|
"error_boundary_reload": "Перезавантажити"
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -79,5 +79,13 @@
|
|||||||
"tg_reset_btn": "ULANISHNI QAYTA O'RNATISH",
|
"tg_reset_btn": "ULANISHNI QAYTA O'RNATISH",
|
||||||
"splash_tagline_1": "Tizim Hammani Kuzatmoqda",
|
"splash_tagline_1": "Tizim Hammani Kuzatmoqda",
|
||||||
"splash_tagline_2": "Sizdan Tashqari",
|
"splash_tagline_2": "Sizdan Tashqari",
|
||||||
"dialog_close": "Yopish"
|
"dialog_close": "Yopish",
|
||||||
|
"update_available_title": "Yangilanish mavjud",
|
||||||
|
"update_available_desc": "Yangi versiya ({{version}}) o'rnatishga tayyor.",
|
||||||
|
"update_downloading": "Yuklab olinmoqda... {{progress}}%",
|
||||||
|
"update_install_now": "O'rnatish va qayta ishga tushirish",
|
||||||
|
"update_retry": "Qayta urinish",
|
||||||
|
"error_boundary_title": "Nimadir xato ketdi",
|
||||||
|
"error_boundary_desc": "Ilovada kutilmagan xatolik yuz berdi. Odatda qayta yuklash yordam beradi.",
|
||||||
|
"error_boundary_reload": "Qayta yuklash"
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -79,5 +79,13 @@
|
|||||||
"tg_reset_btn": "ĐẶT LẠI KẾT NỐI",
|
"tg_reset_btn": "ĐẶT LẠI KẾT NỐI",
|
||||||
"splash_tagline_1": "Hệ Thống Theo Dõi Mọi Người",
|
"splash_tagline_1": "Hệ Thống Theo Dõi Mọi Người",
|
||||||
"splash_tagline_2": "Trừ Bạn",
|
"splash_tagline_2": "Trừ Bạn",
|
||||||
"dialog_close": "Đóng"
|
"dialog_close": "Đóng",
|
||||||
|
"update_available_title": "Có bản cập nhật",
|
||||||
|
"update_available_desc": "Phiên bản mới ({{version}}) đã sẵn sàng để cài đặt.",
|
||||||
|
"update_downloading": "Đang tải xuống... {{progress}}%",
|
||||||
|
"update_install_now": "Cài đặt và khởi động lại",
|
||||||
|
"update_retry": "Thử lại",
|
||||||
|
"error_boundary_title": "Đã xảy ra sự cố",
|
||||||
|
"error_boundary_desc": "Ứng dụng gặp lỗi ngoài dự kiến. Tải lại thường sẽ khắc phục được.",
|
||||||
|
"error_boundary_reload": "Tải lại"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,5 +79,13 @@
|
|||||||
"tg_reset_btn": "重置连接",
|
"tg_reset_btn": "重置连接",
|
||||||
"splash_tagline_1": "系统监视着所有人",
|
"splash_tagline_1": "系统监视着所有人",
|
||||||
"splash_tagline_2": "除了你",
|
"splash_tagline_2": "除了你",
|
||||||
"dialog_close": "关闭"
|
"dialog_close": "关闭",
|
||||||
|
"update_available_title": "有可用更新",
|
||||||
|
"update_available_desc": "新版本({{version}})已准备好安装。",
|
||||||
|
"update_downloading": "正在下载... {{progress}}%",
|
||||||
|
"update_install_now": "安装并重启",
|
||||||
|
"update_retry": "重试",
|
||||||
|
"error_boundary_title": "出了点问题",
|
||||||
|
"error_boundary_desc": "应用遇到了意外错误。通常重新加载即可解决。",
|
||||||
|
"error_boundary_reload": "重新加载"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user