mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-11 21:59:06 +00:00
feat(rdclient): enhance desktop client with server validation, LAN discovery, and settings management
- Introduced server URL validation via `GET /api/bd/server-info` and `probe_server_url`. - Added LAN discovery capabilities using UDP and optional mDNS for local network panel detection. - Implemented a local settings window for managing URL, TLS settings, and user preferences. - Enhanced dashboard with a unified sidebar and improved scrolling behavior. - Updated documentation and pre-release checklist to reflect new features and requirements.
This commit is contained in:
+13
-2
@@ -1,7 +1,17 @@
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **RdClient desktop (production hardening):** server URL validation via `GET /api/bd/server-info` and `probe_server_url`; extended `config.json` (`tls_strict`, `ui_lang`, `discovered_via`); `BETTERDESK_SERVER_URL` env and embedded `betterdesk-rdclient.json` on first launch; LAN discovery (UDP port 21119 + optional mDNS `_betterdesk._tcp`); local **Settings** window (change URL, TLS, sign out, factory reset); peer **Remember device password** vault (IndexedDB AES-GCM per device); language switcher on `/remote/login`, dashboard, and viewer (26 locales); **Generator → RdClient desktop** bundles with `rdclientBuildWorker` (6 platform variants, embedded panel URL).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- _(none yet)_
|
|
||||||
|
- **RdClient codecs (Linux):** WebKitGTK skips unreliable AV1 negotiation; runtime fallback to VP9/H.264 on decode failure.
|
||||||
|
- **RdClient dashboard:** unified sidebar + device panel scrolling; desktop settings button in header.
|
||||||
|
|
||||||
|
### Manual steps (build host)
|
||||||
|
|
||||||
|
- RdClient installer builds require **Rust stable**, **npm**, **@tauri-apps/cli**, and Linux deps (`webkit2gtk4.1-devel`, `openssl-devel`, …) on the panel build host. Optional: `npm install bonjour-service` in `web-nodejs` for mDNS panel publish (`PANEL_MDNS=off` to disable).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -16,7 +26,8 @@
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- **RdClient dashboard (web):** address book list/grid scrolls inside the workspace instead of stretching the whole page (fixed-height flex layout); collapsible device list panel and sidebar sections (Folders/Groups/Tags) with internal scroll; desktop shell applies stricter viewport height.
|
- **RdClient dashboard (web):** address book and sidebar scroll inside fixed columns with hidden scrollbars; `.rd-desk-content` flex chain fixed so the device grid no longer stretches the window.
|
||||||
|
- **RdClient video (AV1):** WebKit uses software AV1 decode; agent `codec_string` is passed to `VideoDecoder`; AV1 keyframes build `description` (av1C); failed codecs auto-reconnect with VP9/H.264.
|
||||||
- **RdClient desktop:** **Connect** from the loaded `/remote` dashboard — runtime ACL for the configured panel origin (fixes LAN IP:port invoke), injected Connect bridge in the desktop shell (works before panel JS update), correct Tauri invoke args (`deviceId` / `deviceName`), and a new app window loading the same `/remote/:id` viewer as the web panel; session window **Back** closes the desktop window (injected handler + no `/devices` fallback).
|
- **RdClient desktop:** **Connect** from the loaded `/remote` dashboard — runtime ACL for the configured panel origin (fixes LAN IP:port invoke), injected Connect bridge in the desktop shell (works before panel JS update), correct Tauri invoke args (`deviceId` / `deviceName`), and a new app window loading the same `/remote/:id` viewer as the web panel; session window **Back** closes the desktop window (injected handler + no `/devices` fallback).
|
||||||
- **RdClient desktop (performance):** Linux Wayland keeps WebKit DMA-BUF/GPU compositing enabled by default; vendor-specific VA-API (Intel `iHD`, AMD `radeonsi`, NVIDIA when `nvidia-vaapi-driver` is present); GStreamer hardware decoder rank for AV1/H264/VP9/H265; session windows disable background throttling; Windows WebView2 AV1/HEVC GPU decode flags; WebCodecs probes multiple AV1/H264 profiles for WebKitGTK compatibility.
|
- **RdClient desktop (performance):** Linux Wayland keeps WebKit DMA-BUF/GPU compositing enabled by default; vendor-specific VA-API (Intel `iHD`, AMD `radeonsi`, NVIDIA when `nvidia-vaapi-driver` is present); GStreamer hardware decoder rank for AV1/H264/VP9/H265; session windows disable background throttling; Windows WebView2 AV1/HEVC GPU decode flags; WebCodecs probes multiple AV1/H264 profiles for WebKitGTK compatibility.
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,18 @@ Use this checklist before every tagged release to ensure quality and stability.
|
|||||||
- [ ] **Login**: Admin login works, session created
|
- [ ] **Login**: Admin login works, session created
|
||||||
- [ ] **Critical pages**: Dashboard, Devices, Users, Settings render correctly
|
- [ ] **Critical pages**: Dashboard, Devices, Users, Settings render correctly
|
||||||
|
|
||||||
## 3. Desktop Client (Tauri)
|
## 3. RdClient Desktop (Tauri)
|
||||||
|
|
||||||
|
- [ ] **Install deps**: `cd rdclient-desktop && npm ci`
|
||||||
|
- [ ] **Rust check**: `cd rdclient-desktop/src-tauri && cargo check && cargo test discovery`
|
||||||
|
- [ ] **Tauri build**: `npm run build` — deb/AppImage/MSI as configured
|
||||||
|
- [ ] **Setup**: LAN discovery or manual URL; `probe_server_url` rejects non-panel hosts
|
||||||
|
- [ ] **Settings**: sign out, reset client clears cookies + vault
|
||||||
|
- [ ] **Linux session**: VP9/H.264 stable on WebKitGTK (no AV1 decode loop)
|
||||||
|
- [ ] **Windows**: WebView2 runtime present; session connects
|
||||||
|
- [ ] **Generator RdClient**: new bundle → 6 platform builds queue on build host with Rust/Tauri toolchain
|
||||||
|
|
||||||
|
## 4. Desktop Client (Tauri — legacy betterdesk-mgmt)
|
||||||
|
|
||||||
- [ ] **Install deps**: `cd betterdesk-mgmt && pnpm install`
|
- [ ] **Install deps**: `cd betterdesk-mgmt && pnpm install`
|
||||||
- [ ] **Frontend build**: `pnpm build` — no errors
|
- [ ] **Frontend build**: `pnpm build` — no errors
|
||||||
@@ -32,20 +43,20 @@ Use this checklist before every tagged release to ensure quality and stability.
|
|||||||
- [ ] **Installer runs**: installs and launches without crash
|
- [ ] **Installer runs**: installs and launches without crash
|
||||||
- [ ] **Single-instance**: second launch brings first to foreground
|
- [ ] **Single-instance**: second launch brings first to foreground
|
||||||
|
|
||||||
## 4. Agent Client (Tauri)
|
## 5. Agent Client (Tauri)
|
||||||
|
|
||||||
- [ ] **Install deps**: `cd betterdesk-agent-client && pnpm install`
|
- [ ] **Install deps**: `cd betterdesk-agent-client && pnpm install`
|
||||||
- [ ] **Build**: `cargo tauri build` — NSIS produced
|
- [ ] **Build**: `cargo tauri build` — NSIS produced
|
||||||
- [ ] **Setup wizard**: 5-step onboarding completes successfully
|
- [ ] **Setup wizard**: 5-step onboarding completes successfully
|
||||||
- [ ] **Registration**: device appears in server peer list
|
- [ ] **Registration**: device appears in server peer list
|
||||||
|
|
||||||
## 5. Native Agent (Go)
|
## 6. Native Agent (Go)
|
||||||
|
|
||||||
- [ ] **Build**: `cd betterdesk-agent && go build -o betterdesk-agent .`
|
- [ ] **Build**: `cd betterdesk-agent && go build -o betterdesk-agent .`
|
||||||
- [ ] **Connection**: connects to CDAP gateway, manifests registers
|
- [ ] **Connection**: connects to CDAP gateway, manifests registers
|
||||||
- [ ] **Heartbeat**: metrics flow (CPU / Memory / Disk)
|
- [ ] **Heartbeat**: metrics flow (CPU / Memory / Disk)
|
||||||
|
|
||||||
## 6. Docker
|
## 7. Docker
|
||||||
|
|
||||||
- [ ] **Build all images**: `docker compose build` — no errors
|
- [ ] **Build all images**: `docker compose build` — no errors
|
||||||
- [ ] **Start stack**: `docker compose up -d` — all containers healthy
|
- [ ] **Start stack**: `docker compose up -d` — all containers healthy
|
||||||
@@ -53,7 +64,7 @@ Use this checklist before every tagged release to ensure quality and stability.
|
|||||||
- [ ] **Console reachable**: `curl http://localhost:5000` returns HTML
|
- [ ] **Console reachable**: `curl http://localhost:5000` returns HTML
|
||||||
- [ ] **Single-container**: `docker compose -f docker-compose.single.yml up -d` works
|
- [ ] **Single-container**: `docker compose -f docker-compose.single.yml up -d` works
|
||||||
|
|
||||||
## 7. Installer Scripts
|
## 8. Installer Scripts
|
||||||
|
|
||||||
- [ ] **Linux fresh**: `sudo ./betterdesk.sh --auto` on clean Ubuntu/Debian
|
- [ ] **Linux fresh**: `sudo ./betterdesk.sh --auto` on clean Ubuntu/Debian
|
||||||
- [ ] **Linux update**: `sudo ./betterdesk.sh` option 2 preserves DB + config
|
- [ ] **Linux update**: `sudo ./betterdesk.sh` option 2 preserves DB + config
|
||||||
@@ -61,7 +72,7 @@ Use this checklist before every tagged release to ensure quality and stability.
|
|||||||
- [ ] **Windows update**: `.\betterdesk.ps1` option 2 preserves DB + config
|
- [ ] **Windows update**: `.\betterdesk.ps1` option 2 preserves DB + config
|
||||||
- [ ] **Docker script**: `./betterdesk-docker.sh` option 1 installs successfully
|
- [ ] **Docker script**: `./betterdesk-docker.sh` option 1 installs successfully
|
||||||
|
|
||||||
## 8. Documentation & Release
|
## 9. Documentation & Release
|
||||||
|
|
||||||
Applies only to merges into **`main`** (stable). Version bump, tag, and GitHub Release are automated by [`.github/workflows/version-bump-main.yml`](../../.github/workflows/version-bump-main.yml) — see [branching-and-versioning.md](important/branching-and-versioning.md).
|
Applies only to merges into **`main`** (stable). Version bump, tag, and GitHub Release are automated by [`.github/workflows/version-bump-main.yml`](../../.github/workflows/version-bump-main.yml) — see [branching-and-versioning.md](important/branching-and-versioning.md).
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Tauri v2 desktop shell for the RdClient operator UI. The app loads your panel’s **`/remote`** dashboard in the main window and opens each remote session in a **separate window** (`/remote/:deviceId`), similar to RustDesk.
|
Tauri v2 desktop shell for the RdClient operator UI. The app loads your panel’s **`/remote`** dashboard in the main window and opens each remote session in a **separate window** (`/remote/:deviceId`), similar to RustDesk.
|
||||||
|
|
||||||
This is **Phase C (MVP)** of the RdClient roadmap. It reuses the web dashboard and login flow; JWT/keychain auth is planned for a later phase.
|
This is **production-ready Phase C+** of the RdClient roadmap: server validation, LAN discovery, settings/reset, encrypted peer passwords, full panel i18n, codec fallbacks on Linux, and generator-built installers with embedded panel URL.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -111,9 +111,28 @@ Production bundle instead of dev:
|
|||||||
|
|
||||||
**Important:** the dashboard HTML/JS is loaded from your **panel URL** (`/remote`). Updating only the desktop binary is not enough for UI tweaks — run **Settings → Updates** on the panel (or deploy `web-nodejs`) so `/js/remote-dashboard.js` is current. The desktop shell also injects a Connect bridge, so **Connect works even before the panel JS update** once you run a freshly built binary.
|
**Important:** the dashboard HTML/JS is loaded from your **panel URL** (`/remote`). Updating only the desktop binary is not enough for UI tweaks — run **Settings → Updates** on the panel (or deploy `web-nodejs`) so `/js/remote-dashboard.js` is current. The desktop shell also injects a Connect bridge, so **Connect works even before the panel JS update** once you run a freshly built binary.
|
||||||
|
|
||||||
1. On first launch, enter your panel base URL (e.g. `https://desk.example.com`).
|
1. On first launch, enter your panel base URL (e.g. `https://desk.example.com`) or pick a server from **LAN discovery**.
|
||||||
2. Sign in at **`/remote/login`** when prompted (same as the web RdClient).
|
2. Sign in at **`/remote/login`** when prompted (same as the web RdClient).
|
||||||
3. Use **Connect** on a device — the desktop opens a new window instead of a browser tab.
|
3. Use **Connect** on a device — the desktop opens a new window instead of a browser tab.
|
||||||
|
4. Open **Settings** (gear icon in the dashboard header) to change URL, TLS mode, language, sign out, or **Reset client** (clears config, cookies, and saved passwords).
|
||||||
|
|
||||||
|
### Environment & embedded URL
|
||||||
|
|
||||||
|
| Source | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `BETTERDESK_SERVER_URL` | Auto-configure panel URL before setup UI |
|
||||||
|
| `betterdesk-rdclient.json` next to the binary | Installer-embedded `{ "server_url": "https://…" }` from Generator |
|
||||||
|
| UDP / mDNS LAN discovery | Setup UI lists panels on the local network |
|
||||||
|
|
||||||
|
### Password storage
|
||||||
|
|
||||||
|
- **Operator login (“Remember me”):** `RdClientSecureStore` in IndexedDB (AES-GCM).
|
||||||
|
- **Device password (“Remember device password”):** same vault, per `deviceId` (`peer:{id}` keys). Passwords never leave the device.
|
||||||
|
- **Reset client:** clears `config.json`, WebView cookies/storage, and the vault.
|
||||||
|
|
||||||
|
### LAN discovery
|
||||||
|
|
||||||
|
The panel publishes itself via UDP (port **21119**, always on) and optionally mDNS `_betterdesk._tcp` when `bonjour-service` is installed and `PANEL_MDNS` is not `off`.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -127,9 +146,12 @@ Installers/binaries are under `src-tauri/target/release/bundle/`.
|
|||||||
|
|
||||||
| Piece | Role |
|
| Piece | Role |
|
||||||
|-------|------|
|
|-------|------|
|
||||||
| `src/setup.html` | First-run local page to save the panel URL |
|
| `src/setup.html` | First-run: LAN discovery list + manual panel URL |
|
||||||
| `src-tauri/src/lib.rs` | Tauri commands: `get_server_url`, `set_server_url`, `open_session` |
|
| `src/settings.html` | Local settings: URL, TLS, language, sign out, reset |
|
||||||
| `src-tauri/src/config.rs` | Persists `server_url` in the app config dir |
|
| `src-tauri/src/lib.rs` | Tauri commands: probe, discover, settings, sign out, reset, sessions |
|
||||||
|
| `src-tauri/src/config.rs` | Persists extended config + embedded/env URL helpers |
|
||||||
|
| `src-tauri/src/server_probe.rs` | Validates panel via `/api/bd/server-info` |
|
||||||
|
| `src-tauri/src/discovery.rs` | UDP LAN browse (BetterDesk announce protocol) |
|
||||||
| `src-tauri/src/linux_display.rs` | Linux X11/Wayland session + WebKitGTK workarounds |
|
| `src-tauri/src/linux_display.rs` | Linux X11/Wayland session + WebKitGTK workarounds |
|
||||||
| `src-tauri/src/tls_policy.rs` | Windows WebView2 + strict-mode env |
|
| `src-tauri/src/tls_policy.rs` | Windows WebView2 + strict-mode env |
|
||||||
| `scripts/rdclient-launcher.sh` | Optional wrapper for release binaries |
|
| `scripts/rdclient-launcher.sh` | Optional wrapper for release binaries |
|
||||||
@@ -146,12 +168,20 @@ Config file: **`config.json`** in the OS app config directory (`com.betterdesk.r
|
|||||||
cargo update -p alloc-no-stdlib@2.0.4 --precise 3.0.0
|
cargo update -p alloc-no-stdlib@2.0.4 --precise 3.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
## Roadmap (not in this MVP)
|
## Verify matrix (manual)
|
||||||
|
|
||||||
|
| Platform | Check |
|
||||||
|
|----------|--------|
|
||||||
|
| **Linux Wayland** | Setup discovery, login, VP9/H.264 session (no AV1 loop), settings reset |
|
||||||
|
| **Linux X11** | Same as Wayland; test `BETTERDESK_UI_BACKEND=x11` if needed |
|
||||||
|
| **Windows x64** | WebView2 present, Connect, remember passwords, MSI/portable from Generator |
|
||||||
|
| **Fedora deb/rpm** | Installed bundle from Generator when build host has toolchain |
|
||||||
|
|
||||||
|
## Roadmap (not in this release)
|
||||||
|
|
||||||
- Operator JWT + OS keychain login (`POST /api/bd/operator/login`)
|
- Operator JWT + OS keychain login (`POST /api/bd/operator/login`)
|
||||||
- WebSocket relay Bearer auth for long-lived desktop sessions
|
- WebSocket relay Bearer auth for long-lived desktop sessions
|
||||||
- Settings → change server URL / sign out
|
- Native Rust video decoder (if WebCodecs insufficient on Linux)
|
||||||
- CI release artifacts (replacing legacy `betterdesk-mgmt`)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
Generated
+331
-1
@@ -307,6 +307,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.45"
|
version = "0.4.45"
|
||||||
@@ -1017,8 +1023,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1028,9 +1036,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 5.3.0",
|
"r-efi 5.3.0",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1302,6 +1312,22 @@ dependencies = [
|
|||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-rustls"
|
||||||
|
version = "0.27.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower-service",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@@ -1733,6 +1759,12 @@ version = "0.4.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru-slab"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markup5ever"
|
name = "markup5ever"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
@@ -2282,6 +2314,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "precomputed-hash"
|
name = "precomputed-hash"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2369,6 +2410,61 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn"
|
||||||
|
version = "0.11.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"cfg_aliases",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn-proto",
|
||||||
|
"quinn-udp",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"socket2",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-proto"
|
||||||
|
version = "0.11.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"lru-slab",
|
||||||
|
"rand",
|
||||||
|
"ring",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"slab",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tinyvec",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-udp"
|
||||||
|
version = "0.5.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"socket2",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.45"
|
||||||
@@ -2390,6 +2486,35 @@ version = "6.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -2400,12 +2525,15 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
|||||||
name = "rdclient-desktop"
|
name = "rdclient-desktop"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sys-locale",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2477,6 +2605,44 @@ version = "0.8.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.12.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
|
"hyper-util",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.4"
|
version = "0.13.4"
|
||||||
@@ -2511,6 +2677,20 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.2"
|
version = "2.1.2"
|
||||||
@@ -2526,12 +2706,53 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||||
|
dependencies = [
|
||||||
|
"web-time",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -2722,6 +2943,18 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.21.0"
|
version = "3.21.0"
|
||||||
@@ -2962,6 +3195,12 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "swift-rs"
|
name = "swift-rs"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -3014,6 +3253,15 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sys-locale"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-deps"
|
name = "system-deps"
|
||||||
version = "6.2.2"
|
version = "6.2.2"
|
||||||
@@ -3114,7 +3362,7 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"plist",
|
"plist",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"reqwest",
|
"reqwest 0.13.4",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@@ -3450,9 +3698,31 @@ dependencies = [
|
|||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||||
|
dependencies = [
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -3749,6 +4019,12 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -3985,6 +4261,16 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web_atoms"
|
name = "web_atoms"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -4041,6 +4327,15 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.38.2"
|
version = "0.38.2"
|
||||||
@@ -4271,6 +4566,15 @@ dependencies = [
|
|||||||
"windows-targets 0.42.2",
|
"windows-targets 0.42.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
@@ -4722,6 +5026,26 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.52"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.52"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
@@ -4743,6 +5067,12 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ tauri-plugin-shell = "2"
|
|||||||
tauri-utils = "2"
|
tauri-utils = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||||
|
sys-locale = "0.3"
|
||||||
|
|
||||||
# brotli 8.0.3 on crates.io pulls conflicting alloc-no-stdlib 2.x/3.x; git brotli + unified allocator crate.
|
# brotli 8.0.3 on crates.io pulls conflicting alloc-no-stdlib 2.x/3.x; git brotli + unified allocator crate.
|
||||||
# wry: accept operator-panel TLS (self-signed / incomplete chain) on Linux WebKitGTK.
|
# wry: accept operator-panel TLS (self-signed / incomplete chain) on Linux WebKitGTK.
|
||||||
|
|||||||
@@ -4,7 +4,16 @@ fn main() {
|
|||||||
tauri_build::AppManifest::new().commands(&[
|
tauri_build::AppManifest::new().commands(&[
|
||||||
"get_client_info",
|
"get_client_info",
|
||||||
"get_server_url",
|
"get_server_url",
|
||||||
|
"get_config",
|
||||||
|
"probe_server_url",
|
||||||
|
"discover_servers",
|
||||||
"set_server_url",
|
"set_server_url",
|
||||||
|
"set_tls_strict",
|
||||||
|
"set_ui_lang",
|
||||||
|
"get_system_locale",
|
||||||
|
"open_settings",
|
||||||
|
"sign_out",
|
||||||
|
"reset_client",
|
||||||
"open_session",
|
"open_session",
|
||||||
"close_current_window",
|
"close_current_window",
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://schema.tauri.app/config/2/capability",
|
"$schema": "https://schema.tauri.app/config/2/capability",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Default RdClient desktop capabilities",
|
"description": "Default RdClient desktop capabilities",
|
||||||
"windows": ["main", "session-*"],
|
"windows": ["main", "settings", "session-*"],
|
||||||
"remote": {
|
"remote": {
|
||||||
"urls": [
|
"urls": [
|
||||||
"http://*",
|
"http://*",
|
||||||
@@ -33,9 +33,19 @@
|
|||||||
"core:window:allow-set-focus",
|
"core:window:allow-set-focus",
|
||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
"core:webview:allow-create-webview-window",
|
"core:webview:allow-create-webview-window",
|
||||||
|
"core:webview:allow-clear-all-browsing-data",
|
||||||
"shell:allow-open",
|
"shell:allow-open",
|
||||||
"allow-get-server-url",
|
"allow-get-server-url",
|
||||||
"allow-set-server-url",
|
"allow-set-server-url",
|
||||||
|
"allow-probe-server-url",
|
||||||
|
"allow-discover-servers",
|
||||||
|
"allow-get-config",
|
||||||
|
"allow-set-tls-strict",
|
||||||
|
"allow-set-ui-lang",
|
||||||
|
"allow-get-system-locale",
|
||||||
|
"allow-open-settings",
|
||||||
|
"allow-sign-out",
|
||||||
|
"allow-reset-client",
|
||||||
"allow-open-session",
|
"allow-open-session",
|
||||||
"allow-close-current-window",
|
"allow-close-current-window",
|
||||||
"allow-get-client-info"
|
"allow-get-client-info"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-discover-servers"
|
||||||
|
description = "Enables the discover_servers command without any pre-configured scope."
|
||||||
|
commands.allow = ["discover_servers"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-discover-servers"
|
||||||
|
description = "Denies the discover_servers command without any pre-configured scope."
|
||||||
|
commands.deny = ["discover_servers"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-get-config"
|
||||||
|
description = "Enables the get_config command without any pre-configured scope."
|
||||||
|
commands.allow = ["get_config"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-get-config"
|
||||||
|
description = "Denies the get_config command without any pre-configured scope."
|
||||||
|
commands.deny = ["get_config"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-get-system-locale"
|
||||||
|
description = "Enables the get_system_locale command without any pre-configured scope."
|
||||||
|
commands.allow = ["get_system_locale"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-get-system-locale"
|
||||||
|
description = "Denies the get_system_locale command without any pre-configured scope."
|
||||||
|
commands.deny = ["get_system_locale"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-open-settings"
|
||||||
|
description = "Enables the open_settings command without any pre-configured scope."
|
||||||
|
commands.allow = ["open_settings"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-open-settings"
|
||||||
|
description = "Denies the open_settings command without any pre-configured scope."
|
||||||
|
commands.deny = ["open_settings"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-probe-server-url"
|
||||||
|
description = "Enables the probe_server_url command without any pre-configured scope."
|
||||||
|
commands.allow = ["probe_server_url"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-probe-server-url"
|
||||||
|
description = "Denies the probe_server_url command without any pre-configured scope."
|
||||||
|
commands.deny = ["probe_server_url"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-reset-client"
|
||||||
|
description = "Enables the reset_client command without any pre-configured scope."
|
||||||
|
commands.allow = ["reset_client"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-reset-client"
|
||||||
|
description = "Denies the reset_client command without any pre-configured scope."
|
||||||
|
commands.deny = ["reset_client"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-set-tls-strict"
|
||||||
|
description = "Enables the set_tls_strict command without any pre-configured scope."
|
||||||
|
commands.allow = ["set_tls_strict"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-set-tls-strict"
|
||||||
|
description = "Denies the set_tls_strict command without any pre-configured scope."
|
||||||
|
commands.deny = ["set_tls_strict"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-set-ui-lang"
|
||||||
|
description = "Enables the set_ui_lang command without any pre-configured scope."
|
||||||
|
commands.allow = ["set_ui_lang"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-set-ui-lang"
|
||||||
|
description = "Denies the set_ui_lang command without any pre-configured scope."
|
||||||
|
commands.deny = ["set_ui_lang"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-sign-out"
|
||||||
|
description = "Enables the sign_out command without any pre-configured scope."
|
||||||
|
commands.allow = ["sign_out"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-sign-out"
|
||||||
|
description = "Denies the sign_out command without any pre-configured scope."
|
||||||
|
commands.deny = ["sign_out"]
|
||||||
@@ -4,11 +4,37 @@ use std::path::PathBuf;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
pub const CONFIG_VERSION: u32 = 1;
|
||||||
const CONFIG_FILE: &str = "config.json";
|
const CONFIG_FILE: &str = "config.json";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
|
#[serde(default = "default_config_version")]
|
||||||
|
pub config_version: u32,
|
||||||
|
#[serde(default)]
|
||||||
pub server_url: Option<String>,
|
pub server_url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub tls_strict: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub discovered_via: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ui_lang: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_config_version() -> u32 {
|
||||||
|
CONFIG_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
config_version: CONFIG_VERSION,
|
||||||
|
server_url: None,
|
||||||
|
tls_strict: false,
|
||||||
|
discovered_via: None,
|
||||||
|
ui_lang: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn config_path(app: &AppHandle) -> Result<PathBuf, String> {
|
pub fn config_path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||||
@@ -26,11 +52,56 @@ pub fn load_config(app: &AppHandle) -> Result<AppConfig, String> {
|
|||||||
return Ok(AppConfig::default());
|
return Ok(AppConfig::default());
|
||||||
}
|
}
|
||||||
let raw = fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
let raw = fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||||
serde_json::from_str(&raw).map_err(|e| e.to_string())
|
let mut cfg: AppConfig = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
|
||||||
|
if cfg.config_version == 0 {
|
||||||
|
cfg.config_version = CONFIG_VERSION;
|
||||||
|
}
|
||||||
|
Ok(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_config(app: &AppHandle, cfg: &AppConfig) -> Result<(), String> {
|
pub fn save_config(app: &AppHandle, cfg: &AppConfig) -> Result<(), String> {
|
||||||
let path = config_path(app)?;
|
let path = config_path(app)?;
|
||||||
let raw = serde_json::to_string_pretty(cfg).map_err(|e| e.to_string())?;
|
let mut to_save = cfg.clone();
|
||||||
|
to_save.config_version = CONFIG_VERSION;
|
||||||
|
let raw = serde_json::to_string_pretty(&to_save).map_err(|e| e.to_string())?;
|
||||||
fs::write(path, raw).map_err(|e| e.to_string())
|
fs::write(path, raw).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear_config(app: &AppHandle) -> Result<(), String> {
|
||||||
|
let path = config_path(app)?;
|
||||||
|
if path.exists() {
|
||||||
|
fs::remove_file(path).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read optional embedded installer config `{exe_dir}/betterdesk-rdclient.json`.
|
||||||
|
pub fn load_embedded_server_url() -> Option<String> {
|
||||||
|
let exe = std::env::current_exe().ok()?;
|
||||||
|
let path = exe.parent()?.join("betterdesk-rdclient.json");
|
||||||
|
if !path.exists() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let raw = fs::read_to_string(path).ok()?;
|
||||||
|
let v: serde_json::Value = serde_json::from_str(&raw).ok()?;
|
||||||
|
v.get("server_url")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn env_server_url() -> Option<String> {
|
||||||
|
std::env::var("BETTERDESK_SERVER_URL")
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_tls_from_config(cfg: &AppConfig) {
|
||||||
|
if cfg.tls_strict {
|
||||||
|
// SAFETY: before any WebView/GTK init in setup().
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("BETTERDESK_TLS_STRICT", "1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::net::{Ipv4Addr, SocketAddr, UdpSocket};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const DISCOVERY_PORT: u16 = 21119;
|
||||||
|
const PROBE: &str = r#"{"type":"betterdesk-discover","version":1}"#;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DiscoveredServer {
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
pub source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Announcement {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: String,
|
||||||
|
server: Option<AnnounceServer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AnnounceServer {
|
||||||
|
name: Option<String>,
|
||||||
|
version: Option<String>,
|
||||||
|
port: Option<u16>,
|
||||||
|
#[serde(rename = "apiPort")]
|
||||||
|
api_port: Option<u16>,
|
||||||
|
protocol: Option<String>,
|
||||||
|
addresses: Option<Vec<String>>,
|
||||||
|
#[serde(rename = "panelUrl")]
|
||||||
|
panel_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UDP LAN discovery using the existing BetterDesk console protocol.
|
||||||
|
pub fn discover_udp(timeout_ms: u64) -> Vec<DiscoveredServer> {
|
||||||
|
let socket = match UdpSocket::bind("0.0.0.0:0") {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return Vec::new(),
|
||||||
|
};
|
||||||
|
let _ = socket.set_broadcast(true);
|
||||||
|
let _ = socket.set_read_timeout(Some(Duration::from_millis(timeout_ms.max(500))));
|
||||||
|
|
||||||
|
let probe = PROBE.as_bytes();
|
||||||
|
let broadcast = SocketAddr::new(Ipv4Addr::BROADCAST.into(), DISCOVERY_PORT);
|
||||||
|
let _ = socket.send_to(probe, broadcast);
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut buf = [0u8; 4096];
|
||||||
|
let deadline = std::time::Instant::now() + Duration::from_millis(timeout_ms);
|
||||||
|
|
||||||
|
while std::time::Instant::now() < deadline {
|
||||||
|
match socket.recv_from(&mut buf) {
|
||||||
|
Ok((len, _addr)) => {
|
||||||
|
if let Ok(text) = std::str::from_utf8(&buf[..len]) {
|
||||||
|
if let Ok(parsed) = serde_json::from_str::<Announcement>(text) {
|
||||||
|
if parsed.kind == "betterdesk-announce" {
|
||||||
|
if let Some(server) = parsed.server {
|
||||||
|
if let Some(entry) = server_to_discovered(&server) {
|
||||||
|
if !out.iter().any(|x: &DiscoveredServer| x.url == entry.url) {
|
||||||
|
out.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_to_discovered(server: &AnnounceServer) -> Option<DiscoveredServer> {
|
||||||
|
if let Some(url) = server.panel_url.as_ref().filter(|u| !u.is_empty()) {
|
||||||
|
return Some(DiscoveredServer {
|
||||||
|
name: server.name.clone().unwrap_or_else(|| url.clone()),
|
||||||
|
url: url.trim_end_matches('/').to_string(),
|
||||||
|
source: "udp".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let protocol = server.protocol.as_deref().unwrap_or("https");
|
||||||
|
let port = server.port.or(server.api_port)?;
|
||||||
|
let addr = server.addresses.as_ref()?.first()?.clone();
|
||||||
|
let url = format!("{protocol}://{addr}:{port}");
|
||||||
|
Some(DiscoveredServer {
|
||||||
|
name: server.name.clone().unwrap_or_else(|| addr.clone()),
|
||||||
|
url,
|
||||||
|
source: "udp".into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_panel_url_from_announcement() {
|
||||||
|
let server = AnnounceServer {
|
||||||
|
name: Some("Test".into()),
|
||||||
|
version: None,
|
||||||
|
port: Some(5443),
|
||||||
|
api_port: None,
|
||||||
|
protocol: Some("https".into()),
|
||||||
|
addresses: Some(vec!["192.168.1.10".into()]),
|
||||||
|
panel_url: Some("https://desk.local:5443".into()),
|
||||||
|
};
|
||||||
|
let d = server_to_discovered(&server).unwrap();
|
||||||
|
assert_eq!(d.url, "https://desk.local:5443");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,35 @@
|
|||||||
mod config;
|
mod config;
|
||||||
|
mod discovery;
|
||||||
|
mod server_probe;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub mod linux_display;
|
pub mod linux_display;
|
||||||
pub mod tls_policy;
|
pub mod tls_policy;
|
||||||
|
|
||||||
use config::{load_config, save_config};
|
use config::{
|
||||||
|
apply_tls_from_config, clear_config, env_server_url, load_config, load_embedded_server_url,
|
||||||
|
save_config, AppConfig,
|
||||||
|
};
|
||||||
|
use discovery::discover_udp;
|
||||||
|
use server_probe::probe_panel_url;
|
||||||
use tls_policy::apply_window_builder;
|
use tls_policy::apply_window_builder;
|
||||||
use tauri::ipc::CapabilityBuilder;
|
use tauri::ipc::CapabilityBuilder;
|
||||||
use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
|
use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
|
||||||
use tauri_utils::config::BackgroundThrottlingPolicy;
|
use tauri_utils::config::BackgroundThrottlingPolicy;
|
||||||
|
|
||||||
const MAIN_LABEL: &str = "main";
|
const MAIN_LABEL: &str = "main";
|
||||||
const CLIENT_BUILD: &str = "0.1.0-connect-bridge";
|
const SETTINGS_LABEL: &str = "settings";
|
||||||
|
const CLIENT_BUILD: &str = "0.1.0-production";
|
||||||
|
|
||||||
/// Injected before page JS — desktop flag + Connect bridge (works even if panel JS is older).
|
/// Injected before page JS — desktop flag + Connect bridge (works even if panel JS is older).
|
||||||
const DESKTOP_INIT_SCRIPT: &str = r#"
|
const DESKTOP_INIT_SCRIPT: &str = r#"
|
||||||
window.__BETTERDESK_RDCLIENT_DESKTOP__=true;
|
window.__BETTERDESK_RDCLIENT_DESKTOP__=true;
|
||||||
(function(){function m(){document.documentElement.classList.add('rd-desk-desktop');if(document.body)document.body.classList.add('rd-desk-desktop');var a=document.getElementById('rd-desk-app');if(a)a.classList.add('rd-desk-desktop');}if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',m,{once:true});else m();})();
|
(function(){
|
||||||
|
function syncVh(){var h=window.innerHeight;if(h<1)return;document.documentElement.style.setProperty('--rd-desk-vh',h+'px');}
|
||||||
|
function mark(){document.documentElement.classList.add('rd-desk-desktop');if(document.body)document.body.classList.add('rd-desk-desktop');var a=document.getElementById('rd-desk-app');if(a)a.classList.add('rd-desk-desktop');syncVh();if(!window.__rdDeskViewportBound){window.__rdDeskViewportBound=true;window.addEventListener('resize',syncVh);}}
|
||||||
|
syncVh();
|
||||||
|
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',mark,{once:true});else mark();
|
||||||
|
})();
|
||||||
function __rdDesktopInvoke(){return window.__TAURI__&&window.__TAURI__.core&&window.__TAURI__.core.invoke;}
|
function __rdDesktopInvoke(){return window.__TAURI__&&window.__TAURI__.core&&window.__TAURI__.core.invoke;}
|
||||||
function __rdIsSessionViewer(){return /\/remote\/[^/?#]+/.test(location.pathname);}
|
function __rdIsSessionViewer(){return /\/remote\/[^/?#]+/.test(location.pathname);}
|
||||||
function __rdCloseWindow(){
|
function __rdCloseWindow(){
|
||||||
@@ -57,6 +70,21 @@ document.addEventListener('click',function(e){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},true);
|
},true);
|
||||||
|
(function(){
|
||||||
|
if(document.cookie.indexOf('betterdesk_lang=')>=0)return;
|
||||||
|
var inv=window.__TAURI__&&window.__TAURI__.core&&window.__TAURI__.core.invoke;
|
||||||
|
if(!inv)return;
|
||||||
|
inv('get_config').then(function(cfg){
|
||||||
|
var pref=cfg&&cfg.ui_lang;
|
||||||
|
if(pref){document.cookie='betterdesk_lang='+encodeURIComponent(pref)+';path=/;max-age=31536000';return;}
|
||||||
|
return inv('get_system_locale').then(function(locale){
|
||||||
|
if(!locale)return;
|
||||||
|
var code=String(locale).split(/[-_]/)[0].toLowerCase();
|
||||||
|
if(!code)return;
|
||||||
|
return fetch('/api/i18n/set/'+encodeURIComponent(code),{method:'POST',credentials:'include',headers:{'X-Requested-With':'XMLHttpRequest'}});
|
||||||
|
});
|
||||||
|
}).catch(function(){});
|
||||||
|
})();
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const PANEL_INVOKE_PERMISSIONS: &[&str] = &[
|
const PANEL_INVOKE_PERMISSIONS: &[&str] = &[
|
||||||
@@ -65,15 +93,25 @@ const PANEL_INVOKE_PERMISSIONS: &[&str] = &[
|
|||||||
"core:window:allow-set-focus",
|
"core:window:allow-set-focus",
|
||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
"core:webview:allow-create-webview-window",
|
"core:webview:allow-create-webview-window",
|
||||||
|
"core:webview:allow-clear-all-browsing-data",
|
||||||
"shell:allow-open",
|
"shell:allow-open",
|
||||||
|
"allow-get-client-info",
|
||||||
"allow-get-server-url",
|
"allow-get-server-url",
|
||||||
"allow-set-server-url",
|
"allow-set-server-url",
|
||||||
|
"allow-probe-server-url",
|
||||||
|
"allow-discover-servers",
|
||||||
|
"allow-get-config",
|
||||||
|
"allow-set-tls-strict",
|
||||||
|
"allow-set-ui-lang",
|
||||||
|
"allow-open-settings",
|
||||||
|
"allow-sign-out",
|
||||||
|
"allow-reset-client",
|
||||||
|
"allow-get-system-locale",
|
||||||
"allow-open-session",
|
"allow-open-session",
|
||||||
"allow-close-current-window",
|
"allow-close-current-window",
|
||||||
"allow-get-client-info",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
fn normalize_server_url(raw: &str) -> Result<String, String> {
|
pub fn normalize_server_url(raw: &str) -> Result<String, String> {
|
||||||
let trimmed = raw.trim();
|
let trimmed = raw.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
return Err("Server URL is required".into());
|
return Err("Server URL is required".into());
|
||||||
@@ -88,8 +126,7 @@ fn normalize_server_url(raw: &str) -> Result<String, String> {
|
|||||||
format!("https://{trimmed}")
|
format!("https://{trimmed}")
|
||||||
};
|
};
|
||||||
|
|
||||||
let parsed = url::parse_helper(&with_scheme)?;
|
url::parse_helper(&with_scheme)
|
||||||
Ok(parsed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mod url {
|
mod url {
|
||||||
@@ -128,6 +165,7 @@ fn register_panel_remote_capability(app: &AppHandle, base: &str) -> Result<(), S
|
|||||||
let mut builder = CapabilityBuilder::new("panel-operator")
|
let mut builder = CapabilityBuilder::new("panel-operator")
|
||||||
.local(true)
|
.local(true)
|
||||||
.window(MAIN_LABEL)
|
.window(MAIN_LABEL)
|
||||||
|
.window(SETTINGS_LABEL)
|
||||||
.window("session-*");
|
.window("session-*");
|
||||||
|
|
||||||
for pattern in patterns {
|
for pattern in patterns {
|
||||||
@@ -180,15 +218,57 @@ fn apply_desktop_window<R: tauri::Runtime, M: tauri::Manager<R>>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn close_session_windows(app: &AppHandle) {
|
||||||
|
for label in app
|
||||||
|
.webview_windows()
|
||||||
|
.keys()
|
||||||
|
.filter(|l| l.starts_with("session-"))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
{
|
||||||
|
if let Some(w) = app.get_webview_window(&label) {
|
||||||
|
let _ = w.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_all_webview_data(app: &AppHandle) -> Result<(), String> {
|
||||||
|
for (_label, window) in app.webview_windows() {
|
||||||
|
window
|
||||||
|
.clear_all_browsing_data()
|
||||||
|
.map_err(|e| format!("Failed to clear browsing data: {e}"))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_server_url(app: &AppHandle, url: String, via: Option<&str>) -> Result<(), String> {
|
||||||
|
let cfg = load_config(app)?;
|
||||||
|
let result = probe_panel_url(&url, cfg.tls_strict).await;
|
||||||
|
if !result.ok {
|
||||||
|
return Err(
|
||||||
|
result
|
||||||
|
.error
|
||||||
|
.unwrap_or_else(|| "Server did not respond as a BetterDesk panel".into()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized = result.normalized_url;
|
||||||
|
register_panel_remote_capability(app, &normalized)?;
|
||||||
|
let mut cfg = load_config(app)?;
|
||||||
|
cfg.server_url = Some(normalized.clone());
|
||||||
|
if let Some(source) = via {
|
||||||
|
cfg.discovered_via = Some(source.to_string());
|
||||||
|
}
|
||||||
|
save_config(app, &cfg)?;
|
||||||
|
open_main_window(app, WebviewUrl::External(dashboard_url(&normalized)?))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn open_main_window(app: &AppHandle, url: WebviewUrl) -> Result<(), String> {
|
fn open_main_window(app: &AppHandle, url: WebviewUrl) -> Result<(), String> {
|
||||||
if let Some(existing) = app.get_webview_window(MAIN_LABEL) {
|
if let Some(existing) = app.get_webview_window(MAIN_LABEL) {
|
||||||
if let WebviewUrl::External(parsed) = url {
|
if let WebviewUrl::External(parsed) = url {
|
||||||
existing
|
existing.navigate(parsed).map_err(|e| e.to_string())?;
|
||||||
.navigate(parsed)
|
existing.set_focus().map_err(|e| e.to_string())?;
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
existing
|
|
||||||
.set_focus()
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
existing.close().map_err(|e| e.to_string())?;
|
existing.close().map_err(|e| e.to_string())?;
|
||||||
@@ -206,10 +286,47 @@ fn open_main_window(app: &AppHandle, url: WebviewUrl) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn try_auto_configure_server(app: &AppHandle) -> Result<bool, String> {
|
||||||
|
let cfg = load_config(app)?;
|
||||||
|
if cfg.server_url.is_some() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let candidate = match env_server_url().or_else(load_embedded_server_url) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Ok(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Handle::current();
|
||||||
|
let probe = rt.block_on(probe_panel_url(&candidate, cfg.tls_strict));
|
||||||
|
if !probe.ok {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
register_panel_remote_capability(app, &probe.normalized_url)?;
|
||||||
|
let mut updated = load_config(app)?;
|
||||||
|
updated.server_url = Some(probe.normalized_url);
|
||||||
|
updated.discovered_via = Some(
|
||||||
|
if env_server_url().is_some() {
|
||||||
|
"env"
|
||||||
|
} else {
|
||||||
|
"embedded"
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
save_config(app, &updated)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
fn launch_main(app: &AppHandle) -> Result<(), String> {
|
fn launch_main(app: &AppHandle) -> Result<(), String> {
|
||||||
let cfg = load_config(app)?;
|
let cfg = load_config(app)?;
|
||||||
let url = if let Some(base) = cfg.server_url {
|
apply_tls_from_config(&cfg);
|
||||||
WebviewUrl::External(dashboard_url(&base)?)
|
|
||||||
|
let url = if cfg.server_url.is_some() {
|
||||||
|
WebviewUrl::External(dashboard_url(cfg.server_url.as_ref().unwrap())?)
|
||||||
|
} else if try_auto_configure_server(app)? {
|
||||||
|
let updated = load_config(app)?;
|
||||||
|
WebviewUrl::External(dashboard_url(updated.server_url.as_ref().unwrap())?)
|
||||||
} else {
|
} else {
|
||||||
WebviewUrl::App("setup.html".into())
|
WebviewUrl::App("setup.html".into())
|
||||||
};
|
};
|
||||||
@@ -230,16 +347,104 @@ fn get_server_url(app: AppHandle) -> Result<Option<String>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn set_server_url(app: AppHandle, url: String) -> Result<(), String> {
|
fn get_config(app: AppHandle) -> Result<AppConfig, String> {
|
||||||
let normalized = normalize_server_url(&url)?;
|
load_config(&app)
|
||||||
register_panel_remote_capability(&app, &normalized)?;
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn probe_server_url(app: AppHandle, url: String) -> Result<server_probe::ServerProbeResult, String> {
|
||||||
|
let cfg = load_config(&app)?;
|
||||||
|
Ok(probe_panel_url(&url, cfg.tls_strict).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn discover_servers() -> Vec<discovery::DiscoveredServer> {
|
||||||
|
discover_udp(2500)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn set_server_url(app: AppHandle, url: String) -> Result<(), String> {
|
||||||
|
save_server_url(&app, url, Some("manual")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_tls_strict(app: AppHandle, strict: bool) -> Result<(), String> {
|
||||||
let mut cfg = load_config(&app)?;
|
let mut cfg = load_config(&app)?;
|
||||||
cfg.server_url = Some(normalized.clone());
|
cfg.tls_strict = strict;
|
||||||
save_config(&app, &cfg)?;
|
save_config(&app, &cfg)?;
|
||||||
open_main_window(
|
if strict {
|
||||||
&app,
|
unsafe {
|
||||||
WebviewUrl::External(dashboard_url(&normalized)?),
|
std::env::set_var("BETTERDESK_TLS_STRICT", "1");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var("BETTERDESK_TLS_STRICT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_ui_lang(app: AppHandle, lang: Option<String>) -> Result<(), String> {
|
||||||
|
let mut cfg = load_config(&app)?;
|
||||||
|
cfg.ui_lang = lang.filter(|s| !s.trim().is_empty());
|
||||||
|
save_config(&app, &cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_system_locale() -> Option<String> {
|
||||||
|
sys_locale::get_locale()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn open_settings(app: AppHandle) -> Result<(), String> {
|
||||||
|
if let Some(existing) = app.get_webview_window(SETTINGS_LABEL) {
|
||||||
|
existing.set_focus().map_err(|e| e.to_string())?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_desktop_window(
|
||||||
|
WebviewWindowBuilder::new(&app, SETTINGS_LABEL, WebviewUrl::App("settings.html".into()))
|
||||||
|
.title("BetterDesk RdClient — Settings")
|
||||||
|
.inner_size(520.0, 640.0)
|
||||||
|
.resizable(true)
|
||||||
|
.center(),
|
||||||
)
|
)
|
||||||
|
.build()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn sign_out(app: AppHandle) -> Result<(), String> {
|
||||||
|
close_session_windows(&app);
|
||||||
|
clear_all_webview_data(&app)?;
|
||||||
|
|
||||||
|
let cfg = load_config(&app)?;
|
||||||
|
if let Some(base) = cfg.server_url {
|
||||||
|
open_main_window(
|
||||||
|
&app,
|
||||||
|
WebviewUrl::External(
|
||||||
|
tauri::Url::parse(&format!("{}/remote/login", base.trim_end_matches('/')))
|
||||||
|
.map_err(|e| e.to_string())?,
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn reset_client(app: AppHandle) -> Result<(), String> {
|
||||||
|
close_session_windows(&app);
|
||||||
|
clear_all_webview_data(&app)?;
|
||||||
|
clear_config(&app)?;
|
||||||
|
|
||||||
|
if let Some(settings) = app.get_webview_window(SETTINGS_LABEL) {
|
||||||
|
let _ = settings.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
open_main_window(&app, WebviewUrl::App("setup.html".into()))?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -295,12 +500,21 @@ pub fn run() {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
get_client_info,
|
get_client_info,
|
||||||
get_server_url,
|
get_server_url,
|
||||||
|
get_config,
|
||||||
|
probe_server_url,
|
||||||
|
discover_servers,
|
||||||
set_server_url,
|
set_server_url,
|
||||||
|
set_tls_strict,
|
||||||
|
set_ui_lang,
|
||||||
|
get_system_locale,
|
||||||
|
open_settings,
|
||||||
|
sign_out,
|
||||||
|
reset_client,
|
||||||
open_session,
|
open_session,
|
||||||
close_current_window
|
close_current_window
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
if let Some(base) = load_config(app.handle())?.server_url {
|
if let Some(base) = load_config(app.handle())?.server_url.clone() {
|
||||||
register_panel_remote_capability(app.handle(), &base)?;
|
register_panel_remote_capability(app.handle(), &base)?;
|
||||||
}
|
}
|
||||||
launch_main(app.handle())?;
|
launch_main(app.handle())?;
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ServerProbeResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub normalized_url: String,
|
||||||
|
pub product: Option<String>,
|
||||||
|
pub version: Option<String>,
|
||||||
|
pub panel_name: Option<String>,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn probe_panel_url(base: &str, tls_strict: bool) -> ServerProbeResult {
|
||||||
|
let normalized = match super::normalize_server_url(base) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => {
|
||||||
|
return ServerProbeResult {
|
||||||
|
ok: false,
|
||||||
|
normalized_url: base.trim().to_string(),
|
||||||
|
product: None,
|
||||||
|
version: None,
|
||||||
|
panel_name: None,
|
||||||
|
error: Some(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!("{}/api/bd/server-info", normalized.trim_end_matches('/'));
|
||||||
|
let client = match build_http_client(tls_strict) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
return ServerProbeResult {
|
||||||
|
ok: false,
|
||||||
|
normalized_url: normalized,
|
||||||
|
product: None,
|
||||||
|
version: None,
|
||||||
|
panel_name: None,
|
||||||
|
error: Some(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match client.get(&url).send().await {
|
||||||
|
Ok(resp) => {
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return ServerProbeResult {
|
||||||
|
ok: false,
|
||||||
|
normalized_url: normalized,
|
||||||
|
product: None,
|
||||||
|
version: None,
|
||||||
|
panel_name: None,
|
||||||
|
error: Some(format!("HTTP {}", resp.status())),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match resp.json::<serde_json::Value>().await {
|
||||||
|
Ok(body) => {
|
||||||
|
let ok = body.get("ok").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
|
ServerProbeResult {
|
||||||
|
ok,
|
||||||
|
normalized_url: normalized,
|
||||||
|
product: body
|
||||||
|
.get("product")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(String::from),
|
||||||
|
version: body
|
||||||
|
.get("version")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(String::from),
|
||||||
|
panel_name: body
|
||||||
|
.get("panel_name")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(String::from),
|
||||||
|
error: if ok {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some("Not a BetterDesk panel".into())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => ServerProbeResult {
|
||||||
|
ok: false,
|
||||||
|
normalized_url: normalized,
|
||||||
|
product: None,
|
||||||
|
version: None,
|
||||||
|
panel_name: None,
|
||||||
|
error: Some(format!("Invalid JSON: {e}")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => ServerProbeResult {
|
||||||
|
ok: false,
|
||||||
|
normalized_url: normalized,
|
||||||
|
product: None,
|
||||||
|
version: None,
|
||||||
|
panel_name: None,
|
||||||
|
error: Some(format!("Connection failed: {e}")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_http_client(tls_strict: bool) -> Result<reqwest::Client, String> {
|
||||||
|
let mut builder = reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(8))
|
||||||
|
.user_agent("BetterDesk-RdClient/0.1");
|
||||||
|
|
||||||
|
if !tls_strict {
|
||||||
|
builder = builder.danger_accept_invalid_certs(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.build().map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>BetterDesk RdClient — Settings</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: #0d1117;
|
||||||
|
--panel: #161b22;
|
||||||
|
--border: #30363d;
|
||||||
|
--text: #e6edf3;
|
||||||
|
--muted: #8b949e;
|
||||||
|
--accent: #58a6ff;
|
||||||
|
--error: #f85149;
|
||||||
|
--warn: #d29922;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
h1 { margin: 0 0 4px; font-size: 1.125rem; }
|
||||||
|
.subtitle { margin: 0 0 20px; color: var(--muted); font-size: 0.8125rem; }
|
||||||
|
section { margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
|
||||||
|
section:last-of-type { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
|
||||||
|
h2 { margin: 0 0 12px; font-size: 0.8125rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
|
||||||
|
label { display: block; margin-bottom: 6px; font-size: 0.8125rem; color: var(--muted); }
|
||||||
|
input, select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: #21262d;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.hint { margin-top: 6px; font-size: 0.75rem; color: var(--muted); line-height: 1.4; }
|
||||||
|
.warn { color: var(--warn); }
|
||||||
|
button {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 9px 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-primary { background: var(--accent); color: #fff; width: 100%; }
|
||||||
|
.btn-secondary { background: #21262d; border: 1px solid var(--border); color: var(--text); width: 100%; }
|
||||||
|
.btn-danger { background: #3d1214; border: 1px solid var(--error); color: #ffb4b0; width: 100%; }
|
||||||
|
button:disabled { opacity: 0.6; cursor: wait; }
|
||||||
|
.checkbox-row { display: flex; align-items: flex-start; gap: 10px; margin-top: 8px; }
|
||||||
|
.checkbox-row input { width: auto; margin-top: 3px; }
|
||||||
|
.checkbox-row label { margin: 0; color: var(--text); }
|
||||||
|
.status { margin-top: 8px; font-size: 0.8125rem; min-height: 1.2em; }
|
||||||
|
.status.error { color: var(--error); }
|
||||||
|
.status.ok { color: #3fb950; }
|
||||||
|
.server-meta { font-size: 0.75rem; color: var(--muted); margin-top: 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>RdClient Settings</h1>
|
||||||
|
<p class="subtitle">Local desktop preferences and account actions.</p>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Server</h2>
|
||||||
|
<label for="server-url">Panel URL</label>
|
||||||
|
<input type="url" id="server-url" placeholder="https://desk.example.com">
|
||||||
|
<div class="server-meta" id="server-meta"></div>
|
||||||
|
<button type="button" class="btn-primary" id="save-url-btn">Save & reconnect</button>
|
||||||
|
<div class="status" id="url-status"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Security</h2>
|
||||||
|
<div class="checkbox-row">
|
||||||
|
<input type="checkbox" id="tls-strict">
|
||||||
|
<label for="tls-strict">Strict TLS (reject self-signed certificates)</label>
|
||||||
|
</div>
|
||||||
|
<p class="hint warn">Only enable if your panel uses a trusted CA certificate. Self-signed panels require this to stay off.</p>
|
||||||
|
<button type="button" class="btn-secondary" id="save-tls-btn">Save TLS setting</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Language</h2>
|
||||||
|
<label for="ui-lang">Preferred UI language (panel pages)</label>
|
||||||
|
<select id="ui-lang">
|
||||||
|
<option value="">System default</option>
|
||||||
|
</select>
|
||||||
|
<p class="hint">Applied on next panel page load. Use the language menu in the dashboard for immediate change.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Session</h2>
|
||||||
|
<button type="button" class="btn-secondary" id="sign-out-btn">Sign out</button>
|
||||||
|
<p class="hint">Clears panel login cookies and opens the login page.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Reset</h2>
|
||||||
|
<button type="button" class="btn-danger" id="reset-btn">Reset client</button>
|
||||||
|
<p class="hint">Removes server URL, cookies, saved passwords, and returns to setup.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<script src="settings.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var LANGS = [
|
||||||
|
['', 'System default'],
|
||||||
|
['en', 'English'],
|
||||||
|
['pl', 'Polski'],
|
||||||
|
['de', 'Deutsch'],
|
||||||
|
['fr', 'Français'],
|
||||||
|
['es', 'Español'],
|
||||||
|
['it', 'Italiano'],
|
||||||
|
['pt', 'Português'],
|
||||||
|
['nl', 'Nederlands'],
|
||||||
|
['cs', 'Čeština'],
|
||||||
|
['da', 'Dansk'],
|
||||||
|
['fi', 'Suomi'],
|
||||||
|
['nb', 'Norsk'],
|
||||||
|
['sv', 'Svenska'],
|
||||||
|
['ro', 'Română'],
|
||||||
|
['hu', 'Magyar'],
|
||||||
|
['uk', 'Українська'],
|
||||||
|
['ru', 'Русский'],
|
||||||
|
['tr', 'Türkçe'],
|
||||||
|
['ar', 'العربية'],
|
||||||
|
['hi', 'हिन्दी'],
|
||||||
|
['ja', '日本語'],
|
||||||
|
['ko', '한국어'],
|
||||||
|
['zh', '简体中文'],
|
||||||
|
['zh-TW', '繁體中文'],
|
||||||
|
['vi', 'Tiếng Việt'],
|
||||||
|
['th', 'ไทย'],
|
||||||
|
['id', 'Bahasa Indonesia']
|
||||||
|
];
|
||||||
|
|
||||||
|
var urlInput = document.getElementById('server-url');
|
||||||
|
var urlStatus = document.getElementById('url-status');
|
||||||
|
var serverMeta = document.getElementById('server-meta');
|
||||||
|
var tlsCheckbox = document.getElementById('tls-strict');
|
||||||
|
var langSelect = document.getElementById('ui-lang');
|
||||||
|
|
||||||
|
function status(el, msg, kind) {
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = msg || '';
|
||||||
|
el.className = 'status' + (kind ? ' ' + kind : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invoke(cmd, args) {
|
||||||
|
if (!window.__TAURI__ || !window.__TAURI__.core || !window.__TAURI__.core.invoke) {
|
||||||
|
throw new Error('Desktop bridge unavailable');
|
||||||
|
}
|
||||||
|
return window.__TAURI__.core.invoke(cmd, args || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillLangSelect(selected) {
|
||||||
|
if (!langSelect) return;
|
||||||
|
langSelect.innerHTML = '';
|
||||||
|
LANGS.forEach(function (pair) {
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.value = pair[0];
|
||||||
|
opt.textContent = pair[1];
|
||||||
|
if (pair[0] === (selected || '')) opt.selected = true;
|
||||||
|
langSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
var cfg = await invoke('get_config');
|
||||||
|
if (urlInput && cfg.server_url) urlInput.value = cfg.server_url;
|
||||||
|
if (tlsCheckbox) tlsCheckbox.checked = !!cfg.tls_strict;
|
||||||
|
fillLangSelect(cfg.ui_lang || '');
|
||||||
|
if (cfg.server_url) {
|
||||||
|
var probe = await invoke('probe_server_url', { url: cfg.server_url });
|
||||||
|
if (probe && probe.ok && serverMeta) {
|
||||||
|
var parts = [];
|
||||||
|
if (probe.panel_name) parts.push(probe.panel_name);
|
||||||
|
if (probe.version) parts.push('v' + probe.version);
|
||||||
|
serverMeta.textContent = parts.join(' · ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
status(urlStatus, String(e), 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('save-url-btn').addEventListener('click', async function () {
|
||||||
|
var url = (urlInput.value || '').trim();
|
||||||
|
if (!url) {
|
||||||
|
status(urlStatus, 'Enter a panel URL.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
status(urlStatus, 'Validating…', '');
|
||||||
|
try {
|
||||||
|
await invoke('set_server_url', { url: url });
|
||||||
|
status(urlStatus, 'Saved. Reconnecting…', 'ok');
|
||||||
|
} catch (e) {
|
||||||
|
status(urlStatus, String(e), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('save-tls-btn').addEventListener('click', async function () {
|
||||||
|
try {
|
||||||
|
await invoke('set_tls_strict', { strict: !!tlsCheckbox.checked });
|
||||||
|
status(urlStatus, 'TLS setting saved. Restart may be required for existing connections.', 'ok');
|
||||||
|
} catch (e) {
|
||||||
|
status(urlStatus, String(e), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (langSelect) {
|
||||||
|
langSelect.addEventListener('change', async function () {
|
||||||
|
try {
|
||||||
|
await invoke('set_ui_lang', { lang: langSelect.value || null });
|
||||||
|
} catch (_) { /* ignore */ }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('sign-out-btn').addEventListener('click', async function () {
|
||||||
|
if (!confirm('Sign out from the panel on this device?')) return;
|
||||||
|
try {
|
||||||
|
await invoke('sign_out');
|
||||||
|
} catch (e) {
|
||||||
|
status(urlStatus, String(e), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('reset-btn').addEventListener('click', async function () {
|
||||||
|
if (!confirm('Reset RdClient? This removes the server URL and all saved data on this device.')) return;
|
||||||
|
try {
|
||||||
|
await invoke('reset_client');
|
||||||
|
} catch (e) {
|
||||||
|
status(urlStatus, String(e), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loadConfig();
|
||||||
|
})();
|
||||||
@@ -29,29 +29,16 @@
|
|||||||
}
|
}
|
||||||
.card {
|
.card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 440px;
|
max-width: 480px;
|
||||||
padding: 28px;
|
padding: 28px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
}
|
}
|
||||||
h1 {
|
h1 { margin: 0 0 8px; font-size: 1.25rem; font-weight: 600; }
|
||||||
margin: 0 0 8px;
|
h2 { margin: 20px 0 8px; font-size: 0.875rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
|
||||||
font-size: 1.25rem;
|
p { margin: 0 0 16px; color: var(--muted); font-size: 0.875rem; line-height: 1.5; }
|
||||||
font-weight: 600;
|
label { display: block; margin-bottom: 6px; font-size: 0.8125rem; color: var(--muted); }
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0 0 20px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
@@ -61,13 +48,8 @@
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
input:focus {
|
input:focus { outline: none; border-color: var(--accent); }
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
button {
|
button {
|
||||||
margin-top: 16px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -77,23 +59,47 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
button.secondary { background: #21262d; border: 1px solid var(--border); color: var(--text); }
|
||||||
button:disabled { opacity: 0.6; cursor: wait; }
|
button:disabled { opacity: 0.6; cursor: wait; }
|
||||||
.error {
|
.row { display: flex; gap: 8px; margin-top: 16px; }
|
||||||
margin-top: 12px;
|
.row button { flex: 1; }
|
||||||
color: var(--error);
|
.error { margin-top: 12px; color: var(--error); font-size: 0.8125rem; min-height: 1.2em; }
|
||||||
font-size: 0.8125rem;
|
.discovered-list { list-style: none; margin: 0; padding: 0; }
|
||||||
min-height: 1.2em;
|
.discovered-list li { margin-bottom: 8px; }
|
||||||
|
.discovered-list button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
background: #21262d;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
.discovered-list button:hover { border-color: var(--accent); }
|
||||||
|
.discovered-name { display: block; font-size: 0.875rem; }
|
||||||
|
.discovered-url { display: block; font-size: 0.75rem; color: var(--muted); margin-top: 2px; }
|
||||||
|
.empty-hint { font-size: 0.8125rem; color: var(--muted); font-style: italic; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h1>BetterDesk RdClient</h1>
|
<h1>BetterDesk RdClient</h1>
|
||||||
<p>Enter the URL of your BetterDesk console panel. The dashboard will load from <code>/remote</code> on this server.</p>
|
<p>Connect to your BetterDesk console panel. The dashboard loads from <code>/remote</code> on the server you choose.</p>
|
||||||
|
|
||||||
|
<h2>Servers on your network</h2>
|
||||||
|
<ul class="discovered-list" id="discovered-list">
|
||||||
|
<li class="empty-hint" id="discovered-empty">Scanning…</li>
|
||||||
|
</ul>
|
||||||
|
<div class="row">
|
||||||
|
<button type="button" class="secondary" id="refresh-btn">Refresh</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Manual URL</h2>
|
||||||
<form id="setup-form">
|
<form id="setup-form">
|
||||||
<label for="server-url">Panel URL</label>
|
<label for="server-url">Panel URL</label>
|
||||||
<input type="url" id="server-url" placeholder="https://desk.example.com" required autofocus>
|
<input type="url" id="server-url" placeholder="https://desk.example.com" required>
|
||||||
<button type="submit" id="submit-btn">Continue</button>
|
<div class="row">
|
||||||
|
<button type="submit" id="submit-btn">Continue</button>
|
||||||
|
</div>
|
||||||
<div class="error" id="error"></div>
|
<div class="error" id="error"></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
var input = document.getElementById('server-url');
|
var input = document.getElementById('server-url');
|
||||||
var errorEl = document.getElementById('error');
|
var errorEl = document.getElementById('error');
|
||||||
var submitBtn = document.getElementById('submit-btn');
|
var submitBtn = document.getElementById('submit-btn');
|
||||||
|
var listEl = document.getElementById('discovered-list');
|
||||||
|
var emptyEl = document.getElementById('discovered-empty');
|
||||||
|
var refreshBtn = document.getElementById('refresh-btn');
|
||||||
|
|
||||||
function showError(msg) {
|
function showError(msg) {
|
||||||
if (errorEl) errorEl.textContent = msg || '';
|
if (errorEl) errorEl.textContent = msg || '';
|
||||||
@@ -17,34 +20,94 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
form.addEventListener('submit', async function (e) {
|
async function connectUrl(url) {
|
||||||
e.preventDefault();
|
|
||||||
showError('');
|
|
||||||
var url = (input.value || '').trim();
|
|
||||||
if (!url) {
|
|
||||||
showError('Please enter a panel URL.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var invoke = await getInvoke();
|
var invoke = await getInvoke();
|
||||||
if (!invoke) {
|
if (!invoke) {
|
||||||
showError('Desktop bridge unavailable.');
|
showError('Desktop bridge unavailable.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
|
if (refreshBtn) refreshBtn.disabled = true;
|
||||||
|
showError('');
|
||||||
try {
|
try {
|
||||||
await invoke('set_server_url', { url: url });
|
var probe = await invoke('probe_server_url', { url: url });
|
||||||
|
if (!probe || !probe.ok) {
|
||||||
|
showError((probe && probe.error) || 'Server did not respond as a BetterDesk panel.');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
if (refreshBtn) refreshBtn.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await invoke('set_server_url', { url: probe.normalized_url || url });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(String(err) || 'Failed to save server URL.');
|
showError(String(err) || 'Failed to save server URL.');
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
|
if (refreshBtn) refreshBtn.disabled = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDiscovered(servers) {
|
||||||
|
if (!listEl) return;
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
if (!servers || !servers.length) {
|
||||||
|
var li = document.createElement('li');
|
||||||
|
li.className = 'empty-hint';
|
||||||
|
li.textContent = 'No servers found on the local network.';
|
||||||
|
listEl.appendChild(li);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
servers.forEach(function (s) {
|
||||||
|
var li = document.createElement('li');
|
||||||
|
var btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.innerHTML = '<span class="discovered-name">' + escapeHtml(s.name || s.url) + '</span>'
|
||||||
|
+ '<span class="discovered-url">' + escapeHtml(s.url) + '</span>';
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
if (input) input.value = s.url;
|
||||||
|
connectUrl(s.url);
|
||||||
|
});
|
||||||
|
li.appendChild(btn);
|
||||||
|
listEl.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scanNetwork() {
|
||||||
|
var invoke = await getInvoke();
|
||||||
|
if (!invoke) {
|
||||||
|
if (emptyEl) emptyEl.textContent = 'Discovery unavailable.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (emptyEl) emptyEl.textContent = 'Scanning…';
|
||||||
|
try {
|
||||||
|
var servers = await invoke('discover_servers');
|
||||||
|
renderDiscovered(servers || []);
|
||||||
|
} catch (_) {
|
||||||
|
renderDiscovered([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var url = (input.value || '').trim();
|
||||||
|
if (!url) {
|
||||||
|
showError('Please enter a panel URL.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
connectUrl(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (refreshBtn) {
|
||||||
|
refreshBtn.addEventListener('click', function () { scanNetwork(); });
|
||||||
|
}
|
||||||
|
|
||||||
getInvoke().then(function (invoke) {
|
getInvoke().then(function (invoke) {
|
||||||
if (!invoke) return;
|
if (!invoke) return;
|
||||||
invoke('get_server_url').then(function (existing) {
|
invoke('get_server_url').then(function (existing) {
|
||||||
if (existing && input) input.value = existing;
|
if (existing && input) input.value = existing;
|
||||||
}).catch(function () { /* ignore */ });
|
}).catch(function () { /* ignore */ });
|
||||||
|
scanNetwork();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
+20
-3
@@ -745,7 +745,11 @@
|
|||||||
"build_status_failed": "فشل",
|
"build_status_failed": "فشل",
|
||||||
"builds_summary": "{{ready}} جاهز · {{pending}} في الانتظار · {{building}} جارٍ البناء · {{failed}} فشل",
|
"builds_summary": "{{ready}} جاهز · {{pending}} في الانتظار · {{building}} جارٍ البناء · {{failed}} فشل",
|
||||||
"build_error_hint": "خطأ في البناء",
|
"build_error_hint": "خطأ في البناء",
|
||||||
"preview_help": "طلب المساعدة"
|
"preview_help": "طلب المساعدة",
|
||||||
|
"rdclient_tab": "RdClient سطح المكتب",
|
||||||
|
"rdclient_new_bundle": "حزمة RdClient جديدة",
|
||||||
|
"rdclient_subtitle": "إنشاء مثبتات RdClient مع رابط اللوحة المضمن",
|
||||||
|
"rdclient_server_url_hint": "الرابط العام لوحة BetterDesk (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1051,7 +1055,9 @@
|
|||||||
"file_loading": "تحميل...",
|
"file_loading": "تحميل...",
|
||||||
"file_timeout": "الجهاز البعيد لم يستجب. قد لا يكون نقل الملفات مدعومًا أو معطلاً على الجهاز البعيد.",
|
"file_timeout": "الجهاز البعيد لم يستجب. قد لا يكون نقل الملفات مدعومًا أو معطلاً على الجهاز البعيد.",
|
||||||
"codec": "ترميز الفيديو",
|
"codec": "ترميز الفيديو",
|
||||||
"codec_auto": "تلقائي (مستحسن)"
|
"codec_auto": "تلقائي (مستحسن)",
|
||||||
|
"remember_peer_password": "تذكر كلمة مرور الجهاز على هذا الحاسوب",
|
||||||
|
"save_peer_password": "حفظ كلمة المرور"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "هوية العلامة التجارية",
|
"identity_title": "هوية العلامة التجارية",
|
||||||
@@ -3835,7 +3841,8 @@
|
|||||||
"no_groups": "لا توجد مجموعات",
|
"no_groups": "لا توجد مجموعات",
|
||||||
"no_tags": "لا توجد وسوم",
|
"no_tags": "لا توجد وسوم",
|
||||||
"collapse_devices": "طي قائمة الأجهزة",
|
"collapse_devices": "طي قائمة الأجهزة",
|
||||||
"expand_devices": "توسيع قائمة الأجهزة"
|
"expand_devices": "توسيع قائمة الأجهزة",
|
||||||
|
"open_settings": "الإعدادات"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "عميل سطح المكتب البعيد",
|
"title": "عميل سطح المكتب البعيد",
|
||||||
@@ -3855,5 +3862,15 @@
|
|||||||
"network_error": "خطأ في الشبكة",
|
"network_error": "خطأ في الشبكة",
|
||||||
"enter_6_digits": "أدخل جميع الأرقام الستة",
|
"enter_6_digits": "أدخل جميع الأرقام الستة",
|
||||||
"invalid_code": "رمز غير صالح"
|
"invalid_code": "رمز غير صالح"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "إعدادات RdClient",
|
||||||
|
"open": "الإعدادات",
|
||||||
|
"server_url": "رابط اللوحة",
|
||||||
|
"tls_strict": "TLS صارم",
|
||||||
|
"sign_out": "تسجيل الخروج",
|
||||||
|
"reset_client": "إعادة ضبط العميل",
|
||||||
|
"discovery_refresh": "تحديث",
|
||||||
|
"discovery_empty": "لم يتم العثور على خوادم في الشبكة المحلية"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -738,7 +738,11 @@
|
|||||||
"build_status_failed": "Neudany",
|
"build_status_failed": "Neudany",
|
||||||
"builds_summary": "{{ready}} gotowych · {{pending}} w kolejce · {{building}} sestavování · {{failed}} nieudanych",
|
"builds_summary": "{{ready}} gotowych · {{pending}} w kolejce · {{building}} sestavování · {{failed}} nieudanych",
|
||||||
"build_error_hint": "Blad buildu",
|
"build_error_hint": "Blad buildu",
|
||||||
"preview_help": "Požádat o pomoc"
|
"preview_help": "Požádat o pomoc",
|
||||||
|
"rdclient_tab": "RdClient desktop",
|
||||||
|
"rdclient_new_bundle": "Nový balíček RdClient",
|
||||||
|
"rdclient_subtitle": "Sestavit instalátory RdClient s vloženou URL panelu",
|
||||||
|
"rdclient_server_url_hint": "Veřejná URL konzole BetterDesk (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1044,7 +1048,9 @@
|
|||||||
"file_loading": "Načítání...",
|
"file_loading": "Načítání...",
|
||||||
"file_timeout": "Vzdálené zařízení neodpovědělo. Přenos souborů nemusí být podporován nebo je na vzdáleném zařízení zakázán.",
|
"file_timeout": "Vzdálené zařízení neodpovědělo. Přenos souborů nemusí být podporován nebo je na vzdáleném zařízení zakázán.",
|
||||||
"codec": "Kodek videa",
|
"codec": "Kodek videa",
|
||||||
"codec_auto": "Automaticky (doporučeno)"
|
"codec_auto": "Automaticky (doporučeno)",
|
||||||
|
"remember_peer_password": "Zapamatovat heslo zařízení na tomto počítači",
|
||||||
|
"save_peer_password": "Uložit heslo"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Identita značky",
|
"identity_title": "Identita značky",
|
||||||
@@ -3828,7 +3834,8 @@
|
|||||||
"no_groups": "Žádné skupiny",
|
"no_groups": "Žádné skupiny",
|
||||||
"no_tags": "Žádné štítky",
|
"no_tags": "Žádné štítky",
|
||||||
"collapse_devices": "Sbalit seznam zařízení",
|
"collapse_devices": "Sbalit seznam zařízení",
|
||||||
"expand_devices": "Rozbalit seznam zařízení"
|
"expand_devices": "Rozbalit seznam zařízení",
|
||||||
|
"open_settings": "Nastavení"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Klient vzdálené plochy",
|
"title": "Klient vzdálené plochy",
|
||||||
@@ -3848,5 +3855,15 @@
|
|||||||
"network_error": "Chyba sítě",
|
"network_error": "Chyba sítě",
|
||||||
"enter_6_digits": "Zadejte všech 6 číslic",
|
"enter_6_digits": "Zadejte všech 6 číslic",
|
||||||
"invalid_code": "Neplatný kód"
|
"invalid_code": "Neplatný kód"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "Nastavení RdClient",
|
||||||
|
"open": "Nastavení",
|
||||||
|
"server_url": "URL panelu",
|
||||||
|
"tls_strict": "Přísné TLS",
|
||||||
|
"sign_out": "Odhlásit",
|
||||||
|
"reset_client": "Resetovat klienta",
|
||||||
|
"discovery_refresh": "Obnovit",
|
||||||
|
"discovery_empty": "V místní síti nebyly nalezeny žádné servery"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -744,7 +744,11 @@
|
|||||||
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
||||||
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} mislykkedes",
|
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} mislykkedes",
|
||||||
"build_error_hint": "Buildfejl",
|
"build_error_hint": "Buildfejl",
|
||||||
"preview_help": "Anmod om hjælp"
|
"preview_help": "Anmod om hjælp",
|
||||||
|
"rdclient_tab": "RdClient desktop",
|
||||||
|
"rdclient_new_bundle": "Ny RdClient-pakke",
|
||||||
|
"rdclient_subtitle": "Byg RdClient-installere med indlejret panel-URL",
|
||||||
|
"rdclient_server_url_hint": "Offentlig URL til BetterDesk-konsollen (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1050,7 +1054,9 @@
|
|||||||
"file_loading": "Indlæser...",
|
"file_loading": "Indlæser...",
|
||||||
"file_timeout": "Fjernenheden reagerede ikke. Filoverførsel understøttes muligvis ikke eller er deaktiveret på fjernmaskinen.",
|
"file_timeout": "Fjernenheden reagerede ikke. Filoverførsel understøttes muligvis ikke eller er deaktiveret på fjernmaskinen.",
|
||||||
"codec": "Video Codec",
|
"codec": "Video Codec",
|
||||||
"codec_auto": "Auto (anbefales)"
|
"codec_auto": "Auto (anbefales)",
|
||||||
|
"remember_peer_password": "Husk enhedsadgangskode på denne computer",
|
||||||
|
"save_peer_password": "Gem adgangskode"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Brand Identitet",
|
"identity_title": "Brand Identitet",
|
||||||
@@ -3834,7 +3840,8 @@
|
|||||||
"no_groups": "Ingen grupper",
|
"no_groups": "Ingen grupper",
|
||||||
"no_tags": "Ingen tags",
|
"no_tags": "Ingen tags",
|
||||||
"collapse_devices": "Fold enhedsliste sammen",
|
"collapse_devices": "Fold enhedsliste sammen",
|
||||||
"expand_devices": "Udvid enhedsliste"
|
"expand_devices": "Udvid enhedsliste",
|
||||||
|
"open_settings": "Indstillinger"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Remote Desktop-klient",
|
"title": "Remote Desktop-klient",
|
||||||
@@ -3854,5 +3861,15 @@
|
|||||||
"network_error": "Netværksfejl",
|
"network_error": "Netværksfejl",
|
||||||
"enter_6_digits": "Indtast alle 6 cifre",
|
"enter_6_digits": "Indtast alle 6 cifre",
|
||||||
"invalid_code": "Ugyldig kode"
|
"invalid_code": "Ugyldig kode"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient-indstillinger",
|
||||||
|
"open": "Indstillinger",
|
||||||
|
"server_url": "Panel-URL",
|
||||||
|
"tls_strict": "Streng TLS",
|
||||||
|
"sign_out": "Log ud",
|
||||||
|
"reset_client": "Nulstil klient",
|
||||||
|
"discovery_refresh": "Opdater",
|
||||||
|
"discovery_empty": "Ingen servere fundet på det lokale netværk"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -738,7 +738,11 @@
|
|||||||
"text_color": "Textfarbe",
|
"text_color": "Textfarbe",
|
||||||
"text_muted_color": "Gedämpfte Textfarbe",
|
"text_muted_color": "Gedämpfte Textfarbe",
|
||||||
"use_https": "HTTPS / WSS verwenden (empfohlen)",
|
"use_https": "HTTPS / WSS verwenden (empfohlen)",
|
||||||
"preview_help": "Hilfe anfordern"
|
"preview_help": "Hilfe anfordern",
|
||||||
|
"rdclient_tab": "RdClient Desktop",
|
||||||
|
"rdclient_new_bundle": "Neues RdClient-Paket",
|
||||||
|
"rdclient_subtitle": "Branded RdClient-Installer mit eingebetteter Panel-URL erstellen",
|
||||||
|
"rdclient_server_url_hint": "Öffentliche URL der BetterDesk-Konsole (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1044,7 +1048,9 @@
|
|||||||
"file_loading": "Wird geladen...",
|
"file_loading": "Wird geladen...",
|
||||||
"file_timeout": "Das entfernte Gerät hat nicht geantwortet. Dateiübertragung wird möglicherweise nicht unterstützt oder ist auf dem entfernten Computer deaktiviert.",
|
"file_timeout": "Das entfernte Gerät hat nicht geantwortet. Dateiübertragung wird möglicherweise nicht unterstützt oder ist auf dem entfernten Computer deaktiviert.",
|
||||||
"codec": "Video-Codec",
|
"codec": "Video-Codec",
|
||||||
"codec_auto": "Automatisch (empfohlen)"
|
"codec_auto": "Automatisch (empfohlen)",
|
||||||
|
"remember_peer_password": "Gerätepasswort auf diesem Gerät merken",
|
||||||
|
"save_peer_password": "Passwort speichern"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Markenidentität",
|
"identity_title": "Markenidentität",
|
||||||
@@ -3828,7 +3834,8 @@
|
|||||||
"no_groups": "Keine Gruppen",
|
"no_groups": "Keine Gruppen",
|
||||||
"no_tags": "Keine Tags",
|
"no_tags": "Keine Tags",
|
||||||
"collapse_devices": "Geräteliste einklappen",
|
"collapse_devices": "Geräteliste einklappen",
|
||||||
"expand_devices": "Geräteliste ausklappen"
|
"expand_devices": "Geräteliste ausklappen",
|
||||||
|
"open_settings": "Einstellungen"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Remote-Desktop-Client",
|
"title": "Remote-Desktop-Client",
|
||||||
@@ -3848,5 +3855,15 @@
|
|||||||
"network_error": "Netzwerkfehler",
|
"network_error": "Netzwerkfehler",
|
||||||
"enter_6_digits": "Alle 6 Ziffern eingeben",
|
"enter_6_digits": "Alle 6 Ziffern eingeben",
|
||||||
"invalid_code": "Ungültiger Code"
|
"invalid_code": "Ungültiger Code"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient-Einstellungen",
|
||||||
|
"open": "Einstellungen",
|
||||||
|
"server_url": "Panel-URL",
|
||||||
|
"tls_strict": "Striktes TLS",
|
||||||
|
"sign_out": "Abmelden",
|
||||||
|
"reset_client": "Client zurücksetzen",
|
||||||
|
"discovery_refresh": "Aktualisieren",
|
||||||
|
"discovery_empty": "Keine Server im lokalen Netzwerk gefunden"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -738,7 +738,11 @@
|
|||||||
"usage_title": "Connection Details",
|
"usage_title": "Connection Details",
|
||||||
"generated": "Configuration generated",
|
"generated": "Configuration generated",
|
||||||
"server_required": "Server address is required",
|
"server_required": "Server address is required",
|
||||||
"nothing_to_copy": "Generate a configuration first"
|
"nothing_to_copy": "Generate a configuration first",
|
||||||
|
"rdclient_tab": "RdClient desktop",
|
||||||
|
"rdclient_new_bundle": "New RdClient bundle",
|
||||||
|
"rdclient_subtitle": "Build branded RdClient desktop installers with embedded panel URL",
|
||||||
|
"rdclient_server_url_hint": "Public URL where operators reach the BetterDesk console (/remote dashboard)."
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"title": "Download BetterDesk Agent",
|
"title": "Download BetterDesk Agent",
|
||||||
@@ -1054,7 +1058,9 @@
|
|||||||
"2fa_required": "2FA Verification Required",
|
"2fa_required": "2FA Verification Required",
|
||||||
"2fa_hint": "Enter the 6-digit code from your authenticator app",
|
"2fa_hint": "Enter the 6-digit code from your authenticator app",
|
||||||
"2fa_invalid": "Enter a valid 6-digit code",
|
"2fa_invalid": "Enter a valid 6-digit code",
|
||||||
"verify": "Verify"
|
"verify": "Verify",
|
||||||
|
"remember_peer_password": "Remember device password on this device",
|
||||||
|
"save_peer_password": "Save password"
|
||||||
},
|
},
|
||||||
"remote_dashboard": {
|
"remote_dashboard": {
|
||||||
"title": "Remote Desktop Client",
|
"title": "Remote Desktop Client",
|
||||||
@@ -1091,7 +1097,8 @@
|
|||||||
"no_groups": "No groups",
|
"no_groups": "No groups",
|
||||||
"no_tags": "No tags",
|
"no_tags": "No tags",
|
||||||
"collapse_devices": "Collapse device list",
|
"collapse_devices": "Collapse device list",
|
||||||
"expand_devices": "Expand device list"
|
"expand_devices": "Expand device list",
|
||||||
|
"open_settings": "Settings"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Brand Identity",
|
"identity_title": "Brand Identity",
|
||||||
@@ -3848,5 +3855,15 @@
|
|||||||
"network_error": "Network error",
|
"network_error": "Network error",
|
||||||
"enter_6_digits": "Enter all 6 digits",
|
"enter_6_digits": "Enter all 6 digits",
|
||||||
"invalid_code": "Invalid code"
|
"invalid_code": "Invalid code"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient Settings",
|
||||||
|
"open": "Settings",
|
||||||
|
"server_url": "Panel URL",
|
||||||
|
"tls_strict": "Strict TLS",
|
||||||
|
"sign_out": "Sign out",
|
||||||
|
"reset_client": "Reset client",
|
||||||
|
"discovery_refresh": "Refresh",
|
||||||
|
"discovery_empty": "No servers found on the local network"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -738,7 +738,11 @@
|
|||||||
"text_color": "Color del texto",
|
"text_color": "Color del texto",
|
||||||
"text_muted_color": "Color de texto atenuado",
|
"text_muted_color": "Color de texto atenuado",
|
||||||
"use_https": "Usar HTTPS / WSS (recomendado)",
|
"use_https": "Usar HTTPS / WSS (recomendado)",
|
||||||
"preview_help": "Solicitar ayuda"
|
"preview_help": "Solicitar ayuda",
|
||||||
|
"rdclient_tab": "RdClient escritorio",
|
||||||
|
"rdclient_new_bundle": "Nuevo paquete RdClient",
|
||||||
|
"rdclient_subtitle": "Generar instaladores RdClient con URL del panel integrada",
|
||||||
|
"rdclient_server_url_hint": "URL pública de la consola BetterDesk (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Puerto",
|
"port": "Puerto",
|
||||||
@@ -1044,7 +1048,9 @@
|
|||||||
"file_loading": "Cargando...",
|
"file_loading": "Cargando...",
|
||||||
"file_timeout": "El dispositivo remoto no respondió. Es posible que la transferencia de archivos no sea compatible o esté deshabilitada en el equipo remoto.",
|
"file_timeout": "El dispositivo remoto no respondió. Es posible que la transferencia de archivos no sea compatible o esté deshabilitada en el equipo remoto.",
|
||||||
"codec": "Códec de vídeo",
|
"codec": "Códec de vídeo",
|
||||||
"codec_auto": "Automático (recomendado)"
|
"codec_auto": "Automático (recomendado)",
|
||||||
|
"remember_peer_password": "Recordar contraseña del dispositivo en este equipo",
|
||||||
|
"save_peer_password": "Guardar contraseña"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Identidad de marca",
|
"identity_title": "Identidad de marca",
|
||||||
@@ -3828,7 +3834,8 @@
|
|||||||
"no_groups": "Sin grupos",
|
"no_groups": "Sin grupos",
|
||||||
"no_tags": "Sin etiquetas",
|
"no_tags": "Sin etiquetas",
|
||||||
"collapse_devices": "Contraer lista de dispositivos",
|
"collapse_devices": "Contraer lista de dispositivos",
|
||||||
"expand_devices": "Expandir lista de dispositivos"
|
"expand_devices": "Expandir lista de dispositivos",
|
||||||
|
"open_settings": "Ajustes"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Cliente de escritorio remoto",
|
"title": "Cliente de escritorio remoto",
|
||||||
@@ -3848,5 +3855,15 @@
|
|||||||
"network_error": "Error de red",
|
"network_error": "Error de red",
|
||||||
"enter_6_digits": "Introduzca los 6 dígitos",
|
"enter_6_digits": "Introduzca los 6 dígitos",
|
||||||
"invalid_code": "Código inválido"
|
"invalid_code": "Código inválido"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "Ajustes de RdClient",
|
||||||
|
"open": "Ajustes",
|
||||||
|
"server_url": "URL del panel",
|
||||||
|
"tls_strict": "TLS estricto",
|
||||||
|
"sign_out": "Cerrar sesión",
|
||||||
|
"reset_client": "Restablecer cliente",
|
||||||
|
"discovery_refresh": "Actualizar",
|
||||||
|
"discovery_empty": "No se encontraron servidores en la red local"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -744,7 +744,11 @@
|
|||||||
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
||||||
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} epäonnistui",
|
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} epäonnistui",
|
||||||
"build_error_hint": "Koonnosvirhe",
|
"build_error_hint": "Koonnosvirhe",
|
||||||
"preview_help": "Pyydä apua"
|
"preview_help": "Pyydä apua",
|
||||||
|
"rdclient_tab": "RdClient-työpöytä",
|
||||||
|
"rdclient_new_bundle": "Uusi RdClient-paketti",
|
||||||
|
"rdclient_subtitle": "Rakenna RdClient-asentajat upotetulla paneelin URL:llä",
|
||||||
|
"rdclient_server_url_hint": "BetterDesk-konsolin julkinen URL (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1050,7 +1054,9 @@
|
|||||||
"file_loading": "Ladataan...",
|
"file_loading": "Ladataan...",
|
||||||
"file_timeout": "Etälaite ei vastannut. Tiedostonsiirtoa ei ehkä tueta tai se on poistettu käytöstä etäkoneessa.",
|
"file_timeout": "Etälaite ei vastannut. Tiedostonsiirtoa ei ehkä tueta tai se on poistettu käytöstä etäkoneessa.",
|
||||||
"codec": "Video Codec",
|
"codec": "Video Codec",
|
||||||
"codec_auto": "Automaattinen (suositus)"
|
"codec_auto": "Automaattinen (suositus)",
|
||||||
|
"remember_peer_password": "Muista laitteen salasana tällä tietokoneella",
|
||||||
|
"save_peer_password": "Tallenna salasana"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Brändin identiteetti",
|
"identity_title": "Brändin identiteetti",
|
||||||
@@ -3834,7 +3840,8 @@
|
|||||||
"no_groups": "Ei ryhmiä",
|
"no_groups": "Ei ryhmiä",
|
||||||
"no_tags": "Ei tageja",
|
"no_tags": "Ei tageja",
|
||||||
"collapse_devices": "Tiivistä laiteluettelo",
|
"collapse_devices": "Tiivistä laiteluettelo",
|
||||||
"expand_devices": "Laajenna laiteluettelo"
|
"expand_devices": "Laajenna laiteluettelo",
|
||||||
|
"open_settings": "Asetukset"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Etätyöpöytäasiakas",
|
"title": "Etätyöpöytäasiakas",
|
||||||
@@ -3854,5 +3861,15 @@
|
|||||||
"network_error": "Verkkovirhe",
|
"network_error": "Verkkovirhe",
|
||||||
"enter_6_digits": "Syötä kaikki 6 numeroa",
|
"enter_6_digits": "Syötä kaikki 6 numeroa",
|
||||||
"invalid_code": "Virheellinen koodi"
|
"invalid_code": "Virheellinen koodi"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient-asetukset",
|
||||||
|
"open": "Asetukset",
|
||||||
|
"server_url": "Paneelin URL",
|
||||||
|
"tls_strict": "Tiukka TLS",
|
||||||
|
"sign_out": "Kirjaudu ulos",
|
||||||
|
"reset_client": "Nollaa asiakas",
|
||||||
|
"discovery_refresh": "Päivitä",
|
||||||
|
"discovery_empty": "Paikallisesta verkosta ei löytynyt palvelimia"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -742,7 +742,11 @@
|
|||||||
"usage_title": "Détails de connexion",
|
"usage_title": "Détails de connexion",
|
||||||
"generated": "Configuration générée",
|
"generated": "Configuration générée",
|
||||||
"server_required": "L'adresse du serveur est requise",
|
"server_required": "L'adresse du serveur est requise",
|
||||||
"nothing_to_copy": "Générez d'abord une configuration"
|
"nothing_to_copy": "Générez d'abord une configuration",
|
||||||
|
"rdclient_tab": "RdClient bureau",
|
||||||
|
"rdclient_new_bundle": "Nouveau bundle RdClient",
|
||||||
|
"rdclient_subtitle": "Créer des installateurs RdClient avec URL du panel intégrée",
|
||||||
|
"rdclient_server_url_hint": "URL publique de la console BetterDesk (/remote)."
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"title": "BetterDesk Agent herunterladen",
|
"title": "BetterDesk Agent herunterladen",
|
||||||
@@ -1058,7 +1062,9 @@
|
|||||||
"2fa_required": "2FA Verification Required",
|
"2fa_required": "2FA Verification Required",
|
||||||
"2fa_hint": "Enter the 6-digit code from your authenticator app",
|
"2fa_hint": "Enter the 6-digit code from your authenticator app",
|
||||||
"2fa_invalid": "Enter a valid 6-digit code",
|
"2fa_invalid": "Enter a valid 6-digit code",
|
||||||
"verify": "Verify"
|
"verify": "Verify",
|
||||||
|
"remember_peer_password": "Mémoriser le mot de passe de l’appareil sur cet ordinateur",
|
||||||
|
"save_peer_password": "Enregistrer le mot de passe"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Identité de marque",
|
"identity_title": "Identité de marque",
|
||||||
@@ -3832,7 +3838,8 @@
|
|||||||
"no_groups": "Aucun groupe",
|
"no_groups": "Aucun groupe",
|
||||||
"no_tags": "Aucun tag",
|
"no_tags": "Aucun tag",
|
||||||
"collapse_devices": "Réduire la liste des appareils",
|
"collapse_devices": "Réduire la liste des appareils",
|
||||||
"expand_devices": "Développer la liste des appareils"
|
"expand_devices": "Développer la liste des appareils",
|
||||||
|
"open_settings": "Paramètres"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Client Bureau à distance",
|
"title": "Client Bureau à distance",
|
||||||
@@ -3852,5 +3859,15 @@
|
|||||||
"network_error": "Erreur réseau",
|
"network_error": "Erreur réseau",
|
||||||
"enter_6_digits": "Entrez les 6 chiffres",
|
"enter_6_digits": "Entrez les 6 chiffres",
|
||||||
"invalid_code": "Code invalide"
|
"invalid_code": "Code invalide"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "Paramètres RdClient",
|
||||||
|
"open": "Paramètres",
|
||||||
|
"server_url": "URL du panel",
|
||||||
|
"tls_strict": "TLS strict",
|
||||||
|
"sign_out": "Se déconnecter",
|
||||||
|
"reset_client": "Réinitialiser le client",
|
||||||
|
"discovery_refresh": "Actualiser",
|
||||||
|
"discovery_empty": "Aucun serveur trouvé sur le réseau local"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -744,7 +744,11 @@
|
|||||||
"build_status_failed": "विफल",
|
"build_status_failed": "विफल",
|
||||||
"builds_summary": "{{ready}} तैयार · {{pending}} कतार में · {{building}} बिल्ड हो रहा है · {{failed}} विफल",
|
"builds_summary": "{{ready}} तैयार · {{pending}} कतार में · {{building}} बिल्ड हो रहा है · {{failed}} विफल",
|
||||||
"build_error_hint": "बिल्ड त्रुटि",
|
"build_error_hint": "बिल्ड त्रुटि",
|
||||||
"preview_help": "सहायता का अनुरोध"
|
"preview_help": "सहायता का अनुरोध",
|
||||||
|
"rdclient_tab": "RdClient डेस्कटॉप",
|
||||||
|
"rdclient_new_bundle": "नया RdClient बंडल",
|
||||||
|
"rdclient_subtitle": "एम्बेडेड पैनल URL के साथ RdClient इंस्टॉलर बनाएं",
|
||||||
|
"rdclient_server_url_hint": "BetterDesk कंसोल का सार्वजनिक URL (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1050,7 +1054,9 @@
|
|||||||
"file_loading": "लोड हो रहा है...",
|
"file_loading": "लोड हो रहा है...",
|
||||||
"file_timeout": "रिमोट डिवाइस ने प्रतिक्रिया नहीं दी. फ़ाइल स्थानांतरण समर्थित नहीं हो सकता है या रिमोट मशीन पर अक्षम है।",
|
"file_timeout": "रिमोट डिवाइस ने प्रतिक्रिया नहीं दी. फ़ाइल स्थानांतरण समर्थित नहीं हो सकता है या रिमोट मशीन पर अक्षम है।",
|
||||||
"codec": "वीडियो कोडेक",
|
"codec": "वीडियो कोडेक",
|
||||||
"codec_auto": "ऑटो (अनुशंसित)"
|
"codec_auto": "ऑटो (अनुशंसित)",
|
||||||
|
"remember_peer_password": "इस डिवाइस पर डिवाइस पासवर्ड याद रखें",
|
||||||
|
"save_peer_password": "पासवर्ड सहेजें"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "ब्रांड पहचान",
|
"identity_title": "ब्रांड पहचान",
|
||||||
@@ -3834,7 +3840,8 @@
|
|||||||
"no_groups": "कोई समूह नहीं",
|
"no_groups": "कोई समूह नहीं",
|
||||||
"no_tags": "कोई टैग नहीं",
|
"no_tags": "कोई टैग नहीं",
|
||||||
"collapse_devices": "डिवाइस सूची संक्षिप्त करें",
|
"collapse_devices": "डिवाइस सूची संक्षिप्त करें",
|
||||||
"expand_devices": "डिवाइस सूची विस्तृत करें"
|
"expand_devices": "डिवाइस सूची विस्तृत करें",
|
||||||
|
"open_settings": "सेटिंग्स"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "रिमोट डेस्कटॉप क्लाइंट",
|
"title": "रिमोट डेस्कटॉप क्लाइंट",
|
||||||
@@ -3854,5 +3861,15 @@
|
|||||||
"network_error": "नेटवर्क त्रुटि",
|
"network_error": "नेटवर्क त्रुटि",
|
||||||
"enter_6_digits": "सभी 6 अंक दर्ज करें",
|
"enter_6_digits": "सभी 6 अंक दर्ज करें",
|
||||||
"invalid_code": "अमान्य कोड"
|
"invalid_code": "अमान्य कोड"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient सेटिंग्स",
|
||||||
|
"open": "सेटिंग्स",
|
||||||
|
"server_url": "पैनल URL",
|
||||||
|
"tls_strict": "सख्त TLS",
|
||||||
|
"sign_out": "साइन आउट",
|
||||||
|
"reset_client": "क्लाइंट रीसेट करें",
|
||||||
|
"discovery_refresh": "रीफ़्रेश",
|
||||||
|
"discovery_empty": "स्थानीय नेटवर्क पर कोई सर्वर नहीं मिला"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -744,7 +744,11 @@
|
|||||||
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
||||||
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} sikertelen",
|
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} sikertelen",
|
||||||
"build_error_hint": "Build hiba",
|
"build_error_hint": "Build hiba",
|
||||||
"preview_help": "Segítség kérése"
|
"preview_help": "Segítség kérése",
|
||||||
|
"rdclient_tab": "RdClient asztali",
|
||||||
|
"rdclient_new_bundle": "Új RdClient csomag",
|
||||||
|
"rdclient_subtitle": "RdClient telepítők készítése beágyazott panel URL-lel",
|
||||||
|
"rdclient_server_url_hint": "A BetterDesk konzol nyilvános URL-je (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1050,7 +1054,9 @@
|
|||||||
"file_loading": "Betöltés...",
|
"file_loading": "Betöltés...",
|
||||||
"file_timeout": "A távoli eszköz nem válaszolt. Előfordulhat, hogy a fájlátvitel nem támogatott vagy le van tiltva a távoli gépen.",
|
"file_timeout": "A távoli eszköz nem válaszolt. Előfordulhat, hogy a fájlátvitel nem támogatott vagy le van tiltva a távoli gépen.",
|
||||||
"codec": "Video Codec",
|
"codec": "Video Codec",
|
||||||
"codec_auto": "Automatikus (ajánlott)"
|
"codec_auto": "Automatikus (ajánlott)",
|
||||||
|
"remember_peer_password": "Eszköz jelszó megjegyzése ezen a gépen",
|
||||||
|
"save_peer_password": "Jelszó mentése"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Márkaidentitás",
|
"identity_title": "Márkaidentitás",
|
||||||
@@ -3834,7 +3840,8 @@
|
|||||||
"no_groups": "Nincs csoport",
|
"no_groups": "Nincs csoport",
|
||||||
"no_tags": "Nincs címke",
|
"no_tags": "Nincs címke",
|
||||||
"collapse_devices": "Eszközlista összecsukása",
|
"collapse_devices": "Eszközlista összecsukása",
|
||||||
"expand_devices": "Eszközlista kibontása"
|
"expand_devices": "Eszközlista kibontása",
|
||||||
|
"open_settings": "Beállítások"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Távoli asztal kliens",
|
"title": "Távoli asztal kliens",
|
||||||
@@ -3854,5 +3861,15 @@
|
|||||||
"network_error": "Hálózati hiba",
|
"network_error": "Hálózati hiba",
|
||||||
"enter_6_digits": "Adja meg mind a 6 számjegyet",
|
"enter_6_digits": "Adja meg mind a 6 számjegyet",
|
||||||
"invalid_code": "Érvénytelen kód"
|
"invalid_code": "Érvénytelen kód"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient beállítások",
|
||||||
|
"open": "Beállítások",
|
||||||
|
"server_url": "Panel URL",
|
||||||
|
"tls_strict": "Szigorú TLS",
|
||||||
|
"sign_out": "Kijelentkezés",
|
||||||
|
"reset_client": "Kliens visszaállítása",
|
||||||
|
"discovery_refresh": "Frissítés",
|
||||||
|
"discovery_empty": "Nem található szerver a helyi hálózaton"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -744,7 +744,11 @@
|
|||||||
"build_status_failed": "Gagal",
|
"build_status_failed": "Gagal",
|
||||||
"builds_summary": "{{ready}} siap · {{pending}} antrian · {{building}} membangun · {{failed}} gagal",
|
"builds_summary": "{{ready}} siap · {{pending}} antrian · {{building}} membangun · {{failed}} gagal",
|
||||||
"build_error_hint": "Kesalahan build",
|
"build_error_hint": "Kesalahan build",
|
||||||
"preview_help": "Minta bantuan"
|
"preview_help": "Minta bantuan",
|
||||||
|
"rdclient_tab": "RdClient desktop",
|
||||||
|
"rdclient_new_bundle": "Paket RdClient baru",
|
||||||
|
"rdclient_subtitle": "Buat installer RdClient dengan URL panel tertanam",
|
||||||
|
"rdclient_server_url_hint": "URL publik konsol BetterDesk (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1050,7 +1054,9 @@
|
|||||||
"file_loading": "Memuat...",
|
"file_loading": "Memuat...",
|
||||||
"file_timeout": "Perangkat jarak jauh tidak merespons. Transfer file mungkin tidak didukung atau dinonaktifkan pada mesin jarak jauh.",
|
"file_timeout": "Perangkat jarak jauh tidak merespons. Transfer file mungkin tidak didukung atau dinonaktifkan pada mesin jarak jauh.",
|
||||||
"codec": "Kodek Video",
|
"codec": "Kodek Video",
|
||||||
"codec_auto": "Otomatis (disarankan)"
|
"codec_auto": "Otomatis (disarankan)",
|
||||||
|
"remember_peer_password": "Ingat kata sandi perangkat di komputer ini",
|
||||||
|
"save_peer_password": "Simpan kata sandi"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Identitas Merek",
|
"identity_title": "Identitas Merek",
|
||||||
@@ -3834,7 +3840,8 @@
|
|||||||
"no_groups": "Tidak ada grup",
|
"no_groups": "Tidak ada grup",
|
||||||
"no_tags": "Tidak ada tag",
|
"no_tags": "Tidak ada tag",
|
||||||
"collapse_devices": "Ciutkan daftar perangkat",
|
"collapse_devices": "Ciutkan daftar perangkat",
|
||||||
"expand_devices": "Perluas daftar perangkat"
|
"expand_devices": "Perluas daftar perangkat",
|
||||||
|
"open_settings": "Pengaturan"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Klien Desktop Jarak Jauh",
|
"title": "Klien Desktop Jarak Jauh",
|
||||||
@@ -3854,5 +3861,15 @@
|
|||||||
"network_error": "Kesalahan jaringan",
|
"network_error": "Kesalahan jaringan",
|
||||||
"enter_6_digits": "Masukkan semua 6 digit",
|
"enter_6_digits": "Masukkan semua 6 digit",
|
||||||
"invalid_code": "Kode tidak valid"
|
"invalid_code": "Kode tidak valid"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "Pengaturan RdClient",
|
||||||
|
"open": "Pengaturan",
|
||||||
|
"server_url": "URL panel",
|
||||||
|
"tls_strict": "TLS ketat",
|
||||||
|
"sign_out": "Keluar",
|
||||||
|
"reset_client": "Reset klien",
|
||||||
|
"discovery_refresh": "Segarkan",
|
||||||
|
"discovery_empty": "Tidak ada server di jaringan lokal"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -738,7 +738,11 @@
|
|||||||
"build_status_failed": "Èchouè",
|
"build_status_failed": "Èchouè",
|
||||||
"builds_summary": "{{ready}} prèt · {{pending}} en file d'attesa · {{building}} en compilation · {{failed}} èchouè",
|
"builds_summary": "{{ready}} prèt · {{pending}} en file d'attesa · {{building}} en compilation · {{failed}} èchouè",
|
||||||
"build_error_hint": "Erreur de compilation",
|
"build_error_hint": "Erreur de compilation",
|
||||||
"preview_help": "Richiedi aiuto"
|
"preview_help": "Richiedi aiuto",
|
||||||
|
"rdclient_tab": "RdClient desktop",
|
||||||
|
"rdclient_new_bundle": "Nuovo bundle RdClient",
|
||||||
|
"rdclient_subtitle": "Crea installer RdClient con URL pannello incorporato",
|
||||||
|
"rdclient_server_url_hint": "URL pubblica della console BetterDesk (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Porta",
|
"port": "Porta",
|
||||||
@@ -1044,7 +1048,9 @@
|
|||||||
"file_loading": "Caricamento...",
|
"file_loading": "Caricamento...",
|
||||||
"file_timeout": "Il dispositivo remoto non ha risposto in tempo. Il trasferimento file potrebbe non essere supportato o potrebbe essere disabilitato.",
|
"file_timeout": "Il dispositivo remoto non ha risposto in tempo. Il trasferimento file potrebbe non essere supportato o potrebbe essere disabilitato.",
|
||||||
"codec": "Codec video",
|
"codec": "Codec video",
|
||||||
"codec_auto": "Automatico (consigliato)"
|
"codec_auto": "Automatico (consigliato)",
|
||||||
|
"remember_peer_password": "Ricorda password dispositivo su questo computer",
|
||||||
|
"save_peer_password": "Salva password"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Identità del marchio",
|
"identity_title": "Identità del marchio",
|
||||||
@@ -3828,7 +3834,8 @@
|
|||||||
"no_groups": "Nessun gruppo",
|
"no_groups": "Nessun gruppo",
|
||||||
"no_tags": "Nessun tag",
|
"no_tags": "Nessun tag",
|
||||||
"collapse_devices": "Comprimi elenco dispositivi",
|
"collapse_devices": "Comprimi elenco dispositivi",
|
||||||
"expand_devices": "Espandi elenco dispositivi"
|
"expand_devices": "Espandi elenco dispositivi",
|
||||||
|
"open_settings": "Impostazioni"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Client desktop remoto",
|
"title": "Client desktop remoto",
|
||||||
@@ -3848,5 +3855,15 @@
|
|||||||
"network_error": "Errore di rete",
|
"network_error": "Errore di rete",
|
||||||
"enter_6_digits": "Inserisci tutte le 6 cifre",
|
"enter_6_digits": "Inserisci tutte le 6 cifre",
|
||||||
"invalid_code": "Codice non valido"
|
"invalid_code": "Codice non valido"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "Impostazioni RdClient",
|
||||||
|
"open": "Impostazioni",
|
||||||
|
"server_url": "URL pannello",
|
||||||
|
"tls_strict": "TLS rigoroso",
|
||||||
|
"sign_out": "Esci",
|
||||||
|
"reset_client": "Reimposta client",
|
||||||
|
"discovery_refresh": "Aggiorna",
|
||||||
|
"discovery_empty": "Nessun server trovato nella rete locale"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -738,7 +738,11 @@
|
|||||||
"build_status_failed": "失敗",
|
"build_status_failed": "失敗",
|
||||||
"builds_summary": "{{ready}} 準備完了 · {{pending}} キュー中 · {{building}} ビルド中 · {{failed}} 失敗",
|
"builds_summary": "{{ready}} 準備完了 · {{pending}} キュー中 · {{building}} ビルド中 · {{failed}} 失敗",
|
||||||
"build_error_hint": "ビルドエラー",
|
"build_error_hint": "ビルドエラー",
|
||||||
"preview_help": "ヘルプを依頼"
|
"preview_help": "ヘルプを依頼",
|
||||||
|
"rdclient_tab": "RdClient デスクトップ",
|
||||||
|
"rdclient_new_bundle": "新規 RdClient バンドル",
|
||||||
|
"rdclient_subtitle": "パネル URL を埋め込んだ RdClient インストーラーをビルド",
|
||||||
|
"rdclient_server_url_hint": "BetterDesk コンソールの公開 URL (/remote)。"
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1044,7 +1048,9 @@
|
|||||||
"file_loading": "読み込み中...",
|
"file_loading": "読み込み中...",
|
||||||
"file_timeout": "リモートデバイスが応答しませんでした。ファイル転送がサポートされていないか、リモート マシンで無効になっている可能性があります。",
|
"file_timeout": "リモートデバイスが応答しませんでした。ファイル転送がサポートされていないか、リモート マシンで無効になっている可能性があります。",
|
||||||
"codec": "ビデオコーデック",
|
"codec": "ビデオコーデック",
|
||||||
"codec_auto": "自動 (推奨)"
|
"codec_auto": "自動 (推奨)",
|
||||||
|
"remember_peer_password": "このデバイスでデバイスパスワードを記憶",
|
||||||
|
"save_peer_password": "パスワードを保存"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "ブランドアイデンティティ",
|
"identity_title": "ブランドアイデンティティ",
|
||||||
@@ -3828,7 +3834,8 @@
|
|||||||
"no_groups": "グループなし",
|
"no_groups": "グループなし",
|
||||||
"no_tags": "タグなし",
|
"no_tags": "タグなし",
|
||||||
"collapse_devices": "デバイス一覧を折りたたむ",
|
"collapse_devices": "デバイス一覧を折りたたむ",
|
||||||
"expand_devices": "デバイス一覧を展開"
|
"expand_devices": "デバイス一覧を展開",
|
||||||
|
"open_settings": "設定"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "リモートデスクトップクライアント",
|
"title": "リモートデスクトップクライアント",
|
||||||
@@ -3848,5 +3855,15 @@
|
|||||||
"network_error": "ネットワークエラー",
|
"network_error": "ネットワークエラー",
|
||||||
"enter_6_digits": "6桁すべてを入力してください",
|
"enter_6_digits": "6桁すべてを入力してください",
|
||||||
"invalid_code": "無効なコード"
|
"invalid_code": "無効なコード"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient 設定",
|
||||||
|
"open": "設定",
|
||||||
|
"server_url": "パネル URL",
|
||||||
|
"tls_strict": "厳格 TLS",
|
||||||
|
"sign_out": "サインアウト",
|
||||||
|
"reset_client": "クライアントをリセット",
|
||||||
|
"discovery_refresh": "更新",
|
||||||
|
"discovery_empty": "ローカルネットワークにサーバーが見つかりません"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -738,7 +738,11 @@
|
|||||||
"build_status_failed": "실패",
|
"build_status_failed": "실패",
|
||||||
"builds_summary": "{{ready}} 준비 완료 · {{pending}} 대기 중 · {{building}} 빌드 중 · {{failed}} 실패",
|
"builds_summary": "{{ready}} 준비 완료 · {{pending}} 대기 중 · {{building}} 빌드 중 · {{failed}} 실패",
|
||||||
"build_error_hint": "빌드 오류",
|
"build_error_hint": "빌드 오류",
|
||||||
"preview_help": "도움 요청"
|
"preview_help": "도움 요청",
|
||||||
|
"rdclient_tab": "RdClient 데스크톱",
|
||||||
|
"rdclient_new_bundle": "새 RdClient 번들",
|
||||||
|
"rdclient_subtitle": "패널 URL이 포함된 RdClient 설치 프로그램 빌드",
|
||||||
|
"rdclient_server_url_hint": "BetterDesk 콘솔 공개 URL (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1044,7 +1048,9 @@
|
|||||||
"file_loading": "로드 중...",
|
"file_loading": "로드 중...",
|
||||||
"file_timeout": "원격 장치가 응답하지 않았습니다. 원격 시스템에서 파일 전송이 지원되지 않거나 비활성화되어 있을 수 있습니다.",
|
"file_timeout": "원격 장치가 응답하지 않았습니다. 원격 시스템에서 파일 전송이 지원되지 않거나 비활성화되어 있을 수 있습니다.",
|
||||||
"codec": "비디오 코덱",
|
"codec": "비디오 코덱",
|
||||||
"codec_auto": "자동(권장)"
|
"codec_auto": "자동(권장)",
|
||||||
|
"remember_peer_password": "이 기기에서 장치 비밀번호 기억",
|
||||||
|
"save_peer_password": "비밀번호 저장"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "브랜드 아이덴티티",
|
"identity_title": "브랜드 아이덴티티",
|
||||||
@@ -3828,7 +3834,8 @@
|
|||||||
"no_groups": "그룹 없음",
|
"no_groups": "그룹 없음",
|
||||||
"no_tags": "태그 없음",
|
"no_tags": "태그 없음",
|
||||||
"collapse_devices": "장치 목록 접기",
|
"collapse_devices": "장치 목록 접기",
|
||||||
"expand_devices": "장치 목록 펼치기"
|
"expand_devices": "장치 목록 펼치기",
|
||||||
|
"open_settings": "설정"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "원격 데스크톱 클라이언트",
|
"title": "원격 데스크톱 클라이언트",
|
||||||
@@ -3848,5 +3855,15 @@
|
|||||||
"network_error": "네트워크 오류",
|
"network_error": "네트워크 오류",
|
||||||
"enter_6_digits": "6자리 모두 입력하세요",
|
"enter_6_digits": "6자리 모두 입력하세요",
|
||||||
"invalid_code": "잘못된 코드"
|
"invalid_code": "잘못된 코드"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient 설정",
|
||||||
|
"open": "설정",
|
||||||
|
"server_url": "패널 URL",
|
||||||
|
"tls_strict": "엄격 TLS",
|
||||||
|
"sign_out": "로그아웃",
|
||||||
|
"reset_client": "클라이언트 재설정",
|
||||||
|
"discovery_refresh": "새로고침",
|
||||||
|
"discovery_empty": "로컬 네트워크에서 서버를 찾을 수 없습니다"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -744,7 +744,11 @@
|
|||||||
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
||||||
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} mislyktes",
|
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} mislyktes",
|
||||||
"build_error_hint": "Byggfeil",
|
"build_error_hint": "Byggfeil",
|
||||||
"preview_help": "Be om hjelp"
|
"preview_help": "Be om hjelp",
|
||||||
|
"rdclient_tab": "RdClient desktop",
|
||||||
|
"rdclient_new_bundle": "Nytt RdClient-pakke",
|
||||||
|
"rdclient_subtitle": "Bygg RdClient-installere med innebygd panel-URL",
|
||||||
|
"rdclient_server_url_hint": "Offentlig URL til BetterDesk-konsollen (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1050,7 +1054,9 @@
|
|||||||
"file_loading": "Laster inn...",
|
"file_loading": "Laster inn...",
|
||||||
"file_timeout": "Ekstern enhet svarte ikke. Filoverføring støttes kanskje ikke eller er deaktivert på den eksterne maskinen.",
|
"file_timeout": "Ekstern enhet svarte ikke. Filoverføring støttes kanskje ikke eller er deaktivert på den eksterne maskinen.",
|
||||||
"codec": "Videokodek",
|
"codec": "Videokodek",
|
||||||
"codec_auto": "Auto (anbefalt)"
|
"codec_auto": "Auto (anbefalt)",
|
||||||
|
"remember_peer_password": "Husk enhetspassord på denne datamaskinen",
|
||||||
|
"save_peer_password": "Lagre passord"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Merkeidentitet",
|
"identity_title": "Merkeidentitet",
|
||||||
@@ -3834,7 +3840,8 @@
|
|||||||
"no_groups": "Ingen grupper",
|
"no_groups": "Ingen grupper",
|
||||||
"no_tags": "Ingen tagger",
|
"no_tags": "Ingen tagger",
|
||||||
"collapse_devices": "Skjul enhetsliste",
|
"collapse_devices": "Skjul enhetsliste",
|
||||||
"expand_devices": "Vis enhetsliste"
|
"expand_devices": "Vis enhetsliste",
|
||||||
|
"open_settings": "Innstillinger"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Remote Desktop-klient",
|
"title": "Remote Desktop-klient",
|
||||||
@@ -3854,5 +3861,15 @@
|
|||||||
"network_error": "Nettverksfeil",
|
"network_error": "Nettverksfeil",
|
||||||
"enter_6_digits": "Skriv inn alle 6 sifre",
|
"enter_6_digits": "Skriv inn alle 6 sifre",
|
||||||
"invalid_code": "Ugyldig kode"
|
"invalid_code": "Ugyldig kode"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient-innstillinger",
|
||||||
|
"open": "Innstillinger",
|
||||||
|
"server_url": "Panel-URL",
|
||||||
|
"tls_strict": "Streng TLS",
|
||||||
|
"sign_out": "Logg ut",
|
||||||
|
"reset_client": "Tilbakestill klient",
|
||||||
|
"discovery_refresh": "Oppdater",
|
||||||
|
"discovery_empty": "Ingen servere funnet på det lokale nettverket"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -738,7 +738,11 @@
|
|||||||
"build_status_failed": "Mislukt",
|
"build_status_failed": "Mislukt",
|
||||||
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} mislukt",
|
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} mislukt",
|
||||||
"build_error_hint": "Build-Fout",
|
"build_error_hint": "Build-Fout",
|
||||||
"preview_help": "Hulp vragen"
|
"preview_help": "Hulp vragen",
|
||||||
|
"rdclient_tab": "RdClient desktop",
|
||||||
|
"rdclient_new_bundle": "Nieuw RdClient-pakket",
|
||||||
|
"rdclient_subtitle": "RdClient-installers bouwen met ingebedde paneel-URL",
|
||||||
|
"rdclient_server_url_hint": "Publieke URL van het BetterDesk-paneel (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Poort",
|
"port": "Poort",
|
||||||
@@ -1044,7 +1048,9 @@
|
|||||||
"file_loading": "Laden...",
|
"file_loading": "Laden...",
|
||||||
"file_timeout": "Het externe apparaat reageerde niet. Bestandsoverdracht wordt mogelijk niet ondersteund of is uitgeschakeld op de externe computer.",
|
"file_timeout": "Het externe apparaat reageerde niet. Bestandsoverdracht wordt mogelijk niet ondersteund of is uitgeschakeld op de externe computer.",
|
||||||
"codec": "Videocodec",
|
"codec": "Videocodec",
|
||||||
"codec_auto": "Automatisch (aanbevolen)"
|
"codec_auto": "Automatisch (aanbevolen)",
|
||||||
|
"remember_peer_password": "Apparaatwachtwoord onthouden op dit apparaat",
|
||||||
|
"save_peer_password": "Wachtwoord opslaan"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Merkidentiteit",
|
"identity_title": "Merkidentiteit",
|
||||||
@@ -3828,7 +3834,8 @@
|
|||||||
"no_groups": "Geen groepen",
|
"no_groups": "Geen groepen",
|
||||||
"no_tags": "Geen tags",
|
"no_tags": "Geen tags",
|
||||||
"collapse_devices": "Apparaatlijst inklappen",
|
"collapse_devices": "Apparaatlijst inklappen",
|
||||||
"expand_devices": "Apparaatlijst uitklappen"
|
"expand_devices": "Apparaatlijst uitklappen",
|
||||||
|
"open_settings": "Instellingen"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Remote Desktop-client",
|
"title": "Remote Desktop-client",
|
||||||
@@ -3848,5 +3855,15 @@
|
|||||||
"network_error": "Netwerkfout",
|
"network_error": "Netwerkfout",
|
||||||
"enter_6_digits": "Voer alle 6 cijfers in",
|
"enter_6_digits": "Voer alle 6 cijfers in",
|
||||||
"invalid_code": "Ongeldige code"
|
"invalid_code": "Ongeldige code"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient-instellingen",
|
||||||
|
"open": "Instellingen",
|
||||||
|
"server_url": "Paneel-URL",
|
||||||
|
"tls_strict": "Strikte TLS",
|
||||||
|
"sign_out": "Afmelden",
|
||||||
|
"reset_client": "Client resetten",
|
||||||
|
"discovery_refresh": "Vernieuwen",
|
||||||
|
"discovery_empty": "Geen servers gevonden op het lokale netwerk"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -738,7 +738,11 @@
|
|||||||
"generated": "Konfiguracja wygenerowana",
|
"generated": "Konfiguracja wygenerowana",
|
||||||
"server_required": "Adres serwera jest wymagany",
|
"server_required": "Adres serwera jest wymagany",
|
||||||
"nothing_to_copy": "Najpierw wygeneruj konfigurację",
|
"nothing_to_copy": "Najpierw wygeneruj konfigurację",
|
||||||
"preview_help": "Poproś o pomoc"
|
"preview_help": "Poproś o pomoc",
|
||||||
|
"rdclient_tab": "RdClient desktop",
|
||||||
|
"rdclient_new_bundle": "Nowy pakiet RdClient",
|
||||||
|
"rdclient_subtitle": "Buduj instalatory RdClient z wbudowanym adresem panelu",
|
||||||
|
"rdclient_server_url_hint": "Publiczny URL panelu BetterDesk (dashboard /remote)."
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"title": "Pobierz Agenta BetterDesk",
|
"title": "Pobierz Agenta BetterDesk",
|
||||||
@@ -1054,7 +1058,9 @@
|
|||||||
"2fa_required": "Wymagana weryfikacja 2FA",
|
"2fa_required": "Wymagana weryfikacja 2FA",
|
||||||
"2fa_hint": "Wprowadź 6-cyfrowy kod z aplikacji uwierzytelniającej",
|
"2fa_hint": "Wprowadź 6-cyfrowy kod z aplikacji uwierzytelniającej",
|
||||||
"2fa_invalid": "Wprowadź prawidłowy 6-cyfrowy kod",
|
"2fa_invalid": "Wprowadź prawidłowy 6-cyfrowy kod",
|
||||||
"verify": "Weryfikuj"
|
"verify": "Weryfikuj",
|
||||||
|
"remember_peer_password": "Zapamiętaj hasło urządzenia na tym komputerze",
|
||||||
|
"save_peer_password": "Zapisz hasło"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Identyfikacja marki",
|
"identity_title": "Identyfikacja marki",
|
||||||
@@ -3828,7 +3834,8 @@
|
|||||||
"no_groups": "Brak grup",
|
"no_groups": "Brak grup",
|
||||||
"no_tags": "Brak tagów",
|
"no_tags": "Brak tagów",
|
||||||
"collapse_devices": "Zwiń listę urządzeń",
|
"collapse_devices": "Zwiń listę urządzeń",
|
||||||
"expand_devices": "Rozwiń listę urządzeń"
|
"expand_devices": "Rozwiń listę urządzeń",
|
||||||
|
"open_settings": "Ustawienia"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Klient pulpitu zdalnego",
|
"title": "Klient pulpitu zdalnego",
|
||||||
@@ -3848,5 +3855,15 @@
|
|||||||
"network_error": "Błąd sieci",
|
"network_error": "Błąd sieci",
|
||||||
"enter_6_digits": "Wprowadź wszystkie 6 cyfr",
|
"enter_6_digits": "Wprowadź wszystkie 6 cyfr",
|
||||||
"invalid_code": "Nieprawidłowy kod"
|
"invalid_code": "Nieprawidłowy kod"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "Ustawienia RdClient",
|
||||||
|
"open": "Ustawienia",
|
||||||
|
"server_url": "URL panelu",
|
||||||
|
"tls_strict": "Ścisłe TLS",
|
||||||
|
"sign_out": "Wyloguj",
|
||||||
|
"reset_client": "Resetuj klienta",
|
||||||
|
"discovery_refresh": "Odśwież",
|
||||||
|
"discovery_empty": "Nie znaleziono serwerów w sieci lokalnej"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -738,7 +738,11 @@
|
|||||||
"build_status_failed": "Falhou",
|
"build_status_failed": "Falhou",
|
||||||
"builds_summary": "{{ready}} pronto · {{pending}} en fila · {{building}} compilando · {{failed}} falhou",
|
"builds_summary": "{{ready}} pronto · {{pending}} en fila · {{building}} compilando · {{failed}} falhou",
|
||||||
"build_error_hint": "Erro de compilação",
|
"build_error_hint": "Erro de compilação",
|
||||||
"preview_help": "Solicitar ajuda"
|
"preview_help": "Solicitar ajuda",
|
||||||
|
"rdclient_tab": "RdClient desktop",
|
||||||
|
"rdclient_new_bundle": "Novo pacote RdClient",
|
||||||
|
"rdclient_subtitle": "Criar instaladores RdClient com URL do painel incorporado",
|
||||||
|
"rdclient_server_url_hint": "URL pública da consola BetterDesk (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Porta",
|
"port": "Porta",
|
||||||
@@ -1044,7 +1048,9 @@
|
|||||||
"file_loading": "Carregando...",
|
"file_loading": "Carregando...",
|
||||||
"file_timeout": "O dispositivo remoto não respondeu. A transferência de arquivos pode não ser compatível ou pode estar desativada no computador remoto.",
|
"file_timeout": "O dispositivo remoto não respondeu. A transferência de arquivos pode não ser compatível ou pode estar desativada no computador remoto.",
|
||||||
"codec": "Codec de vídeo",
|
"codec": "Codec de vídeo",
|
||||||
"codec_auto": "Automático (recomendado)"
|
"codec_auto": "Automático (recomendado)",
|
||||||
|
"remember_peer_password": "Lembrar senha do dispositivo neste computador",
|
||||||
|
"save_peer_password": "Guardar senha"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Identidade da marca",
|
"identity_title": "Identidade da marca",
|
||||||
@@ -3828,7 +3834,8 @@
|
|||||||
"no_groups": "Sem grupos",
|
"no_groups": "Sem grupos",
|
||||||
"no_tags": "Sem etiquetas",
|
"no_tags": "Sem etiquetas",
|
||||||
"collapse_devices": "Recolher lista de dispositivos",
|
"collapse_devices": "Recolher lista de dispositivos",
|
||||||
"expand_devices": "Expandir lista de dispositivos"
|
"expand_devices": "Expandir lista de dispositivos",
|
||||||
|
"open_settings": "Definições"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Cliente de ambiente de trabalho remoto",
|
"title": "Cliente de ambiente de trabalho remoto",
|
||||||
@@ -3848,5 +3855,15 @@
|
|||||||
"network_error": "Erro de rede",
|
"network_error": "Erro de rede",
|
||||||
"enter_6_digits": "Introduza os 6 dígitos",
|
"enter_6_digits": "Introduza os 6 dígitos",
|
||||||
"invalid_code": "Código inválido"
|
"invalid_code": "Código inválido"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "Definições RdClient",
|
||||||
|
"open": "Definições",
|
||||||
|
"server_url": "URL do painel",
|
||||||
|
"tls_strict": "TLS estrito",
|
||||||
|
"sign_out": "Terminar sessão",
|
||||||
|
"reset_client": "Repor cliente",
|
||||||
|
"discovery_refresh": "Atualizar",
|
||||||
|
"discovery_empty": "Nenhum servidor encontrado na rede local"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -744,7 +744,11 @@
|
|||||||
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
||||||
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} eșuat",
|
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} eșuat",
|
||||||
"build_error_hint": "Eroare build",
|
"build_error_hint": "Eroare build",
|
||||||
"preview_help": "Solicită ajutor"
|
"preview_help": "Solicită ajutor",
|
||||||
|
"rdclient_tab": "RdClient desktop",
|
||||||
|
"rdclient_new_bundle": "Pachet RdClient nou",
|
||||||
|
"rdclient_subtitle": "Construiește instalatoare RdClient cu URL panou integrat",
|
||||||
|
"rdclient_server_url_hint": "URL public al consolei BetterDesk (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1050,7 +1054,9 @@
|
|||||||
"file_loading": "Se încarcă...",
|
"file_loading": "Se încarcă...",
|
||||||
"file_timeout": "Dispozitivul de la distanță nu a răspuns. Este posibil ca transferul de fișiere să nu fie acceptat sau să fie dezactivat pe computerul de la distanță.",
|
"file_timeout": "Dispozitivul de la distanță nu a răspuns. Este posibil ca transferul de fișiere să nu fie acceptat sau să fie dezactivat pe computerul de la distanță.",
|
||||||
"codec": "Codec video",
|
"codec": "Codec video",
|
||||||
"codec_auto": "Auto (recomandat)"
|
"codec_auto": "Auto (recomandat)",
|
||||||
|
"remember_peer_password": "Memorează parola dispozitivului pe acest computer",
|
||||||
|
"save_peer_password": "Salvează parola"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Identitatea de marcă",
|
"identity_title": "Identitatea de marcă",
|
||||||
@@ -3834,7 +3840,8 @@
|
|||||||
"no_groups": "Fără grupuri",
|
"no_groups": "Fără grupuri",
|
||||||
"no_tags": "Fără etichete",
|
"no_tags": "Fără etichete",
|
||||||
"collapse_devices": "Restrânge lista de dispozitive",
|
"collapse_devices": "Restrânge lista de dispozitive",
|
||||||
"expand_devices": "Extinde lista de dispozitive"
|
"expand_devices": "Extinde lista de dispozitive",
|
||||||
|
"open_settings": "Setări"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Client desktop la distanță",
|
"title": "Client desktop la distanță",
|
||||||
@@ -3854,5 +3861,15 @@
|
|||||||
"network_error": "Eroare de rețea",
|
"network_error": "Eroare de rețea",
|
||||||
"enter_6_digits": "Introduceți toate cele 6 cifre",
|
"enter_6_digits": "Introduceți toate cele 6 cifre",
|
||||||
"invalid_code": "Cod invalid"
|
"invalid_code": "Cod invalid"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "Setări RdClient",
|
||||||
|
"open": "Setări",
|
||||||
|
"server_url": "URL panou",
|
||||||
|
"tls_strict": "TLS strict",
|
||||||
|
"sign_out": "Deconectare",
|
||||||
|
"reset_client": "Resetează clientul",
|
||||||
|
"discovery_refresh": "Reîmprospătează",
|
||||||
|
"discovery_empty": "Nu s-au găsit servere în rețeaua locală"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -744,7 +744,11 @@
|
|||||||
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
||||||
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} misslyckades",
|
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} misslyckades",
|
||||||
"build_error_hint": "Byggfel",
|
"build_error_hint": "Byggfel",
|
||||||
"preview_help": "Begär hjälp"
|
"preview_help": "Begär hjälp",
|
||||||
|
"rdclient_tab": "RdClient desktop",
|
||||||
|
"rdclient_new_bundle": "Nytt RdClient-paket",
|
||||||
|
"rdclient_subtitle": "Bygg RdClient-installerare med inbäddad panel-URL",
|
||||||
|
"rdclient_server_url_hint": "Offentlig URL till BetterDesk-konsolen (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1050,7 +1054,9 @@
|
|||||||
"file_loading": "Laddar...",
|
"file_loading": "Laddar...",
|
||||||
"file_timeout": "Fjärrenheten svarade inte. Filöverföring kanske inte stöds eller är inaktiverad på fjärrmaskinen.",
|
"file_timeout": "Fjärrenheten svarade inte. Filöverföring kanske inte stöds eller är inaktiverad på fjärrmaskinen.",
|
||||||
"codec": "Video Codec",
|
"codec": "Video Codec",
|
||||||
"codec_auto": "Auto (rekommenderas)"
|
"codec_auto": "Auto (rekommenderas)",
|
||||||
|
"remember_peer_password": "Kom ihåg enhetslösenord på den här datorn",
|
||||||
|
"save_peer_password": "Spara lösenord"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Varumärkesidentitet",
|
"identity_title": "Varumärkesidentitet",
|
||||||
@@ -3834,7 +3840,8 @@
|
|||||||
"no_groups": "Inga grupper",
|
"no_groups": "Inga grupper",
|
||||||
"no_tags": "Inga taggar",
|
"no_tags": "Inga taggar",
|
||||||
"collapse_devices": "Fäll ihop enhetslista",
|
"collapse_devices": "Fäll ihop enhetslista",
|
||||||
"expand_devices": "Expandera enhetslista"
|
"expand_devices": "Expandera enhetslista",
|
||||||
|
"open_settings": "Inställningar"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Remote Desktop-klient",
|
"title": "Remote Desktop-klient",
|
||||||
@@ -3854,5 +3861,15 @@
|
|||||||
"network_error": "Nätverksfel",
|
"network_error": "Nätverksfel",
|
||||||
"enter_6_digits": "Ange alla 6 siffror",
|
"enter_6_digits": "Ange alla 6 siffror",
|
||||||
"invalid_code": "Ogiltig kod"
|
"invalid_code": "Ogiltig kod"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient-inställningar",
|
||||||
|
"open": "Inställningar",
|
||||||
|
"server_url": "Panel-URL",
|
||||||
|
"tls_strict": "Strikt TLS",
|
||||||
|
"sign_out": "Logga ut",
|
||||||
|
"reset_client": "Återställ klient",
|
||||||
|
"discovery_refresh": "Uppdatera",
|
||||||
|
"discovery_empty": "Inga servrar hittades i det lokala nätverket"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -744,7 +744,11 @@
|
|||||||
"build_status_failed": "ล้มเหลว",
|
"build_status_failed": "ล้มเหลว",
|
||||||
"builds_summary": "{{ready}} พร้อม · {{pending}} รอคิว · {{building}} กำลัง Build · {{failed}} ล้มเหลว",
|
"builds_summary": "{{ready}} พร้อม · {{pending}} รอคิว · {{building}} กำลัง Build · {{failed}} ล้มเหลว",
|
||||||
"build_error_hint": "ข้อผิดพลาด Build",
|
"build_error_hint": "ข้อผิดพลาด Build",
|
||||||
"preview_help": "ขอความช่วยเหลือ"
|
"preview_help": "ขอความช่วยเหลือ",
|
||||||
|
"rdclient_tab": "RdClient เดสก์ท็อป",
|
||||||
|
"rdclient_new_bundle": "ชุด RdClient ใหม่",
|
||||||
|
"rdclient_subtitle": "สร้างตัวติดตั้ง RdClient พร้อม URL แผงควบคุมฝังตัว",
|
||||||
|
"rdclient_server_url_hint": "URL สาธารณะของคอนโซล BetterDesk (/remote)"
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1050,7 +1054,9 @@
|
|||||||
"file_loading": "กำลังโหลด...",
|
"file_loading": "กำลังโหลด...",
|
||||||
"file_timeout": "อุปกรณ์ระยะไกลไม่ตอบสนอง การถ่ายโอนไฟล์อาจไม่รองรับหรือถูกปิดใช้งานบนเครื่องระยะไกล",
|
"file_timeout": "อุปกรณ์ระยะไกลไม่ตอบสนอง การถ่ายโอนไฟล์อาจไม่รองรับหรือถูกปิดใช้งานบนเครื่องระยะไกล",
|
||||||
"codec": "ตัวแปลงสัญญาณวิดีโอ",
|
"codec": "ตัวแปลงสัญญาณวิดีโอ",
|
||||||
"codec_auto": "อัตโนมัติ (แนะนำ)"
|
"codec_auto": "อัตโนมัติ (แนะนำ)",
|
||||||
|
"remember_peer_password": "จดจำรหัสผ่านอุปกรณ์บนคอมพิวเตอร์นี้",
|
||||||
|
"save_peer_password": "บันทึกรหัสผ่าน"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "เอกลักษณ์ของแบรนด์",
|
"identity_title": "เอกลักษณ์ของแบรนด์",
|
||||||
@@ -3834,7 +3840,8 @@
|
|||||||
"no_groups": "ไม่มีกลุ่ม",
|
"no_groups": "ไม่มีกลุ่ม",
|
||||||
"no_tags": "ไม่มีแท็ก",
|
"no_tags": "ไม่มีแท็ก",
|
||||||
"collapse_devices": "ยุบรายการอุปกรณ์",
|
"collapse_devices": "ยุบรายการอุปกรณ์",
|
||||||
"expand_devices": "ขยายรายการอุปกรณ์"
|
"expand_devices": "ขยายรายการอุปกรณ์",
|
||||||
|
"open_settings": "การตั้งค่า"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "ไคลเอนต์เดสก์ท็อประยะไกล",
|
"title": "ไคลเอนต์เดสก์ท็อประยะไกล",
|
||||||
@@ -3854,5 +3861,15 @@
|
|||||||
"network_error": "ข้อผิดพลาดเครือข่าย",
|
"network_error": "ข้อผิดพลาดเครือข่าย",
|
||||||
"enter_6_digits": "ป้อนตัวเลขครบ 6 หลัก",
|
"enter_6_digits": "ป้อนตัวเลขครบ 6 หลัก",
|
||||||
"invalid_code": "รหัสไม่ถูกต้อง"
|
"invalid_code": "รหัสไม่ถูกต้อง"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "การตั้งค่า RdClient",
|
||||||
|
"open": "การตั้งค่า",
|
||||||
|
"server_url": "URL แผงควบคุม",
|
||||||
|
"tls_strict": "TLS เข้มงวด",
|
||||||
|
"sign_out": "ออกจากระบบ",
|
||||||
|
"reset_client": "รีเซ็ตไคลเอนต์",
|
||||||
|
"discovery_refresh": "รีเฟรช",
|
||||||
|
"discovery_empty": "ไม่พบเซิร์ฟเวอร์ในเครือข่ายท้องถิ่น"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -744,7 +744,11 @@
|
|||||||
"build_status_failed": "Başarısız",
|
"build_status_failed": "Başarısız",
|
||||||
"builds_summary": "{{ready}} hazır · {{pending}} sırada · {{building}} derleniyor · {{failed}} başarısız",
|
"builds_summary": "{{ready}} hazır · {{pending}} sırada · {{building}} derleniyor · {{failed}} başarısız",
|
||||||
"build_error_hint": "Derleme hatası",
|
"build_error_hint": "Derleme hatası",
|
||||||
"preview_help": "Yardım iste"
|
"preview_help": "Yardım iste",
|
||||||
|
"rdclient_tab": "RdClient masaüstü",
|
||||||
|
"rdclient_new_bundle": "Yeni RdClient paketi",
|
||||||
|
"rdclient_subtitle": "Gömülü panel URL’li RdClient yükleyicileri oluştur",
|
||||||
|
"rdclient_server_url_hint": "BetterDesk konsolunun genel URL’si (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1050,7 +1054,9 @@
|
|||||||
"file_loading": "Yükleniyor...",
|
"file_loading": "Yükleniyor...",
|
||||||
"file_timeout": "Uzak aygıt yanıt vermedi. Uzak makinede dosya aktarımı desteklenmiyor veya devre dışı bırakılmış olabilir.",
|
"file_timeout": "Uzak aygıt yanıt vermedi. Uzak makinede dosya aktarımı desteklenmiyor veya devre dışı bırakılmış olabilir.",
|
||||||
"codec": "Video Codec'i",
|
"codec": "Video Codec'i",
|
||||||
"codec_auto": "Otomatik (önerilen)"
|
"codec_auto": "Otomatik (önerilen)",
|
||||||
|
"remember_peer_password": "Cihaz parolasını bu bilgisayarda hatırla",
|
||||||
|
"save_peer_password": "Parolayı kaydet"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Marka Kimliği",
|
"identity_title": "Marka Kimliği",
|
||||||
@@ -3834,7 +3840,8 @@
|
|||||||
"no_groups": "Grup yok",
|
"no_groups": "Grup yok",
|
||||||
"no_tags": "Etiket yok",
|
"no_tags": "Etiket yok",
|
||||||
"collapse_devices": "Cihaz listesini daralt",
|
"collapse_devices": "Cihaz listesini daralt",
|
||||||
"expand_devices": "Cihaz listesini genişlet"
|
"expand_devices": "Cihaz listesini genişlet",
|
||||||
|
"open_settings": "Ayarlar"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Uzak Masaüstü İstemcisi",
|
"title": "Uzak Masaüstü İstemcisi",
|
||||||
@@ -3854,5 +3861,15 @@
|
|||||||
"network_error": "Ağ hatası",
|
"network_error": "Ağ hatası",
|
||||||
"enter_6_digits": "6 hanenin tamamını girin",
|
"enter_6_digits": "6 hanenin tamamını girin",
|
||||||
"invalid_code": "Geçersiz kod"
|
"invalid_code": "Geçersiz kod"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient ayarları",
|
||||||
|
"open": "Ayarlar",
|
||||||
|
"server_url": "Panel URL",
|
||||||
|
"tls_strict": "Katı TLS",
|
||||||
|
"sign_out": "Oturumu kapat",
|
||||||
|
"reset_client": "İstemciyi sıfırla",
|
||||||
|
"discovery_refresh": "Yenile",
|
||||||
|
"discovery_empty": "Yerel ağda sunucu bulunamadı"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -744,7 +744,11 @@
|
|||||||
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
"rebuild_queued": "Alle Plattform-Builds in der Warteschlange",
|
||||||
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} не вдалося",
|
"builds_summary": "{{ready}} bereit · {{pending}} in Warteschlange · {{building}} wird erstellt · {{failed}} не вдалося",
|
||||||
"build_error_hint": "Помилка збірки",
|
"build_error_hint": "Помилка збірки",
|
||||||
"preview_help": "Запросити допомогу"
|
"preview_help": "Запросити допомогу",
|
||||||
|
"rdclient_tab": "RdClient desktop",
|
||||||
|
"rdclient_new_bundle": "Новий пакет RdClient",
|
||||||
|
"rdclient_subtitle": "Збирати інсталятори RdClient із вбудованою URL панелі",
|
||||||
|
"rdclient_server_url_hint": "Публічна URL консолі BetterDesk (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1050,7 +1054,9 @@
|
|||||||
"file_loading": "Завантаження...",
|
"file_loading": "Завантаження...",
|
||||||
"file_timeout": "Віддалений пристрій не відповідає. Передача файлів може не підтримуватися або вимкнена на віддаленій машині.",
|
"file_timeout": "Віддалений пристрій не відповідає. Передача файлів може не підтримуватися або вимкнена на віддаленій машині.",
|
||||||
"codec": "Відеокодек",
|
"codec": "Відеокодек",
|
||||||
"codec_auto": "Авто (рекомендовано)"
|
"codec_auto": "Авто (рекомендовано)",
|
||||||
|
"remember_peer_password": "Запам’ятати пароль пристрою на цьому комп’ютері",
|
||||||
|
"save_peer_password": "Зберегти пароль"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Ідентичність бренду",
|
"identity_title": "Ідентичність бренду",
|
||||||
@@ -3834,7 +3840,8 @@
|
|||||||
"no_groups": "Немає груп",
|
"no_groups": "Немає груп",
|
||||||
"no_tags": "Немає тегів",
|
"no_tags": "Немає тегів",
|
||||||
"collapse_devices": "Згорнути список пристроїв",
|
"collapse_devices": "Згорнути список пристроїв",
|
||||||
"expand_devices": "Розгорнути список пристроїв"
|
"expand_devices": "Розгорнути список пристроїв",
|
||||||
|
"open_settings": "Налаштування"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Клієнт віддаленого робочого столу",
|
"title": "Клієнт віддаленого робочого столу",
|
||||||
@@ -3854,5 +3861,15 @@
|
|||||||
"network_error": "Помилка мережі",
|
"network_error": "Помилка мережі",
|
||||||
"enter_6_digits": "Введіть усі 6 цифр",
|
"enter_6_digits": "Введіть усі 6 цифр",
|
||||||
"invalid_code": "Невірний код"
|
"invalid_code": "Невірний код"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "Налаштування RdClient",
|
||||||
|
"open": "Налаштування",
|
||||||
|
"server_url": "URL панелі",
|
||||||
|
"tls_strict": "Суворий TLS",
|
||||||
|
"sign_out": "Вийти",
|
||||||
|
"reset_client": "Скинути клієнт",
|
||||||
|
"discovery_refresh": "Оновити",
|
||||||
|
"discovery_empty": "Серверів у локальній мережі не знайдено"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -744,7 +744,11 @@
|
|||||||
"build_status_failed": "Thất bại",
|
"build_status_failed": "Thất bại",
|
||||||
"builds_summary": "{{ready}} sẵn sàng · {{pending}} đang chờ · {{building}} đang build · {{failed}} thất bại",
|
"builds_summary": "{{ready}} sẵn sàng · {{pending}} đang chờ · {{building}} đang build · {{failed}} thất bại",
|
||||||
"build_error_hint": "Lỗi build",
|
"build_error_hint": "Lỗi build",
|
||||||
"preview_help": "Yêu cầu trợ giúp"
|
"preview_help": "Yêu cầu trợ giúp",
|
||||||
|
"rdclient_tab": "RdClient desktop",
|
||||||
|
"rdclient_new_bundle": "Gói RdClient mới",
|
||||||
|
"rdclient_subtitle": "Tạo trình cài RdClient với URL bảng điều khiển nhúng",
|
||||||
|
"rdclient_server_url_hint": "URL công khai của bảng điều khiển BetterDesk (/remote)."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
@@ -1050,7 +1054,9 @@
|
|||||||
"file_loading": "Đang tải...",
|
"file_loading": "Đang tải...",
|
||||||
"file_timeout": "Thiết bị từ xa không phản hồi. Truyền tệp có thể không được hỗ trợ hoặc bị tắt trên máy từ xa.",
|
"file_timeout": "Thiết bị từ xa không phản hồi. Truyền tệp có thể không được hỗ trợ hoặc bị tắt trên máy từ xa.",
|
||||||
"codec": "Bộ giải mã video",
|
"codec": "Bộ giải mã video",
|
||||||
"codec_auto": "Tự động (được khuyến nghị)"
|
"codec_auto": "Tự động (được khuyến nghị)",
|
||||||
|
"remember_peer_password": "Ghi nhớ mật khẩu thiết bị trên máy này",
|
||||||
|
"save_peer_password": "Lưu mật khẩu"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "Nhận diện thương hiệu",
|
"identity_title": "Nhận diện thương hiệu",
|
||||||
@@ -3834,7 +3840,8 @@
|
|||||||
"no_groups": "Không có nhóm",
|
"no_groups": "Không có nhóm",
|
||||||
"no_tags": "Không có thẻ",
|
"no_tags": "Không có thẻ",
|
||||||
"collapse_devices": "Thu gọn danh sách thiết bị",
|
"collapse_devices": "Thu gọn danh sách thiết bị",
|
||||||
"expand_devices": "Mở rộng danh sách thiết bị"
|
"expand_devices": "Mở rộng danh sách thiết bị",
|
||||||
|
"open_settings": "Cài đặt"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "Máy khách Desktop từ xa",
|
"title": "Máy khách Desktop từ xa",
|
||||||
@@ -3854,5 +3861,15 @@
|
|||||||
"network_error": "Lỗi mạng",
|
"network_error": "Lỗi mạng",
|
||||||
"enter_6_digits": "Nhập đủ 6 chữ số",
|
"enter_6_digits": "Nhập đủ 6 chữ số",
|
||||||
"invalid_code": "Mã không hợp lệ"
|
"invalid_code": "Mã không hợp lệ"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "Cài đặt RdClient",
|
||||||
|
"open": "Cài đặt",
|
||||||
|
"server_url": "URL bảng điều khiển",
|
||||||
|
"tls_strict": "TLS nghiêm ngặt",
|
||||||
|
"sign_out": "Đăng xuất",
|
||||||
|
"reset_client": "Đặt lại client",
|
||||||
|
"discovery_refresh": "Làm mới",
|
||||||
|
"discovery_empty": "Không tìm thấy máy chủ trên mạng cục bộ"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -742,7 +742,11 @@
|
|||||||
"usage_title": "連接詳情",
|
"usage_title": "連接詳情",
|
||||||
"generated": "配置已生成",
|
"generated": "配置已生成",
|
||||||
"server_required": "服務器地址爲必填項",
|
"server_required": "服務器地址爲必填項",
|
||||||
"nothing_to_copy": "請先生成配置"
|
"nothing_to_copy": "請先生成配置",
|
||||||
|
"rdclient_tab": "RdClient 桌面版",
|
||||||
|
"rdclient_new_bundle": "新增 RdClient 套件",
|
||||||
|
"rdclient_subtitle": "建置內嵌面板 URL 的 RdClient 安裝程式",
|
||||||
|
"rdclient_server_url_hint": "BetterDesk 主控台的公開 URL (/remote)。"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"title": "下載 BetterDesk 代理",
|
"title": "下載 BetterDesk 代理",
|
||||||
@@ -1058,7 +1062,9 @@
|
|||||||
"2fa_required": "需要雙因素驗證",
|
"2fa_required": "需要雙因素驗證",
|
||||||
"2fa_hint": "輸入驗證器應用中的6位數代碼",
|
"2fa_hint": "輸入驗證器應用中的6位數代碼",
|
||||||
"2fa_invalid": "請輸入有效的6位數代碼",
|
"2fa_invalid": "請輸入有效的6位數代碼",
|
||||||
"verify": "驗證"
|
"verify": "驗證",
|
||||||
|
"remember_peer_password": "在此裝置上記住裝置密碼",
|
||||||
|
"save_peer_password": "儲存密碼"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "品牌標識",
|
"identity_title": "品牌標識",
|
||||||
@@ -3832,7 +3838,8 @@
|
|||||||
"no_groups": "無群組",
|
"no_groups": "無群組",
|
||||||
"no_tags": "無標籤",
|
"no_tags": "無標籤",
|
||||||
"collapse_devices": "摺疊裝置清單",
|
"collapse_devices": "摺疊裝置清單",
|
||||||
"expand_devices": "展開裝置清單"
|
"expand_devices": "展開裝置清單",
|
||||||
|
"open_settings": "設定"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "遠端桌面用戶端",
|
"title": "遠端桌面用戶端",
|
||||||
@@ -3852,5 +3859,15 @@
|
|||||||
"network_error": "網路錯誤",
|
"network_error": "網路錯誤",
|
||||||
"enter_6_digits": "請輸入全部6位數字",
|
"enter_6_digits": "請輸入全部6位數字",
|
||||||
"invalid_code": "代碼無效"
|
"invalid_code": "代碼無效"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient 設定",
|
||||||
|
"open": "設定",
|
||||||
|
"server_url": "面板 URL",
|
||||||
|
"tls_strict": "嚴格 TLS",
|
||||||
|
"sign_out": "登出",
|
||||||
|
"reset_client": "重設用戶端",
|
||||||
|
"discovery_refresh": "重新整理",
|
||||||
|
"discovery_empty": "區域網路中找不到伺服器"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -738,7 +738,11 @@
|
|||||||
"build_status_failed": "失败",
|
"build_status_failed": "失败",
|
||||||
"builds_summary": "{{ready}} 就绪 · {{pending}} 排队中 · {{building}} 构建中 · {{failed}} 失败",
|
"builds_summary": "{{ready}} 就绪 · {{pending}} 排队中 · {{building}} 构建中 · {{failed}} 失败",
|
||||||
"build_error_hint": "构建错误",
|
"build_error_hint": "构建错误",
|
||||||
"preview_help": "请求帮助"
|
"preview_help": "请求帮助",
|
||||||
|
"rdclient_tab": "RdClient 桌面版",
|
||||||
|
"rdclient_new_bundle": "新建 RdClient 包",
|
||||||
|
"rdclient_subtitle": "构建嵌入面板 URL 的 RdClient 安装程序",
|
||||||
|
"rdclient_server_url_hint": "BetterDesk 控制台的公开 URL (/remote)。"
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"port": "端口",
|
"port": "端口",
|
||||||
@@ -1044,7 +1048,9 @@
|
|||||||
"use_cdap_fallback": "使用 CDAP 查看器",
|
"use_cdap_fallback": "使用 CDAP 查看器",
|
||||||
"cdap_fallback_hint": "Open the lightweight JPEG-polling viewer that uses bd-signal/CDAP instead of the RustDesk relay. Useful when the peer is offline on the relay but reachable via the agent management channel.",
|
"cdap_fallback_hint": "Open the lightweight JPEG-polling viewer that uses bd-signal/CDAP instead of the RustDesk relay. Useful when the peer is offline on the relay but reachable via the agent management channel.",
|
||||||
"codec": "视频编解码器",
|
"codec": "视频编解码器",
|
||||||
"codec_auto": "自动(推荐)"
|
"codec_auto": "自动(推荐)",
|
||||||
|
"remember_peer_password": "在此设备上记住设备密码",
|
||||||
|
"save_peer_password": "保存密码"
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"identity_title": "品牌标识",
|
"identity_title": "品牌标识",
|
||||||
@@ -3828,7 +3834,8 @@
|
|||||||
"no_groups": "无分组",
|
"no_groups": "无分组",
|
||||||
"no_tags": "无标签",
|
"no_tags": "无标签",
|
||||||
"collapse_devices": "折叠设备列表",
|
"collapse_devices": "折叠设备列表",
|
||||||
"expand_devices": "展开设备列表"
|
"expand_devices": "展开设备列表",
|
||||||
|
"open_settings": "设置"
|
||||||
},
|
},
|
||||||
"rdclient_login": {
|
"rdclient_login": {
|
||||||
"title": "远程桌面客户端",
|
"title": "远程桌面客户端",
|
||||||
@@ -3848,5 +3855,15 @@
|
|||||||
"network_error": "网络错误",
|
"network_error": "网络错误",
|
||||||
"enter_6_digits": "请输入全部6位数字",
|
"enter_6_digits": "请输入全部6位数字",
|
||||||
"invalid_code": "代码无效"
|
"invalid_code": "代码无效"
|
||||||
|
},
|
||||||
|
"rdclient_settings": {
|
||||||
|
"title": "RdClient 设置",
|
||||||
|
"open": "设置",
|
||||||
|
"server_url": "面板 URL",
|
||||||
|
"tls_strict": "严格 TLS",
|
||||||
|
"sign_out": "退出登录",
|
||||||
|
"reset_client": "重置客户端",
|
||||||
|
"discovery_refresh": "刷新",
|
||||||
|
"discovery_empty": "在本地网络中未找到服务器"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html.rd-desk-desktop {
|
html.rd-desk-desktop {
|
||||||
height: 100%;
|
height: var(--rd-desk-vh, 100vh);
|
||||||
height: 100vh;
|
max-height: var(--rd-desk-vh, 100vh);
|
||||||
max-height: 100vh;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rd-desk-body {
|
.rd-desk-body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 100%;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--bg-primary, #0d1117);
|
background: var(--bg-primary, #0d1117);
|
||||||
color: var(--text-primary, #e6edf3);
|
color: var(--text-primary, #e6edf3);
|
||||||
@@ -27,9 +27,14 @@ html.rd-desk-desktop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rd-desk-desktop.rd-desk-body {
|
.rd-desk-desktop.rd-desk-body {
|
||||||
height: 100vh;
|
position: fixed;
|
||||||
max-height: 100vh;
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--rd-desk-vh, 100vh) !important;
|
||||||
|
max-height: var(--rd-desk-vh, 100vh);
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow: hidden !important;
|
||||||
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rd-desk-app {
|
.rd-desk-app {
|
||||||
@@ -43,8 +48,10 @@ html.rd-desk-desktop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rd-desk-desktop .rd-desk-app {
|
.rd-desk-desktop .rd-desk-app {
|
||||||
height: 100vh;
|
position: absolute;
|
||||||
max-height: 100vh;
|
inset: 0;
|
||||||
|
height: 100% !important;
|
||||||
|
max-height: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Header ── */
|
/* ── Header ── */
|
||||||
@@ -193,6 +200,35 @@ html.rd-desk-desktop {
|
|||||||
75% { transform: translateX(4px); }
|
75% { transform: translateX(4px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Scroll areas (independent sidebar + device list) ── */
|
||||||
|
.rd-desk-sidebar-scroll,
|
||||||
|
.rd-desk-devices-scroll {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--scrollbar-thumb, var(--bg-elevated, #30363d)) var(--scrollbar-track, var(--bg-secondary, #161b22));
|
||||||
|
}
|
||||||
|
|
||||||
|
.rd-desk-sidebar-scroll::-webkit-scrollbar,
|
||||||
|
.rd-desk-devices-scroll::-webkit-scrollbar {
|
||||||
|
width: var(--scrollbar-width, 8px);
|
||||||
|
height: var(--scrollbar-width, 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rd-desk-sidebar-scroll::-webkit-scrollbar-track,
|
||||||
|
.rd-desk-devices-scroll::-webkit-scrollbar-track {
|
||||||
|
background: var(--scrollbar-track, var(--bg-secondary, #161b22));
|
||||||
|
}
|
||||||
|
|
||||||
|
.rd-desk-sidebar-scroll::-webkit-scrollbar-thumb,
|
||||||
|
.rd-desk-devices-scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--scrollbar-thumb, var(--bg-elevated, #30363d));
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rd-desk-sidebar-scroll::-webkit-scrollbar-thumb:hover,
|
||||||
|
.rd-desk-devices-scroll::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--scrollbar-thumb-hover, var(--border-hover, #484f58));
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Workspace ── */
|
/* ── Workspace ── */
|
||||||
.rd-desk-workspace {
|
.rd-desk-workspace {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -204,14 +240,24 @@ html.rd-desk-desktop {
|
|||||||
/* ── Sidebar ── */
|
/* ── Sidebar ── */
|
||||||
.rd-desk-sidebar {
|
.rd-desk-sidebar {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
flex-shrink: 0;
|
flex: 0 0 240px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-right: 1px solid var(--border-primary, #30363d);
|
border-right: 1px solid var(--border-primary, #30363d);
|
||||||
background: var(--surface-glass-bg-secondary, var(--bg-secondary, #161b22));
|
background: var(--surface-glass-bg-secondary, var(--bg-secondary, #161b22));
|
||||||
padding: 12px 0 12px;
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rd-desk-sidebar-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
padding: 12px 0;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rd-desk-nav-section {
|
.rd-desk-nav-section {
|
||||||
@@ -267,20 +313,13 @@ html.rd-desk-desktop {
|
|||||||
|
|
||||||
.rd-desk-nav-body {
|
.rd-desk-nav-body {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-height: min(220px, 28vh);
|
overflow: visible;
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rd-desk-nav-section.is-collapsed .rd-desk-nav-body {
|
.rd-desk-nav-section.is-collapsed .rd-desk-nav-body {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rd-desk-desktop .rd-desk-nav-body {
|
|
||||||
max-height: min(180px, 24vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rd-desk-nav-list {
|
.rd-desk-nav-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -359,6 +398,7 @@ html.rd-desk-desktop {
|
|||||||
/* ── Content ── */
|
/* ── Content ── */
|
||||||
.rd-desk-content {
|
.rd-desk-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -872,16 +912,13 @@ html.rd-desk-desktop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rd-desk-sidebar {
|
.rd-desk-sidebar {
|
||||||
|
flex: 0 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: min(240px, 35vh);
|
max-height: min(240px, 35vh);
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-bottom: 1px solid var(--border-primary, #30363d);
|
border-bottom: 1px solid var(--border-primary, #30363d);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rd-desk-nav-body {
|
|
||||||
max-height: min(140px, 18vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rd-desk-quick-label {
|
.rd-desk-quick-label {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
slugManual: false,
|
slugManual: false,
|
||||||
previewTimer: null,
|
previewTimer: null,
|
||||||
buildsPollTimer: null,
|
buildsPollTimer: null,
|
||||||
|
productType: 'agent',
|
||||||
};
|
};
|
||||||
|
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
@@ -421,14 +422,16 @@
|
|||||||
els['gen-save-btn'].disabled = true;
|
els['gen-save-btn'].disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setEditorForNew() {
|
function setEditorForNew(productType) {
|
||||||
state.currentId = 'new';
|
state.currentId = 'new';
|
||||||
state.currentBundle = null;
|
state.currentBundle = null;
|
||||||
state.currentBuilds = [];
|
state.currentBuilds = [];
|
||||||
state.dirty = false;
|
state.dirty = false;
|
||||||
state.slugManual = false;
|
state.slugManual = false;
|
||||||
|
state.productType = productType || 'agent';
|
||||||
stopBuildsPoll();
|
stopBuildsPoll();
|
||||||
els['gen-editor-title'].innerHTML = `<span class="material-icons">add_circle</span> ${escapeText(t('generator.new_bundle', 'New bundle'))}`;
|
const titleKey = state.productType === 'rdclient' ? 'generator.rdclient_new_bundle' : 'generator.new_bundle';
|
||||||
|
els['gen-editor-title'].innerHTML = `<span class="material-icons">add_circle</span> ${escapeText(t(titleKey, 'New bundle'))}`;
|
||||||
els['gen-name'].value = '';
|
els['gen-name'].value = '';
|
||||||
if (els['gen-slug']) els['gen-slug'].value = '';
|
if (els['gen-slug']) els['gen-slug'].value = '';
|
||||||
writeBranding(DEFAULT_BRANDING);
|
writeBranding(DEFAULT_BRANDING);
|
||||||
@@ -453,6 +456,7 @@
|
|||||||
function setEditorForBundle(bundle) {
|
function setEditorForBundle(bundle) {
|
||||||
state.currentId = bundle.bundle_id;
|
state.currentId = bundle.bundle_id;
|
||||||
state.currentBundle = bundle;
|
state.currentBundle = bundle;
|
||||||
|
state.productType = bundle.product_type || 'agent';
|
||||||
state.dirty = false;
|
state.dirty = false;
|
||||||
state.slugManual = true;
|
state.slugManual = true;
|
||||||
stopBuildsPoll();
|
stopBuildsPoll();
|
||||||
@@ -514,6 +518,7 @@
|
|||||||
name: els['gen-name'].value.trim(),
|
name: els['gen-name'].value.trim(),
|
||||||
slug: readSlugInput(),
|
slug: readSlugInput(),
|
||||||
branding: readBranding(),
|
branding: readBranding(),
|
||||||
|
product_type: state.productType || 'agent',
|
||||||
};
|
};
|
||||||
if (!payload.name) {
|
if (!payload.name) {
|
||||||
showErrors([t('generator.errors.name_required', 'Bundle name is required')]);
|
showErrors([t('generator.errors.name_required', 'Bundle name is required')]);
|
||||||
@@ -653,7 +658,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
els['gen-new-bundle'].addEventListener('click', () => setEditorForNew());
|
els['gen-new-bundle'].addEventListener('click', () => setEditorForNew('agent'));
|
||||||
|
const rdBtn = $('gen-new-rdclient');
|
||||||
|
if (rdBtn) rdBtn.addEventListener('click', () => setEditorForNew('rdclient'));
|
||||||
els['gen-save-btn'].addEventListener('click', saveBundle);
|
els['gen-save-btn'].addEventListener('click', saveBundle);
|
||||||
els['gen-rebuild-btn'].addEventListener('click', rebuildAllBuilds);
|
els['gen-rebuild-btn'].addEventListener('click', rebuildAllBuilds);
|
||||||
els['gen-revoke-btn'].addEventListener('click', toggleRevoke);
|
els['gen-revoke-btn'].addEventListener('click', toggleRevoke);
|
||||||
|
|||||||
@@ -88,23 +88,23 @@
|
|||||||
&& typeof window.VideoDecoder !== 'undefined';
|
&& typeof window.VideoDecoder !== 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {Promise<string[]>|null} */
|
||||||
|
let _decodableCodecsPromise = null;
|
||||||
|
/** Codecs confirmed broken at runtime (e.g. AV1 on WebKit HW). */
|
||||||
|
const _blockedWireCodecs = new Set();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the ordered list of codecs the operator can decode, advertised to
|
* Sync fallback before async probe completes.
|
||||||
* the agent in the `desktop_start` payload. The agent intersects this with
|
|
||||||
* its own GPU/encoder ability and picks the first match in its own
|
|
||||||
* preference order (AV1 → VP9 → H264 → WebP).
|
|
||||||
*
|
|
||||||
* - Secure context (WebCodecs): advertise AV1/VP9/H264 for max quality.
|
|
||||||
* - Plain HTTP with MSE H.264: advertise H264 so the agent still sends a
|
|
||||||
* GPU-encoded video stream (decoded via JMuxer/MSE) instead of falling
|
|
||||||
* back to MJPEG — this is the key to smooth video without HTTPS.
|
|
||||||
* - Otherwise: image formats only (WebP/JPEG).
|
|
||||||
* @returns {string[]}
|
* @returns {string[]}
|
||||||
*/
|
*/
|
||||||
function decodableCodecs() {
|
function decodableCodecsSync() {
|
||||||
const list = [];
|
const list = [];
|
||||||
if (hasWebCodecs()) {
|
if (hasWebCodecs()) {
|
||||||
list.push('av1', 'vp9', 'h264');
|
const allowAv1 = typeof RDVideo !== 'undefined'
|
||||||
|
&& RDVideo.av1ReliableOnRuntime
|
||||||
|
&& RDVideo.av1ReliableOnRuntime();
|
||||||
|
if (allowAv1 && !_blockedWireCodecs.has('av1')) list.push('av1');
|
||||||
|
list.push('vp9', 'h264');
|
||||||
} else if (hasMseH264()) {
|
} else if (hasMseH264()) {
|
||||||
list.push('h264');
|
list.push('h264');
|
||||||
}
|
}
|
||||||
@@ -112,6 +112,33 @@
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Probe WebCodecs support and return wire codecs safe to advertise.
|
||||||
|
* @returns {Promise<string[]>}
|
||||||
|
*/
|
||||||
|
function decodableCodecs() {
|
||||||
|
if (!_decodableCodecsPromise) {
|
||||||
|
_decodableCodecsPromise = (async () => {
|
||||||
|
if (typeof RDVideo !== 'undefined' && RDVideo.probeDecodableWireCodecs) {
|
||||||
|
try {
|
||||||
|
const probed = await RDVideo.probeDecodableWireCodecs();
|
||||||
|
return probed.filter((c) => !_blockedWireCodecs.has(c));
|
||||||
|
} catch { /* fall through */ }
|
||||||
|
}
|
||||||
|
return decodableCodecsSync();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
return _decodableCodecsPromise.then((list) =>
|
||||||
|
list.filter((c) => !_blockedWireCodecs.has(c))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockWireCodec(codec) {
|
||||||
|
if (!codec) return;
|
||||||
|
_blockedWireCodecs.add(String(codec).toLowerCase());
|
||||||
|
_decodableCodecsPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stub renderer that mirrors the subset of `RDRenderer` used by
|
* Stub renderer that mirrors the subset of `RDRenderer` used by
|
||||||
* `remote.js` (resize + scale mode). Frames are painted directly by
|
* `remote.js` (resize + scale mode). Frames are painted directly by
|
||||||
@@ -217,6 +244,8 @@
|
|||||||
// from `desktop_meta` and defaults to the legacy MJPEG path.
|
// from `desktop_meta` and defaults to the legacy MJPEG path.
|
||||||
this._activeFormat = 'jpeg';
|
this._activeFormat = 'jpeg';
|
||||||
this._codecString = null;
|
this._codecString = null;
|
||||||
|
this._codecFallbackDone = false;
|
||||||
|
this._videoRenderedFrames = 0;
|
||||||
this._video = null; // RDVideo instance (lazy)
|
this._video = null; // RDVideo instance (lazy)
|
||||||
this._gotKeyframe = false;
|
this._gotKeyframe = false;
|
||||||
this._keyframeRequestedAt = 0;
|
this._keyframeRequestedAt = 0;
|
||||||
@@ -268,6 +297,7 @@
|
|||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
this._setState('connecting');
|
this._setState('connecting');
|
||||||
|
this._codecFallbackDone = false;
|
||||||
this._emit('log', 'Opening CDAP desktop session…');
|
this._emit('log', 'Opening CDAP desktop session…');
|
||||||
console.log('[CDAP] connect()', this.deviceId);
|
console.log('[CDAP] connect()', this.deviceId);
|
||||||
|
|
||||||
@@ -495,6 +525,17 @@
|
|||||||
// ── Internal: WS lifecycle ───────────────────────────────────────
|
// ── Internal: WS lifecycle ───────────────────────────────────────
|
||||||
|
|
||||||
_handleOpen() {
|
_handleOpen() {
|
||||||
|
const self = this;
|
||||||
|
decodableCodecs().then(function (codecs) {
|
||||||
|
if (!self._ws || self._ws.readyState !== WebSocket.OPEN) return;
|
||||||
|
self._sendDesktopStart(codecs);
|
||||||
|
}).catch(function () {
|
||||||
|
if (!self._ws || self._ws.readyState !== WebSocket.OPEN) return;
|
||||||
|
self._sendDesktopStart(decodableCodecsSync());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendDesktopStart(codecs) {
|
||||||
const dpr = window.devicePixelRatio || 1;
|
const dpr = window.devicePixelRatio || 1;
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
const screenW = (window.screen && window.screen.width) || 1920;
|
const screenW = (window.screen && window.screen.width) || 1920;
|
||||||
@@ -515,7 +556,7 @@
|
|||||||
// sends a GPU-encoded video stream. Over plain HTTP this still
|
// sends a GPU-encoded video stream. Over plain HTTP this still
|
||||||
// includes H.264 (decoded via JMuxer/MSE), avoiding the slow
|
// includes H.264 (decoded via JMuxer/MSE), avoiding the slow
|
||||||
// MJPEG fallback. See decodableCodecs().
|
// MJPEG fallback. See decodableCodecs().
|
||||||
codecs: decodableCodecs(),
|
codecs: codecs,
|
||||||
video_codec: 'auto',
|
video_codec: 'auto',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -742,21 +783,45 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._closeVideoDecoder();
|
this._closeVideoDecoder();
|
||||||
|
this._videoRenderedFrames = 0;
|
||||||
const v = new RDVideo();
|
const v = new RDVideo();
|
||||||
this._video = v;
|
this._video = v;
|
||||||
v.onFrame = (frame) => this._drawVideoFrame(frame);
|
v.onFrame = (frame) => {
|
||||||
|
this._videoRenderedFrames++;
|
||||||
|
this._drawVideoFrame(frame);
|
||||||
|
};
|
||||||
v.onError = () => {
|
v.onError = () => {
|
||||||
// A decode error usually means we fed a delta frame before the
|
|
||||||
// first keyframe — ask the agent for a fresh keyframe.
|
|
||||||
this._gotKeyframe = false;
|
this._gotKeyframe = false;
|
||||||
this._requestKeyframe();
|
this._requestKeyframe();
|
||||||
};
|
};
|
||||||
v.init(codecName).catch((err) => {
|
v.onCodecFailed = (failedCodec) => {
|
||||||
|
this._handleCodecFailed(failedCodec);
|
||||||
|
};
|
||||||
|
v.onNeedKeyframe = () => this._requestKeyframe();
|
||||||
|
v.init(codecName, { codecString: this._codecString || null }).catch((err) => {
|
||||||
console.warn('[CDAP] video decoder init failed:', err && err.message);
|
console.warn('[CDAP] video decoder init failed:', err && err.message);
|
||||||
this._video = null;
|
this._handleCodecFailed(codecName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleCodecFailed(codec) {
|
||||||
|
const c = String(codec || '').toLowerCase();
|
||||||
|
if (!c || this._codecFallbackDone) return;
|
||||||
|
blockWireCodec(c);
|
||||||
|
this._codecFallbackDone = true;
|
||||||
|
this._closeVideoDecoder();
|
||||||
|
this._gotKeyframe = false;
|
||||||
|
this._emit('log', 'Codec ' + c.toUpperCase() + ' failed — reconnecting with VP9/H.264…');
|
||||||
|
const ws = this._ws;
|
||||||
|
const deviceId = this.deviceId;
|
||||||
|
try { if (ws) ws.close(4002, 'codec_fallback'); } catch { /* noop */ }
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this._state === 'disconnected' || this._state === 'error') {
|
||||||
|
this.connect().catch(() => {});
|
||||||
|
}
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feed one binary video frame to the decoder. The agent prefixes each
|
* Feed one binary video frame to the decoder. The agent prefixes each
|
||||||
* access unit with a single flag byte (bit0 = keyframe); the Node proxy
|
* access unit with a single flag byte (bit0 = keyframe); the Node proxy
|
||||||
@@ -788,7 +853,12 @@
|
|||||||
this._gotKeyframe = true;
|
this._gotKeyframe = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._video.decode({ data: payload, key: isKey, codec: this._activeFormat });
|
this._video.decode({
|
||||||
|
data: payload,
|
||||||
|
key: isKey,
|
||||||
|
codec: this._activeFormat,
|
||||||
|
codecString: this._codecString || null
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class RDClient {
|
|||||||
this._codecAbilities = null; // probed VideoDecoder support map
|
this._codecAbilities = null; // probed VideoDecoder support map
|
||||||
this._preferCodec = opts.preferCodec || 'Auto';
|
this._preferCodec = opts.preferCodec || 'Auto';
|
||||||
this._adaptivePaused = false; // true once the user picks codec/quality manually
|
this._adaptivePaused = false; // true once the user picks codec/quality manually
|
||||||
|
this._codecFallbackDone = false; // one automatic downgrade per session
|
||||||
|
|
||||||
// Relay state tracking
|
// Relay state tracking
|
||||||
this._relayFrameIdx = 0; // Counter for relay frames (debugging)
|
this._relayFrameIdx = 0; // Counter for relay frames (debugging)
|
||||||
@@ -1052,6 +1053,7 @@ class RDClient {
|
|||||||
*/
|
*/
|
||||||
_startSession() {
|
_startSession() {
|
||||||
this._setState('streaming');
|
this._setState('streaming');
|
||||||
|
this._codecFallbackDone = false;
|
||||||
this.conn.setConnected();
|
this.conn.setConnected();
|
||||||
|
|
||||||
// Enable file transfer
|
// Enable file transfer
|
||||||
@@ -1068,6 +1070,9 @@ class RDClient {
|
|||||||
this._sendPeerMessage(this.proto.buildMisc('refreshVideo', true));
|
this._sendPeerMessage(this.proto.buildMisc('refreshVideo', true));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this.video.onCodecFailed = (failedCodec) => {
|
||||||
|
this._handleCodecFallback(failedCodec);
|
||||||
|
};
|
||||||
|
|
||||||
// Request keyframe on resize/fullscreen to fix blur
|
// Request keyframe on resize/fullscreen to fix blur
|
||||||
this.renderer.onResizeRefresh = () => {
|
this.renderer.onResizeRefresh = () => {
|
||||||
@@ -1325,6 +1330,7 @@ class RDClient {
|
|||||||
this.audio.close();
|
this.audio.close();
|
||||||
this.fileTransfer.disable();
|
this.fileTransfer.disable();
|
||||||
this.conn.close();
|
this.conn.close();
|
||||||
|
this._codecFallbackDone = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Public Utility Methods ----
|
// ---- Public Utility Methods ----
|
||||||
@@ -1651,6 +1657,33 @@ class RDClient {
|
|||||||
this._emit('codec_changed', name);
|
this._emit('codec_changed', name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downgrade to the next working codec when WebCodecs fails at runtime.
|
||||||
|
* @param {string} failedCodec
|
||||||
|
*/
|
||||||
|
_handleCodecFallback(failedCodec) {
|
||||||
|
const failed = String(failedCodec || '').toLowerCase();
|
||||||
|
if (!failed || this._codecFallbackDone || this._state !== 'streaming') return;
|
||||||
|
this._codecFallbackDone = true;
|
||||||
|
|
||||||
|
if (!this._codecAbilities) this._codecAbilities = {};
|
||||||
|
this._codecAbilities[failed] = false;
|
||||||
|
|
||||||
|
const order = ['vp9', 'h264', 'vp8'];
|
||||||
|
let next = 'H264';
|
||||||
|
for (let i = 0; i < order.length; i++) {
|
||||||
|
const candidate = order[i];
|
||||||
|
if (candidate === failed) continue;
|
||||||
|
if (this._codecAbilities[candidate] !== false) {
|
||||||
|
next = candidate === 'h264' ? 'H264' : candidate.toUpperCase();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._emit('log', 'Codec ' + failed.toUpperCase() + ' failed — switching to ' + next);
|
||||||
|
this.setCodec(next);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request remote device restart
|
* Request remote device restart
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -65,9 +65,12 @@ class RDVideo {
|
|||||||
this._codecConfig = null;
|
this._codecConfig = null;
|
||||||
/** @type {boolean} Whether we already retried software decoding after a hardware failure */
|
/** @type {boolean} Whether we already retried software decoding after a hardware failure */
|
||||||
this._softwareRetry = false;
|
this._softwareRetry = false;
|
||||||
/** @type {number} Last decoder error timestamp (ms) for throttled recovery */
|
/** @type {Function|null} Called when decoder cannot recover (codec switch needed) */
|
||||||
this._lastDecodeErrorTime = 0;
|
this.onCodecFailed = null;
|
||||||
}
|
/** @type {number} Decode attempts since last successful frame */
|
||||||
|
this._decodeErrorsSinceFrame = 0;
|
||||||
|
/** @type {boolean} Whether AV1 description was applied from a keyframe */
|
||||||
|
this._av1DescriptionApplied = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if hardware WebCodecs is supported (requires secure context)
|
* Check if hardware WebCodecs is supported (requires secure context)
|
||||||
@@ -85,6 +88,56 @@ class RDVideo {
|
|||||||
return window.isSecureContext === true;
|
return window.isSecureContext === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** WebKitGTK / Safari — AV1 HW decode is often broken; prefer software. */
|
||||||
|
static isWebKit() {
|
||||||
|
const ua = navigator.userAgent || '';
|
||||||
|
return /AppleWebKit/i.test(ua) && !/Edg\//.test(ua) && !/Chrome\//.test(ua);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Chromium / WebView2 — full WebCodecs stack (incl. AV1 on recent builds). */
|
||||||
|
static isChromiumWebView() {
|
||||||
|
const ua = navigator.userAgent || '';
|
||||||
|
return /Chrome\//.test(ua) || /Edg\//.test(ua);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tauri RdClient desktop shell (WebKitGTK on Linux, WKWebView on macOS, WebView2 on Windows). */
|
||||||
|
static isRdClientDesktop() {
|
||||||
|
return window.__BETTERDESK_RDCLIENT_DESKTOP__ === true
|
||||||
|
|| !!(window.__TAURI__ && window.__TAURI__.core);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Linux WebKitGTK — WebCodecs AV1 is advertised but fails at decode time. */
|
||||||
|
static isWebKitGTK() {
|
||||||
|
if (RDVideo.isChromiumWebView()) return false;
|
||||||
|
const ua = navigator.userAgent || '';
|
||||||
|
if (!/AppleWebKit/i.test(ua)) return false;
|
||||||
|
if (/Linux/i.test(ua)) return true;
|
||||||
|
return RDVideo.isRdClientDesktop() && !/Macintosh|Windows|CrOS/i.test(ua);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether AV1 should be offered to the encoder. WebKit runtimes often pass
|
||||||
|
* VideoDecoder.isConfigSupported for AV1 but fail with "Decode error" at runtime.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
static av1ReliableOnRuntime() {
|
||||||
|
if (RDVideo.isWebKitGTK()) return false;
|
||||||
|
if (RDVideo.isWebKit() && !RDVideo.isChromiumWebView()) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default hardwareAcceleration for a logical codec on this runtime.
|
||||||
|
* @param {string} codecName
|
||||||
|
* @returns {string|undefined}
|
||||||
|
*/
|
||||||
|
static defaultAcceleration(codecName) {
|
||||||
|
if (codecName === 'av1' && RDVideo.isWebKit()) {
|
||||||
|
return 'prefer-software';
|
||||||
|
}
|
||||||
|
return 'prefer-hardware';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if JMuxer fallback is available
|
* Check if JMuxer fallback is available
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
@@ -102,7 +155,7 @@ class RDVideo {
|
|||||||
const map = {
|
const map = {
|
||||||
vp9: ['vp09.00.10.08', 'vp09.00.40.08', 'vp09.00.50.08'],
|
vp9: ['vp09.00.10.08', 'vp09.00.40.08', 'vp09.00.50.08'],
|
||||||
h264: ['avc1.640028', 'avc1.4d4028', 'avc1.42E01E'],
|
h264: ['avc1.640028', 'avc1.4d4028', 'avc1.42E01E'],
|
||||||
av1: ['av01.0.08M.08', 'av01.0.05M.08', 'av01.0.04M.08', 'av01.0.01M.08', 'av01.0.15M.08'],
|
av1: ['av01.0.04M.08', 'av01.0.08M.08', 'av01.0.05M.08', 'av01.0.01M.08', 'av01.0.15M.08'],
|
||||||
vp8: ['vp8'],
|
vp8: ['vp8'],
|
||||||
h265: ['hev1.1.6.L93.B0', 'hvc1.1.6.L93.B0']
|
h265: ['hev1.1.6.L93.B0', 'hvc1.1.6.L93.B0']
|
||||||
};
|
};
|
||||||
@@ -115,12 +168,15 @@ class RDVideo {
|
|||||||
* @param {string} [accel] hardwareAcceleration preference
|
* @param {string} [accel] hardwareAcceleration preference
|
||||||
* @returns {Promise<Object|null>} supported VideoDecoderConfig
|
* @returns {Promise<Object|null>} supported VideoDecoderConfig
|
||||||
*/
|
*/
|
||||||
static async resolveCodecConfig(codecName, accel) {
|
static async resolveCodecConfig(codecName, accel, explicitCodec) {
|
||||||
const candidates = RDVideo.codecCandidates(codecName);
|
const candidates = explicitCodec
|
||||||
|
? [explicitCodec].concat(RDVideo.codecCandidates(codecName).filter((c) => c !== explicitCodec))
|
||||||
|
: RDVideo.codecCandidates(codecName);
|
||||||
if (!candidates.length) return null;
|
if (!candidates.length) return null;
|
||||||
|
|
||||||
const probes = accel
|
const baseAccel = accel || RDVideo.defaultAcceleration(codecName);
|
||||||
? [{ hardwareAcceleration: accel }, {}]
|
const probes = baseAccel
|
||||||
|
? [{ hardwareAcceleration: baseAccel }, { hardwareAcceleration: 'prefer-software' }, {}]
|
||||||
: [{ hardwareAcceleration: 'prefer-hardware' }, { hardwareAcceleration: 'prefer-software' }, {}];
|
: [{ hardwareAcceleration: 'prefer-hardware' }, { hardwareAcceleration: 'prefer-software' }, {}];
|
||||||
|
|
||||||
for (const codec of candidates) {
|
for (const codec of candidates) {
|
||||||
@@ -158,17 +214,44 @@ class RDVideo {
|
|||||||
const names = ['vp9', 'h264', 'av1', 'vp8', 'h265'];
|
const names = ['vp9', 'h264', 'av1', 'vp8', 'h265'];
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
result[name] = !!(await RDVideo.resolveCodecConfig(name));
|
if (name === 'av1' && !RDVideo.av1ReliableOnRuntime()) {
|
||||||
|
result[name] = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result[name] = !!(await RDVideo.resolveCodecConfig(name, RDVideo.defaultAcceleration(name)));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordered wire codecs safe to advertise to the agent (most efficient first).
|
||||||
|
* AV1 is omitted when the runtime cannot decode it reliably (WebKit HW bugs).
|
||||||
|
* @returns {Promise<string[]>}
|
||||||
|
*/
|
||||||
|
static async probeDecodableWireCodecs() {
|
||||||
|
const list = [];
|
||||||
|
if (!RDVideo.isSupported()) {
|
||||||
|
if (RDVideo.isJMuxerAvailable()) list.push('h264');
|
||||||
|
list.push('webp', 'jpeg');
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
const support = await RDVideo.getSupportedCodecs();
|
||||||
|
if (support.av1 && RDVideo.av1ReliableOnRuntime()) list.push('av1');
|
||||||
|
if (support.vp9) list.push('vp9');
|
||||||
|
if (support.h264) list.push('h264');
|
||||||
|
list.push('webp', 'jpeg');
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize decoder for a specific codec.
|
* Initialize decoder for a specific codec.
|
||||||
* Uses WebCodecs if available, otherwise falls back to JMuxer for H.264.
|
* Uses WebCodecs if available, otherwise falls back to JMuxer for H.264.
|
||||||
* @param {string} codecName - vp9, h264, av1, vp8
|
* @param {string} codecName - vp9, h264, av1, vp8
|
||||||
|
* @param {Object} [opts]
|
||||||
|
* @param {string} [opts.codecString] WebCodecs codec string from agent
|
||||||
*/
|
*/
|
||||||
async init(codecName) {
|
async init(codecName, opts) {
|
||||||
|
opts = opts || {};
|
||||||
if (this.decoder || this._jmuxer) {
|
if (this.decoder || this._jmuxer) {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
@@ -207,16 +290,18 @@ class RDVideo {
|
|||||||
throw new Error(`Unsupported codec: ${codecName}`);
|
throw new Error(`Unsupported codec: ${codecName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Choose acceleration: prefer hardware, but fall back to software after
|
const accel = this._softwareRetry
|
||||||
// a hardware decode failure (some Linux/Chrome setups have flaky
|
? 'prefer-software'
|
||||||
// hardware H.264 paths that silently stop producing frames).
|
: RDVideo.defaultAcceleration(codecName);
|
||||||
const accel = this._softwareRetry ? 'prefer-software' : 'prefer-hardware';
|
|
||||||
|
|
||||||
const resolved = await RDVideo.resolveCodecConfig(codecName, accel);
|
const resolved = await RDVideo.resolveCodecConfig(codecName, accel, opts.codecString || null);
|
||||||
if (!resolved || !resolved.codec) {
|
if (!resolved || !resolved.codec) {
|
||||||
throw new Error(`Codec ${codecName} not supported by browser`);
|
throw new Error(`Codec ${codecName} not supported by browser`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._softwareRetry = resolved.hardwareAcceleration === 'prefer-software'
|
||||||
|
|| resolved.hardwareAcceleration === 'no-preference';
|
||||||
|
|
||||||
this.decoder = new VideoDecoder({
|
this.decoder = new VideoDecoder({
|
||||||
output: (frame) => this._handleDecodedFrame(frame),
|
output: (frame) => this._handleDecodedFrame(frame),
|
||||||
error: (err) => this._handleError(err)
|
error: (err) => this._handleError(err)
|
||||||
@@ -228,8 +313,16 @@ class RDVideo {
|
|||||||
hardwareAcceleration: resolved.hardwareAcceleration || accel,
|
hardwareAcceleration: resolved.hardwareAcceleration || accel,
|
||||||
optimizeForLatency: true
|
optimizeForLatency: true
|
||||||
};
|
};
|
||||||
|
if (opts.description) {
|
||||||
|
this._codecConfig.description = opts.description;
|
||||||
|
}
|
||||||
this.decoder.configure(this._codecConfig);
|
this.decoder.configure(this._codecConfig);
|
||||||
|
|
||||||
|
console.log('[RDVideo] Configured', codecName, this._codecConfig.codec,
|
||||||
|
'hw=' + (this._codecConfig.hardwareAcceleration || 'default'),
|
||||||
|
'runtime=' + (RDVideo.isWebKitGTK() ? 'webkitgtk'
|
||||||
|
: (RDVideo.isChromiumWebView() ? 'chromium' : 'other')));
|
||||||
|
|
||||||
this.fallbackMode = false;
|
this.fallbackMode = false;
|
||||||
this.currentCodec = codecName;
|
this.currentCodec = codecName;
|
||||||
this.frameCount = 0;
|
this.frameCount = 0;
|
||||||
@@ -239,6 +332,8 @@ class RDVideo {
|
|||||||
// frame. Drop deltas until one arrives and proactively request one.
|
// frame. Drop deltas until one arrives and proactively request one.
|
||||||
this._needKeyframe = true;
|
this._needKeyframe = true;
|
||||||
this._decodeInputCount = 0;
|
this._decodeInputCount = 0;
|
||||||
|
this._decodeErrorsSinceFrame = 0;
|
||||||
|
this._av1DescriptionApplied = false;
|
||||||
if (this.onNeedKeyframe) {
|
if (this.onNeedKeyframe) {
|
||||||
this.onNeedKeyframe();
|
this.onNeedKeyframe();
|
||||||
}
|
}
|
||||||
@@ -588,7 +683,24 @@ class RDVideo {
|
|||||||
|
|
||||||
// Switch codec if needed
|
// Switch codec if needed
|
||||||
if (frameData.codec && frameData.codec !== this.currentCodec) {
|
if (frameData.codec && frameData.codec !== this.currentCodec) {
|
||||||
await this.init(frameData.codec);
|
await this.init(frameData.codec, { codecString: frameData.codecString });
|
||||||
|
}
|
||||||
|
|
||||||
|
// AV1: apply codec description from the first keyframe (WebKitGTK needs this).
|
||||||
|
if (this.currentCodec === 'av1' && frameData.key && !this._av1DescriptionApplied) {
|
||||||
|
const desc = RDVideo.av1DescriptionFromKeyframe(frameData.data);
|
||||||
|
if (desc) {
|
||||||
|
try {
|
||||||
|
this._codecConfig.description = desc;
|
||||||
|
if (this.decoder && this.decoder.state === 'configured') {
|
||||||
|
this.decoder.configure(this._codecConfig);
|
||||||
|
}
|
||||||
|
this._av1DescriptionApplied = true;
|
||||||
|
this._needKeyframe = false;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RDVideo] AV1 description configure failed:', e && e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// JMuxer fallback mode
|
// JMuxer fallback mode
|
||||||
@@ -710,6 +822,7 @@ class RDVideo {
|
|||||||
*/
|
*/
|
||||||
_handleDecodedFrame(frame) {
|
_handleDecodedFrame(frame) {
|
||||||
this.frameCount++;
|
this.frameCount++;
|
||||||
|
this._decodeErrorsSinceFrame = 0;
|
||||||
// Record timestamp so getStats() can report real FPS on the WebCodecs
|
// Record timestamp so getStats() can report real FPS on the WebCodecs
|
||||||
// path (previously only the JMuxer fallback fed this array, so HTTPS
|
// path (previously only the JMuxer fallback fed this array, so HTTPS
|
||||||
// sessions always reported 0 FPS).
|
// sessions always reported 0 FPS).
|
||||||
@@ -742,6 +855,7 @@ class RDVideo {
|
|||||||
*/
|
*/
|
||||||
_handleError(err) {
|
_handleError(err) {
|
||||||
console.error('[RDVideo] Decoder error:', err && err.message ? err.message : err);
|
console.error('[RDVideo] Decoder error:', err && err.message ? err.message : err);
|
||||||
|
this._decodeErrorsSinceFrame++;
|
||||||
if (this.onError) {
|
if (this.onError) {
|
||||||
this.onError(err);
|
this.onError(err);
|
||||||
}
|
}
|
||||||
@@ -750,6 +864,15 @@ class RDVideo {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebKitGTK / Safari: AV1 passes isConfigSupported but fails at runtime — switch codec immediately.
|
||||||
|
if (this.currentCodec === 'av1' && !RDVideo.av1ReliableOnRuntime()) {
|
||||||
|
console.warn('[RDVideo] AV1 decode failed on WebKit runtime — requesting VP9/H.264 fallback');
|
||||||
|
if (this.onCodecFailed) {
|
||||||
|
this.onCodecFailed(this.currentCodec, err);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Throttle recovery attempts to once per second.
|
// Throttle recovery attempts to once per second.
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
if (now - this._lastDecodeErrorTime < 1000) {
|
if (now - this._lastDecodeErrorTime < 1000) {
|
||||||
@@ -761,9 +884,14 @@ class RDVideo {
|
|||||||
if (!this._softwareRetry) {
|
if (!this._softwareRetry) {
|
||||||
this._softwareRetry = true;
|
this._softwareRetry = true;
|
||||||
console.warn('[RDVideo] Rebuilding decoder with software decoding fallback');
|
console.warn('[RDVideo] Rebuilding decoder with software decoding fallback');
|
||||||
|
} else if (this._decodeErrorsSinceFrame > 8 && this.onCodecFailed) {
|
||||||
|
console.warn('[RDVideo] Codec', this.currentCodec, 'unrecoverable — requesting fallback');
|
||||||
|
this.onCodecFailed(this.currentCodec, err);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._needKeyframe = true;
|
this._needKeyframe = true;
|
||||||
|
this._av1DescriptionApplied = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.decoder && this.decoder.state !== 'closed') {
|
if (this.decoder && this.decoder.state !== 'closed') {
|
||||||
@@ -774,15 +902,108 @@ class RDVideo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const codecName = this.currentCodec;
|
const codecName = this.currentCodec;
|
||||||
|
const savedCodecString = this._codecConfig && this._codecConfig.codec;
|
||||||
this.decoder = null;
|
this.decoder = null;
|
||||||
this.initialized = false;
|
this.initialized = false;
|
||||||
if (codecName) {
|
if (codecName) {
|
||||||
this.init(codecName).catch((e) => {
|
this.init(codecName, { codecString: savedCodecString }).catch((e) => {
|
||||||
console.error('[RDVideo] Decoder rebuild failed:', e && e.message ? e.message : e);
|
console.error('[RDVideo] Decoder rebuild failed:', e && e.message ? e.message : e);
|
||||||
|
if (this.onCodecFailed) {
|
||||||
|
this.onCodecFailed(codecName, e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build WebCodecs AV1CodecConfigurationRecord (av1C) from a keyframe OBU stream.
|
||||||
|
* @param {Uint8Array} data
|
||||||
|
* @returns {Uint8Array|null}
|
||||||
|
*/
|
||||||
|
static av1DescriptionFromKeyframe(data) {
|
||||||
|
if (!data || data.length < 2) return null;
|
||||||
|
let i = 0;
|
||||||
|
let fullSeqObu = null;
|
||||||
|
let seqPayload = null;
|
||||||
|
while (i < data.length) {
|
||||||
|
const obuStart = i;
|
||||||
|
const hdr = data[i++];
|
||||||
|
const obuType = (hdr >> 3) & 0x0F;
|
||||||
|
const extFlag = (hdr >> 2) & 0x01;
|
||||||
|
const hasSize = (hdr >> 1) & 0x01;
|
||||||
|
if (extFlag === 1 && i < data.length) i++;
|
||||||
|
let size;
|
||||||
|
if (hasSize === 1) {
|
||||||
|
const parsed = RDVideo._readLeb128(data, i);
|
||||||
|
if (!parsed) break;
|
||||||
|
size = parsed.value;
|
||||||
|
i += parsed.bytes;
|
||||||
|
} else {
|
||||||
|
size = data.length - i;
|
||||||
|
}
|
||||||
|
if (i + size > data.length) break;
|
||||||
|
if (obuType === 1) {
|
||||||
|
fullSeqObu = data.subarray(obuStart, i + size);
|
||||||
|
seqPayload = data.subarray(i, i + size);
|
||||||
|
}
|
||||||
|
i += size;
|
||||||
|
}
|
||||||
|
if (!fullSeqObu || !seqPayload || seqPayload.length < 4) return null;
|
||||||
|
|
||||||
|
const br = { data: seqPayload, pos: 0 };
|
||||||
|
const read = (n) => RDVideo._readBits(br, n);
|
||||||
|
const profile = read(3);
|
||||||
|
const level = read(5);
|
||||||
|
const tier = read(1);
|
||||||
|
const highBitdepth = read(1);
|
||||||
|
const twelveBit = read(1);
|
||||||
|
const monochrome = read(1);
|
||||||
|
const subsamplingX = read(1);
|
||||||
|
const subsamplingY = read(1);
|
||||||
|
const samplePos = read(2);
|
||||||
|
read(3); // reserved
|
||||||
|
|
||||||
|
const out = new Uint8Array(4 + fullSeqObu.length);
|
||||||
|
out[0] = 0x81;
|
||||||
|
out[1] = (profile << 5) | (level & 0x1F);
|
||||||
|
out[2] = ((level >> 5) & 0xFF) | (tier << 7) | (highBitdepth << 6)
|
||||||
|
| (twelveBit << 5) | (monochrome << 4) | (subsamplingX << 3)
|
||||||
|
| (subsamplingY << 2) | (samplePos >> 1);
|
||||||
|
out[3] = ((samplePos & 1) << 7);
|
||||||
|
out.set(fullSeqObu, 4);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
static _readLeb128(b, start) {
|
||||||
|
let v = 0;
|
||||||
|
for (let i = 0; i < 8 && start + i < b.length; i++) {
|
||||||
|
const byte = b[start + i];
|
||||||
|
v |= (byte & 0x7F) << (7 * i);
|
||||||
|
if ((byte & 0x80) === 0) {
|
||||||
|
return { value: v, bytes: i + 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
static _readBits(br, n) {
|
||||||
|
let v = 0;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const bytePos = br.pos >> 3;
|
||||||
|
if (bytePos >= br.data.length) {
|
||||||
|
br.pos++;
|
||||||
|
v <<= 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bit = (br.data[bytePos] >> (7 - (br.pos & 7))) & 1;
|
||||||
|
v = (v << 1) | bit;
|
||||||
|
br.pos++;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flush pending frames
|
* Flush pending frames
|
||||||
*/
|
*/
|
||||||
@@ -874,6 +1095,8 @@ class RDVideo {
|
|||||||
this.fallbackMode = false;
|
this.fallbackMode = false;
|
||||||
this.currentCodec = null;
|
this.currentCodec = null;
|
||||||
this.initialized = false;
|
this.initialized = false;
|
||||||
|
this._softwareRetry = false;
|
||||||
|
this._av1DescriptionApplied = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ var RdClientSecureStore = (function () {
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var DB_NAME = 'betterdesk-rdclient';
|
var DB_NAME = 'betterdesk-rdclient';
|
||||||
var DB_VERSION = 1;
|
var DB_VERSION = 2;
|
||||||
var STORE = 'vault';
|
var STORE = 'vault';
|
||||||
var KEY_ID = 'device-key';
|
var KEY_ID = 'device-key';
|
||||||
var CRED_ID = 'credentials';
|
var CRED_ID = 'credentials';
|
||||||
@@ -198,13 +198,84 @@ var RdClientSecureStore = (function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function peerKey(deviceId) {
|
||||||
|
return 'peer:' + String(deviceId || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePeerPassword(deviceId, password) {
|
||||||
|
if (!deviceId || !password || !window.crypto || !window.crypto.subtle) return;
|
||||||
|
var db = await openDb();
|
||||||
|
try {
|
||||||
|
var key = await getOrCreateDeviceKey(db);
|
||||||
|
var encrypted = await encryptPassword(key, password);
|
||||||
|
await idbPut(db, peerKey(deviceId), {
|
||||||
|
encrypted: encrypted,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPeerPassword(deviceId) {
|
||||||
|
if (!deviceId || !window.crypto || !window.crypto.subtle || !window.indexedDB) return '';
|
||||||
|
var db = await openDb();
|
||||||
|
try {
|
||||||
|
var record = await idbGet(db, peerKey(deviceId));
|
||||||
|
if (!record || !record.encrypted) return '';
|
||||||
|
var key = await getOrCreateDeviceKey(db);
|
||||||
|
return await decryptPassword(key, record.encrypted);
|
||||||
|
} catch (_) {
|
||||||
|
return '';
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearPeerPassword(deviceId) {
|
||||||
|
if (!deviceId || !window.indexedDB) return;
|
||||||
|
var db = await openDb();
|
||||||
|
try {
|
||||||
|
await idbDelete(db, peerKey(deviceId));
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearAllPeerPasswords() {
|
||||||
|
if (!window.indexedDB) return;
|
||||||
|
var db = await openDb();
|
||||||
|
try {
|
||||||
|
var tx = db.transaction(STORE, 'readwrite');
|
||||||
|
var store = tx.objectStore(STORE);
|
||||||
|
var req = store.openCursor();
|
||||||
|
await new Promise(function (resolve, reject) {
|
||||||
|
req.onsuccess = function () {
|
||||||
|
var cursor = req.result;
|
||||||
|
if (!cursor) { resolve(); return; }
|
||||||
|
if (String(cursor.key).indexOf('peer:') === 0) {
|
||||||
|
cursor.delete();
|
||||||
|
}
|
||||||
|
cursor.continue();
|
||||||
|
};
|
||||||
|
req.onerror = function () { reject(req.error); };
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
saveCredentials: saveCredentials,
|
saveCredentials: saveCredentials,
|
||||||
loadCredentials: loadCredentials,
|
loadCredentials: loadCredentials,
|
||||||
clearCredentials: clearCredentials,
|
clearCredentials: clearCredentials,
|
||||||
clearStoredPassword: clearStoredPassword,
|
clearStoredPassword: clearStoredPassword,
|
||||||
hasStoredPassword: hasStoredPassword,
|
hasStoredPassword: hasStoredPassword,
|
||||||
loadLastUsername: loadLastUsername
|
loadLastUsername: loadLastUsername,
|
||||||
|
savePeerPassword: savePeerPassword,
|
||||||
|
loadPeerPassword: loadPeerPassword,
|
||||||
|
clearPeerPassword: clearPeerPassword,
|
||||||
|
clearAllPeerPasswords: clearAllPeerPasswords
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -419,6 +419,7 @@
|
|||||||
renderGrid();
|
renderGrid();
|
||||||
showState('grid');
|
showState('grid');
|
||||||
}
|
}
|
||||||
|
syncDesktopViewport();
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRdClientDesktop() {
|
function isRdClientDesktop() {
|
||||||
@@ -426,12 +427,36 @@
|
|||||||
return !!(window.__TAURI__ && window.__TAURI__.core && typeof window.__TAURI__.core.invoke === 'function');
|
return !!(window.__TAURI__ && window.__TAURI__.core && typeof window.__TAURI__.core.invoke === 'function');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncDesktopViewport() {
|
||||||
|
var h = window.innerHeight;
|
||||||
|
if (h < 1) return;
|
||||||
|
document.documentElement.style.setProperty('--rd-desk-vh', h + 'px');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSidebarScroll() {
|
||||||
|
var sidebar = document.getElementById('rd-desk-sidebar');
|
||||||
|
if (!sidebar || document.getElementById('rd-desk-sidebar-scroll')) return;
|
||||||
|
var scroll = document.createElement('div');
|
||||||
|
scroll.className = 'rd-desk-sidebar-scroll';
|
||||||
|
scroll.id = 'rd-desk-sidebar-scroll';
|
||||||
|
while (sidebar.firstChild) {
|
||||||
|
scroll.appendChild(sidebar.firstChild);
|
||||||
|
}
|
||||||
|
sidebar.appendChild(scroll);
|
||||||
|
}
|
||||||
|
|
||||||
function markDesktopLayout() {
|
function markDesktopLayout() {
|
||||||
|
syncDesktopViewport();
|
||||||
|
ensureSidebarScroll();
|
||||||
if (!isRdClientDesktop()) return;
|
if (!isRdClientDesktop()) return;
|
||||||
document.documentElement.classList.add('rd-desk-desktop');
|
document.documentElement.classList.add('rd-desk-desktop');
|
||||||
document.body.classList.add('rd-desk-desktop');
|
document.body.classList.add('rd-desk-desktop');
|
||||||
var app = document.getElementById('rd-desk-app');
|
var app = document.getElementById('rd-desk-app');
|
||||||
if (app) app.classList.add('rd-desk-desktop');
|
if (app) app.classList.add('rd-desk-desktop');
|
||||||
|
if (!window.__rdDeskViewportBound) {
|
||||||
|
window.__rdDeskViewportBound = true;
|
||||||
|
window.addEventListener('resize', syncDesktopViewport);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDevicesPanelCollapsed(collapsed) {
|
function setDevicesPanelCollapsed(collapsed) {
|
||||||
@@ -708,6 +733,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function populateLanguageSelect() {
|
||||||
|
var sel = document.getElementById('rd-desk-lang');
|
||||||
|
if (!sel || sel.options.length > 0) return;
|
||||||
|
var langs = (window.BetterDesk && window.BetterDesk.availableLanguages) || [];
|
||||||
|
langs.forEach(function (lang) {
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.value = lang.code;
|
||||||
|
opt.textContent = lang.native || lang.name || lang.code;
|
||||||
|
if (lang.code === (window.BetterDesk && window.BetterDesk.lang)) opt.selected = true;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindDesktopSettings() {
|
||||||
|
if (!isRdClientDesktop()) return;
|
||||||
|
var btn = document.getElementById('rd-desk-settings');
|
||||||
|
if (!btn) return;
|
||||||
|
btn.style.display = '';
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var invoke = window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke;
|
||||||
|
if (invoke) invoke('open_settings').catch(function () { /* ignore */ });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function startAutoRefresh() {
|
function startAutoRefresh() {
|
||||||
if (refreshTimer) clearInterval(refreshTimer);
|
if (refreshTimer) clearInterval(refreshTimer);
|
||||||
refreshTimer = setInterval(function () { loadAll(true); }, REFRESH_MS);
|
refreshTimer = setInterval(function () { loadAll(true); }, REFRESH_MS);
|
||||||
@@ -715,6 +764,8 @@
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
markDesktopLayout();
|
markDesktopLayout();
|
||||||
|
populateLanguageSelect();
|
||||||
|
bindDesktopSettings();
|
||||||
renderBrandLogo();
|
renderBrandLogo();
|
||||||
renderUser();
|
renderUser();
|
||||||
bindUi();
|
bindUi();
|
||||||
|
|||||||
@@ -138,6 +138,7 @@
|
|||||||
this.connectionOverlay = panel.querySelector('.session-connection-overlay');
|
this.connectionOverlay = panel.querySelector('.session-connection-overlay');
|
||||||
this.passwordOverlay = panel.querySelector('.session-password-overlay');
|
this.passwordOverlay = panel.querySelector('.session-password-overlay');
|
||||||
this.passwordInput = panel.querySelector('.session-password-input');
|
this.passwordInput = panel.querySelector('.session-password-input');
|
||||||
|
this.rememberPeerCheckbox = panel.querySelector('.session-remember-peer-checkbox');
|
||||||
this.loginError = panel.querySelector('.session-login-error');
|
this.loginError = panel.querySelector('.session-login-error');
|
||||||
this.statusText = panel.querySelector('.session-status-text');
|
this.statusText = panel.querySelector('.session-status-text');
|
||||||
this.overlayActions = panel.querySelector('.session-overlay-actions');
|
this.overlayActions = panel.querySelector('.session-overlay-actions');
|
||||||
@@ -545,6 +546,14 @@
|
|||||||
session.passwordOverlay.style.display = 'flex';
|
session.passwordOverlay.style.display = 'flex';
|
||||||
session.loginError.style.display = 'none';
|
session.loginError.style.display = 'none';
|
||||||
session.passwordInput.value = '';
|
session.passwordInput.value = '';
|
||||||
|
if (window.RdClientSecureStore && session.deviceId) {
|
||||||
|
window.RdClientSecureStore.loadPeerPassword(session.deviceId).then(function (saved) {
|
||||||
|
if (saved) {
|
||||||
|
session.passwordInput.value = saved;
|
||||||
|
if (session.rememberPeerCheckbox) session.rememberPeerCheckbox.checked = true;
|
||||||
|
}
|
||||||
|
}).catch(function () { /* ignore */ });
|
||||||
|
}
|
||||||
if (isActive(session)) session.passwordInput.focus();
|
if (isActive(session)) session.passwordInput.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -583,6 +592,14 @@
|
|||||||
session.passwordOverlay.style.display = 'none';
|
session.passwordOverlay.style.display = 'none';
|
||||||
session.tfaOverlay.style.display = 'none';
|
session.tfaOverlay.style.display = 'none';
|
||||||
session.passwordInput.blur();
|
session.passwordInput.blur();
|
||||||
|
if (window.RdClientSecureStore && session.rememberPeerCheckbox) {
|
||||||
|
var pw = session.passwordInput.value;
|
||||||
|
if (session.rememberPeerCheckbox.checked && pw) {
|
||||||
|
window.RdClientSecureStore.savePeerPassword(session.deviceId, pw).catch(function () { /* ignore */ });
|
||||||
|
} else if (!session.rememberPeerCheckbox.checked) {
|
||||||
|
window.RdClientSecureStore.clearPeerPassword(session.deviceId).catch(function () { /* ignore */ });
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
c.on('session_start', () => {
|
c.on('session_start', () => {
|
||||||
@@ -1708,7 +1725,21 @@
|
|||||||
|
|
||||||
// ---- Initialize ----
|
// ---- Initialize ----
|
||||||
|
|
||||||
|
function populateViewerLanguageSelect() {
|
||||||
|
var sel = document.getElementById('viewer-language-select');
|
||||||
|
if (!sel || sel.options.length > 0) return;
|
||||||
|
var langs = (window.BetterDesk && window.BetterDesk.availableLanguages) || [];
|
||||||
|
langs.forEach(function (lang) {
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.value = lang.code;
|
||||||
|
opt.textContent = lang.native || lang.name || lang.code;
|
||||||
|
if (lang.code === (window.BetterDesk && window.BetterDesk.lang)) opt.selected = true;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
populateViewerLanguageSelect();
|
||||||
const deviceId = window.__initialDeviceId;
|
const deviceId = window.__initialDeviceId;
|
||||||
const deviceName = window.__initialDeviceName || '';
|
const deviceName = window.__initialDeviceName || '';
|
||||||
if (deviceId) {
|
if (deviceId) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const config = require('../config/config');
|
||||||
const db = require('../services/database');
|
const db = require('../services/database');
|
||||||
const bdRelay = require('../services/bdRelay');
|
const bdRelay = require('../services/bdRelay');
|
||||||
const brandingService = require('../services/brandingService');
|
const brandingService = require('../services/brandingService');
|
||||||
@@ -147,6 +148,19 @@ function buildSessionHistory(entries, limit) {
|
|||||||
.slice(0, limit);
|
.slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// GET /api/bd/server-info — Public panel identity (RdClient URL validation)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
router.get('/server-info', (_req, res) => {
|
||||||
|
res.json({
|
||||||
|
ok: true,
|
||||||
|
product: 'betterdesk-panel',
|
||||||
|
version: config.appVersion,
|
||||||
|
panel_name: config.appName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Middleware — authenticate desktop client via access token or session cookie
|
// Middleware — authenticate desktop client via access token or session cookie
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const { requireAuth, requireAdmin } = require('../middleware/auth');
|
|||||||
const keyService = require('../services/keyService');
|
const keyService = require('../services/keyService');
|
||||||
const bundleService = require('../services/agentBundleService');
|
const bundleService = require('../services/agentBundleService');
|
||||||
const buildWorker = require('../services/agentBuildWorker');
|
const buildWorker = require('../services/agentBuildWorker');
|
||||||
|
const rdclientBuildWorker = require('../services/rdclientBuildWorker');
|
||||||
const db = require('../services/database');
|
const db = require('../services/database');
|
||||||
const config = require('../config/config');
|
const config = require('../config/config');
|
||||||
const brandingService = require('../services/brandingService');
|
const brandingService = require('../services/brandingService');
|
||||||
@@ -52,6 +53,7 @@ function serializeBundle(row) {
|
|||||||
created_at: row.created_at,
|
created_at: row.created_at,
|
||||||
updated_at: row.updated_at,
|
updated_at: row.updated_at,
|
||||||
download_url: `/d/${publicId}`,
|
download_url: `/d/${publicId}`,
|
||||||
|
product_type: row.product_type || 'agent',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +191,11 @@ router.post('/api/generator/bundles', requireAuth, requireAdmin, async (req, res
|
|||||||
if (!name) {
|
if (!name) {
|
||||||
return res.status(400).json({ success: false, error: req.t('generator.errors.name_required') });
|
return res.status(400).json({ success: false, error: req.t('generator.errors.name_required') });
|
||||||
}
|
}
|
||||||
const { valid, errors, normalized: base } = bundleService.validateBranding(req.body.branding || {});
|
const productType = String(req.body.product_type || 'agent').toLowerCase() === 'rdclient' ? 'rdclient' : 'agent';
|
||||||
|
const validateFn = productType === 'rdclient'
|
||||||
|
? bundleService.validateRdclientBranding
|
||||||
|
: bundleService.validateBranding;
|
||||||
|
const { valid, errors, normalized: base } = validateFn(req.body.branding || {});
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return res.status(400).json({ success: false, error: req.t('generator.errors.validation_failed'), errors, details: errors });
|
return res.status(400).json({ success: false, error: req.t('generator.errors.validation_failed'), errors, details: errors });
|
||||||
}
|
}
|
||||||
@@ -207,9 +213,13 @@ router.post('/api/generator/bundles', requireAuth, requireAdmin, async (req, res
|
|||||||
details: [slugResult.error],
|
details: [slugResult.error],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const normalized = finalizeBundleBranding(base);
|
const normalized = productType === 'rdclient'
|
||||||
normalized.bundle_id = bundleId;
|
? { ...base, bundle_id: bundleId, server_url: base.panel_url }
|
||||||
normalized.product_name = normalized.company_name || 'BetterDesk Support';
|
: finalizeBundleBranding(base);
|
||||||
|
if (productType !== 'rdclient') {
|
||||||
|
normalized.bundle_id = bundleId;
|
||||||
|
normalized.product_name = normalized.company_name || 'BetterDesk Support';
|
||||||
|
}
|
||||||
const brandingHash = bundleService.hashBranding(normalized);
|
const brandingHash = bundleService.hashBranding(normalized);
|
||||||
const created = await db.createAgentBundle({
|
const created = await db.createAgentBundle({
|
||||||
bundleId,
|
bundleId,
|
||||||
@@ -218,9 +228,12 @@ router.post('/api/generator/bundles', requireAuth, requireAdmin, async (req, res
|
|||||||
branding: JSON.stringify(normalized),
|
branding: JSON.stringify(normalized),
|
||||||
brandingHash,
|
brandingHash,
|
||||||
createdBy: req.session?.userId || null,
|
createdBy: req.session?.userId || null,
|
||||||
|
productType,
|
||||||
});
|
});
|
||||||
// Phase 2: queue installer builds for every supported platform.
|
const enqueue = productType === 'rdclient'
|
||||||
buildWorker.enqueueBuildsForHash(brandingHash).catch((e) => {
|
? rdclientBuildWorker.enqueueBuildsForHash
|
||||||
|
: buildWorker.enqueueBuildsForHash;
|
||||||
|
enqueue(brandingHash).catch((e) => {
|
||||||
console.error('[generator] enqueue builds failed:', e.message);
|
console.error('[generator] enqueue builds failed:', e.message);
|
||||||
});
|
});
|
||||||
res.json({ success: true, data: { bundle: serializeBundle(created) } });
|
res.json({ success: true, data: { bundle: serializeBundle(created) } });
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const LANG_DIR = path.join(__dirname, '..', 'lang');
|
||||||
|
|
||||||
|
/** New keys per locale — English source + translations for all 26 locales. */
|
||||||
|
const PATCHES = {
|
||||||
|
en: {
|
||||||
|
remote: {
|
||||||
|
remember_peer_password: 'Remember device password on this device',
|
||||||
|
save_peer_password: 'Save password',
|
||||||
|
},
|
||||||
|
remote_dashboard: {
|
||||||
|
open_settings: 'Settings',
|
||||||
|
},
|
||||||
|
rdclient_settings: {
|
||||||
|
title: 'RdClient Settings',
|
||||||
|
open: 'Settings',
|
||||||
|
server_url: 'Panel URL',
|
||||||
|
tls_strict: 'Strict TLS',
|
||||||
|
sign_out: 'Sign out',
|
||||||
|
reset_client: 'Reset client',
|
||||||
|
discovery_refresh: 'Refresh',
|
||||||
|
discovery_empty: 'No servers found on the local network',
|
||||||
|
},
|
||||||
|
generator: {
|
||||||
|
rdclient_tab: 'RdClient desktop',
|
||||||
|
rdclient_new_bundle: 'New RdClient bundle',
|
||||||
|
rdclient_subtitle: 'Build branded RdClient desktop installers with embedded panel URL',
|
||||||
|
rdclient_server_url_hint: 'Public URL where operators reach the BetterDesk console (/remote dashboard).',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pl: {
|
||||||
|
remote: { remember_peer_password: 'Zapamiętaj hasło urządzenia na tym komputerze', save_peer_password: 'Zapisz hasło' },
|
||||||
|
remote_dashboard: { open_settings: 'Ustawienia' },
|
||||||
|
rdclient_settings: { title: 'Ustawienia RdClient', open: 'Ustawienia', server_url: 'URL panelu', tls_strict: 'Ścisłe TLS', sign_out: 'Wyloguj', reset_client: 'Resetuj klienta', discovery_refresh: 'Odśwież', discovery_empty: 'Nie znaleziono serwerów w sieci lokalnej' },
|
||||||
|
generator: { rdclient_tab: 'RdClient desktop', rdclient_new_bundle: 'Nowy pakiet RdClient', rdclient_subtitle: 'Buduj instalatory RdClient z wbudowanym adresem panelu', rdclient_server_url_hint: 'Publiczny URL panelu BetterDesk (dashboard /remote).' },
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
remote: { remember_peer_password: 'Gerätepasswort auf diesem Gerät merken', save_peer_password: 'Passwort speichern' },
|
||||||
|
remote_dashboard: { open_settings: 'Einstellungen' },
|
||||||
|
rdclient_settings: { title: 'RdClient-Einstellungen', open: 'Einstellungen', server_url: 'Panel-URL', tls_strict: 'Striktes TLS', sign_out: 'Abmelden', reset_client: 'Client zurücksetzen', discovery_refresh: 'Aktualisieren', discovery_empty: 'Keine Server im lokalen Netzwerk gefunden' },
|
||||||
|
generator: { rdclient_tab: 'RdClient Desktop', rdclient_new_bundle: 'Neues RdClient-Paket', rdclient_subtitle: 'Branded RdClient-Installer mit eingebetteter Panel-URL erstellen', rdclient_server_url_hint: 'Öffentliche URL der BetterDesk-Konsole (/remote).' },
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
remote: { remember_peer_password: 'Mémoriser le mot de passe de l’appareil sur cet ordinateur', save_peer_password: 'Enregistrer le mot de passe' },
|
||||||
|
remote_dashboard: { open_settings: 'Paramètres' },
|
||||||
|
rdclient_settings: { title: 'Paramètres RdClient', open: 'Paramètres', server_url: 'URL du panel', tls_strict: 'TLS strict', sign_out: 'Se déconnecter', reset_client: 'Réinitialiser le client', discovery_refresh: 'Actualiser', discovery_empty: 'Aucun serveur trouvé sur le réseau local' },
|
||||||
|
generator: { rdclient_tab: 'RdClient bureau', rdclient_new_bundle: 'Nouveau bundle RdClient', rdclient_subtitle: 'Créer des installateurs RdClient avec URL du panel intégrée', rdclient_server_url_hint: 'URL publique de la console BetterDesk (/remote).' },
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
remote: { remember_peer_password: 'Recordar contraseña del dispositivo en este equipo', save_peer_password: 'Guardar contraseña' },
|
||||||
|
remote_dashboard: { open_settings: 'Ajustes' },
|
||||||
|
rdclient_settings: { title: 'Ajustes de RdClient', open: 'Ajustes', server_url: 'URL del panel', tls_strict: 'TLS estricto', sign_out: 'Cerrar sesión', reset_client: 'Restablecer cliente', discovery_refresh: 'Actualizar', discovery_empty: 'No se encontraron servidores en la red local' },
|
||||||
|
generator: { rdclient_tab: 'RdClient escritorio', rdclient_new_bundle: 'Nuevo paquete RdClient', rdclient_subtitle: 'Generar instaladores RdClient con URL del panel integrada', rdclient_server_url_hint: 'URL pública de la consola BetterDesk (/remote).' },
|
||||||
|
},
|
||||||
|
it: {
|
||||||
|
remote: { remember_peer_password: 'Ricorda password dispositivo su questo computer', save_peer_password: 'Salva password' },
|
||||||
|
remote_dashboard: { open_settings: 'Impostazioni' },
|
||||||
|
rdclient_settings: { title: 'Impostazioni RdClient', open: 'Impostazioni', server_url: 'URL pannello', tls_strict: 'TLS rigoroso', sign_out: 'Esci', reset_client: 'Reimposta client', discovery_refresh: 'Aggiorna', discovery_empty: 'Nessun server trovato nella rete locale' },
|
||||||
|
generator: { rdclient_tab: 'RdClient desktop', rdclient_new_bundle: 'Nuovo bundle RdClient', rdclient_subtitle: 'Crea installer RdClient con URL pannello incorporato', rdclient_server_url_hint: 'URL pubblica della console BetterDesk (/remote).' },
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
remote: { remember_peer_password: 'Lembrar senha do dispositivo neste computador', save_peer_password: 'Guardar senha' },
|
||||||
|
remote_dashboard: { open_settings: 'Definições' },
|
||||||
|
rdclient_settings: { title: 'Definições RdClient', open: 'Definições', server_url: 'URL do painel', tls_strict: 'TLS estrito', sign_out: 'Terminar sessão', reset_client: 'Repor cliente', discovery_refresh: 'Atualizar', discovery_empty: 'Nenhum servidor encontrado na rede local' },
|
||||||
|
generator: { rdclient_tab: 'RdClient desktop', rdclient_new_bundle: 'Novo pacote RdClient', rdclient_subtitle: 'Criar instaladores RdClient com URL do painel incorporado', rdclient_server_url_hint: 'URL pública da consola BetterDesk (/remote).' },
|
||||||
|
},
|
||||||
|
nl: {
|
||||||
|
remote: { remember_peer_password: 'Apparaatwachtwoord onthouden op dit apparaat', save_peer_password: 'Wachtwoord opslaan' },
|
||||||
|
remote_dashboard: { open_settings: 'Instellingen' },
|
||||||
|
rdclient_settings: { title: 'RdClient-instellingen', open: 'Instellingen', server_url: 'Paneel-URL', tls_strict: 'Strikte TLS', sign_out: 'Afmelden', reset_client: 'Client resetten', discovery_refresh: 'Vernieuwen', discovery_empty: 'Geen servers gevonden op het lokale netwerk' },
|
||||||
|
generator: { rdclient_tab: 'RdClient desktop', rdclient_new_bundle: 'Nieuw RdClient-pakket', rdclient_subtitle: 'RdClient-installers bouwen met ingebedde paneel-URL', rdclient_server_url_hint: 'Publieke URL van het BetterDesk-paneel (/remote).' },
|
||||||
|
},
|
||||||
|
cs: {
|
||||||
|
remote: { remember_peer_password: 'Zapamatovat heslo zařízení na tomto počítači', save_peer_password: 'Uložit heslo' },
|
||||||
|
remote_dashboard: { open_settings: 'Nastavení' },
|
||||||
|
rdclient_settings: { title: 'Nastavení RdClient', open: 'Nastavení', server_url: 'URL panelu', tls_strict: 'Přísné TLS', sign_out: 'Odhlásit', reset_client: 'Resetovat klienta', discovery_refresh: 'Obnovit', discovery_empty: 'V místní síti nebyly nalezeny žádné servery' },
|
||||||
|
generator: { rdclient_tab: 'RdClient desktop', rdclient_new_bundle: 'Nový balíček RdClient', rdclient_subtitle: 'Sestavit instalátory RdClient s vloženou URL panelu', rdclient_server_url_hint: 'Veřejná URL konzole BetterDesk (/remote).' },
|
||||||
|
},
|
||||||
|
da: {
|
||||||
|
remote: { remember_peer_password: 'Husk enhedsadgangskode på denne computer', save_peer_password: 'Gem adgangskode' },
|
||||||
|
remote_dashboard: { open_settings: 'Indstillinger' },
|
||||||
|
rdclient_settings: { title: 'RdClient-indstillinger', open: 'Indstillinger', server_url: 'Panel-URL', tls_strict: 'Streng TLS', sign_out: 'Log ud', reset_client: 'Nulstil klient', discovery_refresh: 'Opdater', discovery_empty: 'Ingen servere fundet på det lokale netværk' },
|
||||||
|
generator: { rdclient_tab: 'RdClient desktop', rdclient_new_bundle: 'Ny RdClient-pakke', rdclient_subtitle: 'Byg RdClient-installere med indlejret panel-URL', rdclient_server_url_hint: 'Offentlig URL til BetterDesk-konsollen (/remote).' },
|
||||||
|
},
|
||||||
|
fi: {
|
||||||
|
remote: { remember_peer_password: 'Muista laitteen salasana tällä tietokoneella', save_peer_password: 'Tallenna salasana' },
|
||||||
|
remote_dashboard: { open_settings: 'Asetukset' },
|
||||||
|
rdclient_settings: { title: 'RdClient-asetukset', open: 'Asetukset', server_url: 'Paneelin URL', tls_strict: 'Tiukka TLS', sign_out: 'Kirjaudu ulos', reset_client: 'Nollaa asiakas', discovery_refresh: 'Päivitä', discovery_empty: 'Paikallisesta verkosta ei löytynyt palvelimia' },
|
||||||
|
generator: { rdclient_tab: 'RdClient-työpöytä', rdclient_new_bundle: 'Uusi RdClient-paketti', rdclient_subtitle: 'Rakenna RdClient-asentajat upotetulla paneelin URL:llä', rdclient_server_url_hint: 'BetterDesk-konsolin julkinen URL (/remote).' },
|
||||||
|
},
|
||||||
|
nb: {
|
||||||
|
remote: { remember_peer_password: 'Husk enhetspassord på denne datamaskinen', save_peer_password: 'Lagre passord' },
|
||||||
|
remote_dashboard: { open_settings: 'Innstillinger' },
|
||||||
|
rdclient_settings: { title: 'RdClient-innstillinger', open: 'Innstillinger', server_url: 'Panel-URL', tls_strict: 'Streng TLS', sign_out: 'Logg ut', reset_client: 'Tilbakestill klient', discovery_refresh: 'Oppdater', discovery_empty: 'Ingen servere funnet på det lokale nettverket' },
|
||||||
|
generator: { rdclient_tab: 'RdClient desktop', rdclient_new_bundle: 'Nytt RdClient-pakke', rdclient_subtitle: 'Bygg RdClient-installere med innebygd panel-URL', rdclient_server_url_hint: 'Offentlig URL til BetterDesk-konsollen (/remote).' },
|
||||||
|
},
|
||||||
|
sv: {
|
||||||
|
remote: { remember_peer_password: 'Kom ihåg enhetslösenord på den här datorn', save_peer_password: 'Spara lösenord' },
|
||||||
|
remote_dashboard: { open_settings: 'Inställningar' },
|
||||||
|
rdclient_settings: { title: 'RdClient-inställningar', open: 'Inställningar', server_url: 'Panel-URL', tls_strict: 'Strikt TLS', sign_out: 'Logga ut', reset_client: 'Återställ klient', discovery_refresh: 'Uppdatera', discovery_empty: 'Inga servrar hittades i det lokala nätverket' },
|
||||||
|
generator: { rdclient_tab: 'RdClient desktop', rdclient_new_bundle: 'Nytt RdClient-paket', rdclient_subtitle: 'Bygg RdClient-installerare med inbäddad panel-URL', rdclient_server_url_hint: 'Offentlig URL till BetterDesk-konsolen (/remote).' },
|
||||||
|
},
|
||||||
|
ro: {
|
||||||
|
remote: { remember_peer_password: 'Memorează parola dispozitivului pe acest computer', save_peer_password: 'Salvează parola' },
|
||||||
|
remote_dashboard: { open_settings: 'Setări' },
|
||||||
|
rdclient_settings: { title: 'Setări RdClient', open: 'Setări', server_url: 'URL panou', tls_strict: 'TLS strict', sign_out: 'Deconectare', reset_client: 'Resetează clientul', discovery_refresh: 'Reîmprospătează', discovery_empty: 'Nu s-au găsit servere în rețeaua locală' },
|
||||||
|
generator: { rdclient_tab: 'RdClient desktop', rdclient_new_bundle: 'Pachet RdClient nou', rdclient_subtitle: 'Construiește instalatoare RdClient cu URL panou integrat', rdclient_server_url_hint: 'URL public al consolei BetterDesk (/remote).' },
|
||||||
|
},
|
||||||
|
hu: {
|
||||||
|
remote: { remember_peer_password: 'Eszköz jelszó megjegyzése ezen a gépen', save_peer_password: 'Jelszó mentése' },
|
||||||
|
remote_dashboard: { open_settings: 'Beállítások' },
|
||||||
|
rdclient_settings: { title: 'RdClient beállítások', open: 'Beállítások', server_url: 'Panel URL', tls_strict: 'Szigorú TLS', sign_out: 'Kijelentkezés', reset_client: 'Kliens visszaállítása', discovery_refresh: 'Frissítés', discovery_empty: 'Nem található szerver a helyi hálózaton' },
|
||||||
|
generator: { rdclient_tab: 'RdClient asztali', rdclient_new_bundle: 'Új RdClient csomag', rdclient_subtitle: 'RdClient telepítők készítése beágyazott panel URL-lel', rdclient_server_url_hint: 'A BetterDesk konzol nyilvános URL-je (/remote).' },
|
||||||
|
},
|
||||||
|
uk: {
|
||||||
|
remote: { remember_peer_password: 'Запам’ятати пароль пристрою на цьому комп’ютері', save_peer_password: 'Зберегти пароль' },
|
||||||
|
remote_dashboard: { open_settings: 'Налаштування' },
|
||||||
|
rdclient_settings: { title: 'Налаштування RdClient', open: 'Налаштування', server_url: 'URL панелі', tls_strict: 'Суворий TLS', sign_out: 'Вийти', reset_client: 'Скинути клієнт', discovery_refresh: 'Оновити', discovery_empty: 'Серверів у локальній мережі не знайдено' },
|
||||||
|
generator: { rdclient_tab: 'RdClient desktop', rdclient_new_bundle: 'Новий пакет RdClient', rdclient_subtitle: 'Збирати інсталятори RdClient із вбудованою URL панелі', rdclient_server_url_hint: 'Публічна URL консолі BetterDesk (/remote).' },
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
remote: { remember_peer_password: 'Cihaz parolasını bu bilgisayarda hatırla', save_peer_password: 'Parolayı kaydet' },
|
||||||
|
remote_dashboard: { open_settings: 'Ayarlar' },
|
||||||
|
rdclient_settings: { title: 'RdClient ayarları', open: 'Ayarlar', server_url: 'Panel URL', tls_strict: 'Katı TLS', sign_out: 'Oturumu kapat', reset_client: 'İstemciyi sıfırla', discovery_refresh: 'Yenile', discovery_empty: 'Yerel ağda sunucu bulunamadı' },
|
||||||
|
generator: { rdclient_tab: 'RdClient masaüstü', rdclient_new_bundle: 'Yeni RdClient paketi', rdclient_subtitle: 'Gömülü panel URL’li RdClient yükleyicileri oluştur', rdclient_server_url_hint: 'BetterDesk konsolunun genel URL’si (/remote).' },
|
||||||
|
},
|
||||||
|
ar: {
|
||||||
|
remote: { remember_peer_password: 'تذكر كلمة مرور الجهاز على هذا الحاسوب', save_peer_password: 'حفظ كلمة المرور' },
|
||||||
|
remote_dashboard: { open_settings: 'الإعدادات' },
|
||||||
|
rdclient_settings: { title: 'إعدادات RdClient', open: 'الإعدادات', server_url: 'رابط اللوحة', tls_strict: 'TLS صارم', sign_out: 'تسجيل الخروج', reset_client: 'إعادة ضبط العميل', discovery_refresh: 'تحديث', discovery_empty: 'لم يتم العثور على خوادم في الشبكة المحلية' },
|
||||||
|
generator: { rdclient_tab: 'RdClient سطح المكتب', rdclient_new_bundle: 'حزمة RdClient جديدة', rdclient_subtitle: 'إنشاء مثبتات RdClient مع رابط اللوحة المضمن', rdclient_server_url_hint: 'الرابط العام لوحة BetterDesk (/remote).' },
|
||||||
|
},
|
||||||
|
hi: {
|
||||||
|
remote: { remember_peer_password: 'इस डिवाइस पर डिवाइस पासवर्ड याद रखें', save_peer_password: 'पासवर्ड सहेजें' },
|
||||||
|
remote_dashboard: { open_settings: 'सेटिंग्स' },
|
||||||
|
rdclient_settings: { title: 'RdClient सेटिंग्स', open: 'सेटिंग्स', server_url: 'पैनल URL', tls_strict: 'सख्त TLS', sign_out: 'साइन आउट', reset_client: 'क्लाइंट रीसेट करें', discovery_refresh: 'रीफ़्रेश', discovery_empty: 'स्थानीय नेटवर्क पर कोई सर्वर नहीं मिला' },
|
||||||
|
generator: { rdclient_tab: 'RdClient डेस्कटॉप', rdclient_new_bundle: 'नया RdClient बंडल', rdclient_subtitle: 'एम्बेडेड पैनल URL के साथ RdClient इंस्टॉलर बनाएं', rdclient_server_url_hint: 'BetterDesk कंसोल का सार्वजनिक URL (/remote).' },
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
remote: { remember_peer_password: 'このデバイスでデバイスパスワードを記憶', save_peer_password: 'パスワードを保存' },
|
||||||
|
remote_dashboard: { open_settings: '設定' },
|
||||||
|
rdclient_settings: { title: 'RdClient 設定', open: '設定', server_url: 'パネル URL', tls_strict: '厳格 TLS', sign_out: 'サインアウト', reset_client: 'クライアントをリセット', discovery_refresh: '更新', discovery_empty: 'ローカルネットワークにサーバーが見つかりません' },
|
||||||
|
generator: { rdclient_tab: 'RdClient デスクトップ', rdclient_new_bundle: '新規 RdClient バンドル', rdclient_subtitle: 'パネル URL を埋め込んだ RdClient インストーラーをビルド', rdclient_server_url_hint: 'BetterDesk コンソールの公開 URL (/remote)。' },
|
||||||
|
},
|
||||||
|
ko: {
|
||||||
|
remote: { remember_peer_password: '이 기기에서 장치 비밀번호 기억', save_peer_password: '비밀번호 저장' },
|
||||||
|
remote_dashboard: { open_settings: '설정' },
|
||||||
|
rdclient_settings: { title: 'RdClient 설정', open: '설정', server_url: '패널 URL', tls_strict: '엄격 TLS', sign_out: '로그아웃', reset_client: '클라이언트 재설정', discovery_refresh: '새로고침', discovery_empty: '로컬 네트워크에서 서버를 찾을 수 없습니다' },
|
||||||
|
generator: { rdclient_tab: 'RdClient 데스크톱', rdclient_new_bundle: '새 RdClient 번들', rdclient_subtitle: '패널 URL이 포함된 RdClient 설치 프로그램 빌드', rdclient_server_url_hint: 'BetterDesk 콘솔 공개 URL (/remote).' },
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
remote: { remember_peer_password: '在此设备上记住设备密码', save_peer_password: '保存密码' },
|
||||||
|
remote_dashboard: { open_settings: '设置' },
|
||||||
|
rdclient_settings: { title: 'RdClient 设置', open: '设置', server_url: '面板 URL', tls_strict: '严格 TLS', sign_out: '退出登录', reset_client: '重置客户端', discovery_refresh: '刷新', discovery_empty: '在本地网络中未找到服务器' },
|
||||||
|
generator: { rdclient_tab: 'RdClient 桌面版', rdclient_new_bundle: '新建 RdClient 包', rdclient_subtitle: '构建嵌入面板 URL 的 RdClient 安装程序', rdclient_server_url_hint: 'BetterDesk 控制台的公开 URL (/remote)。' },
|
||||||
|
},
|
||||||
|
'zh-TW': {
|
||||||
|
remote: { remember_peer_password: '在此裝置上記住裝置密碼', save_peer_password: '儲存密碼' },
|
||||||
|
remote_dashboard: { open_settings: '設定' },
|
||||||
|
rdclient_settings: { title: 'RdClient 設定', open: '設定', server_url: '面板 URL', tls_strict: '嚴格 TLS', sign_out: '登出', reset_client: '重設用戶端', discovery_refresh: '重新整理', discovery_empty: '區域網路中找不到伺服器' },
|
||||||
|
generator: { rdclient_tab: 'RdClient 桌面版', rdclient_new_bundle: '新增 RdClient 套件', rdclient_subtitle: '建置內嵌面板 URL 的 RdClient 安裝程式', rdclient_server_url_hint: 'BetterDesk 主控台的公開 URL (/remote)。' },
|
||||||
|
},
|
||||||
|
vi: {
|
||||||
|
remote: { remember_peer_password: 'Ghi nhớ mật khẩu thiết bị trên máy này', save_peer_password: 'Lưu mật khẩu' },
|
||||||
|
remote_dashboard: { open_settings: 'Cài đặt' },
|
||||||
|
rdclient_settings: { title: 'Cài đặt RdClient', open: 'Cài đặt', server_url: 'URL bảng điều khiển', tls_strict: 'TLS nghiêm ngặt', sign_out: 'Đăng xuất', reset_client: 'Đặt lại client', discovery_refresh: 'Làm mới', discovery_empty: 'Không tìm thấy máy chủ trên mạng cục bộ' },
|
||||||
|
generator: { rdclient_tab: 'RdClient desktop', rdclient_new_bundle: 'Gói RdClient mới', rdclient_subtitle: 'Tạo trình cài RdClient với URL bảng điều khiển nhúng', rdclient_server_url_hint: 'URL công khai của bảng điều khiển BetterDesk (/remote).' },
|
||||||
|
},
|
||||||
|
th: {
|
||||||
|
remote: { remember_peer_password: 'จดจำรหัสผ่านอุปกรณ์บนคอมพิวเตอร์นี้', save_peer_password: 'บันทึกรหัสผ่าน' },
|
||||||
|
remote_dashboard: { open_settings: 'การตั้งค่า' },
|
||||||
|
rdclient_settings: { title: 'การตั้งค่า RdClient', open: 'การตั้งค่า', server_url: 'URL แผงควบคุม', tls_strict: 'TLS เข้มงวด', sign_out: 'ออกจากระบบ', reset_client: 'รีเซ็ตไคลเอนต์', discovery_refresh: 'รีเฟรช', discovery_empty: 'ไม่พบเซิร์ฟเวอร์ในเครือข่ายท้องถิ่น' },
|
||||||
|
generator: { rdclient_tab: 'RdClient เดสก์ท็อป', rdclient_new_bundle: 'ชุด RdClient ใหม่', rdclient_subtitle: 'สร้างตัวติดตั้ง RdClient พร้อม URL แผงควบคุมฝังตัว', rdclient_server_url_hint: 'URL สาธารณะของคอนโซล BetterDesk (/remote)' },
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
remote: { remember_peer_password: 'Ingat kata sandi perangkat di komputer ini', save_peer_password: 'Simpan kata sandi' },
|
||||||
|
remote_dashboard: { open_settings: 'Pengaturan' },
|
||||||
|
rdclient_settings: { title: 'Pengaturan RdClient', open: 'Pengaturan', server_url: 'URL panel', tls_strict: 'TLS ketat', sign_out: 'Keluar', reset_client: 'Reset klien', discovery_refresh: 'Segarkan', discovery_empty: 'Tidak ada server di jaringan lokal' },
|
||||||
|
generator: { rdclient_tab: 'RdClient desktop', rdclient_new_bundle: 'Paket RdClient baru', rdclient_subtitle: 'Buat installer RdClient dengan URL panel tertanam', rdclient_server_url_hint: 'URL publik konsol BetterDesk (/remote).' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function deepMerge(target, patch) {
|
||||||
|
for (const [k, v] of Object.entries(patch)) {
|
||||||
|
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
||||||
|
if (!target[k] || typeof target[k] !== 'object') target[k] = {};
|
||||||
|
deepMerge(target[k], v);
|
||||||
|
} else {
|
||||||
|
target[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(LANG_DIR).filter((f) => f.endsWith('.json'));
|
||||||
|
for (const file of files) {
|
||||||
|
const code = file.replace(/\.json$/, '');
|
||||||
|
const patch = PATCHES[code] || PATCHES.en;
|
||||||
|
const filePath = path.join(LANG_DIR, file);
|
||||||
|
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||||
|
deepMerge(data, patch);
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
||||||
|
console.log('patched', file);
|
||||||
|
}
|
||||||
@@ -526,6 +526,12 @@ async function startServer() {
|
|||||||
|
|
||||||
// Start LAN Discovery UDP service
|
// Start LAN Discovery UDP service
|
||||||
startDiscoveryService();
|
startDiscoveryService();
|
||||||
|
try {
|
||||||
|
const panelDiscovery = require('./services/panelDiscovery');
|
||||||
|
panelDiscovery.startPanelMdns();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[server] mDNS panel discovery disabled:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
// Start branded agent installer build worker (Generator Agenta / Phase 2).
|
// Start branded agent installer build worker (Generator Agenta / Phase 2).
|
||||||
// Disabled when AGENT_BUILD_WORKER=off — useful for hosts without the
|
// Disabled when AGENT_BUILD_WORKER=off — useful for hosts without the
|
||||||
@@ -538,6 +544,14 @@ async function startServer() {
|
|||||||
console.warn('[server] agent build worker disabled:', err.message);
|
console.warn('[server] agent build worker disabled:', err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (process.env.RDCLIENT_BUILD_WORKER !== 'off') {
|
||||||
|
try {
|
||||||
|
const rdclientBuildWorker = require('./services/rdclientBuildWorker');
|
||||||
|
rdclientBuildWorker.startWorker();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[server] rdclient build worker disabled:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============ RustDesk Client API (WAN :21121 → Go :21114 proxy) ============
|
// ============ RustDesk Client API (WAN :21121 → Go :21114 proxy) ============
|
||||||
let apiServer = null;
|
let apiServer = null;
|
||||||
|
|||||||
@@ -592,6 +592,8 @@ async function _claimNextBuild() {
|
|||||||
return String(a.created_at || '').localeCompare(String(b.created_at || ''));
|
return String(a.created_at || '').localeCompare(String(b.created_at || ''));
|
||||||
});
|
});
|
||||||
for (const row of candidates) {
|
for (const row of candidates) {
|
||||||
|
const bundleRow = await _findBundleForHash(row.branding_hash);
|
||||||
|
if (bundleRow && (bundleRow.product_type || 'agent') === 'rdclient') continue;
|
||||||
const profile = BUILD_PROFILES[`${row.platform}/${row.arch}/${row.format}`];
|
const profile = BUILD_PROFILES[`${row.platform}/${row.arch}/${row.format}`];
|
||||||
if (!profile) continue;
|
if (!profile) continue;
|
||||||
await db.upsertAgentBundleBuild({
|
await db.upsertAgentBundleBuild({
|
||||||
@@ -615,6 +617,7 @@ async function _listPendingBuilds(limit) {
|
|||||||
const out = [];
|
const out = [];
|
||||||
for (const b of bundles) {
|
for (const b of bundles) {
|
||||||
if (b.revoked) continue;
|
if (b.revoked) continue;
|
||||||
|
if ((b.product_type || 'agent') === 'rdclient') continue;
|
||||||
const builds = await db.listAgentBundleBuildsForHash(b.branding_hash);
|
const builds = await db.listAgentBundleBuildsForHash(b.branding_hash);
|
||||||
for (const r of builds) {
|
for (const r of builds) {
|
||||||
if (r.status === 'pending') out.push(r);
|
if (r.status === 'pending') out.push(r);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const config = require('../config/config');
|
||||||
const conn = require('./agentBundleConnection');
|
const conn = require('./agentBundleConnection');
|
||||||
|
|
||||||
// Supported delivery targets. The portal renders one card per entry.
|
// Supported delivery targets. The portal renders one card per entry.
|
||||||
@@ -343,6 +344,39 @@ function defaultBranding() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate branding for RdClient desktop bundles (panel URL embedded in installer).
|
||||||
|
*/
|
||||||
|
function validateRdclientBranding(input = {}) {
|
||||||
|
const errors = [];
|
||||||
|
const out = {};
|
||||||
|
|
||||||
|
out.company_name = clip(input.company_name || input.companyName || 'BetterDesk RdClient', MAX_NAME);
|
||||||
|
out.server_host = clip(input.server_host || input.serverHost, 253);
|
||||||
|
out.use_https = input.use_https !== false && input.useHttps !== false;
|
||||||
|
|
||||||
|
const hostNorm = conn.normalizeServerHost(out.server_host);
|
||||||
|
if (!hostNorm.valid) {
|
||||||
|
errors.push(hostNorm.error || 'server_host_invalid');
|
||||||
|
} else {
|
||||||
|
out.server_host = hostNorm.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheme = out.use_https ? 'https' : 'http';
|
||||||
|
const port = config.port;
|
||||||
|
const omitPort = (scheme === 'https' && port === 443) || (scheme === 'http' && port === 80);
|
||||||
|
out.panel_url = omitPort
|
||||||
|
? `${scheme}://${out.server_host}`
|
||||||
|
: `${scheme}://${out.server_host}:${port}`;
|
||||||
|
|
||||||
|
out.default_lang = clip(input.default_lang || input.defaultLang || 'en', 10);
|
||||||
|
if (!SUPPORTED_LANGS.includes(out.default_lang)) {
|
||||||
|
out.default_lang = 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: errors.length === 0, errors, normalized: out };
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
SUPPORTED_LANGS,
|
SUPPORTED_LANGS,
|
||||||
@@ -350,6 +384,7 @@ module.exports = {
|
|||||||
MAX_SLUG_LENGTH,
|
MAX_SLUG_LENGTH,
|
||||||
MIN_SLUG_LENGTH,
|
MIN_SLUG_LENGTH,
|
||||||
validateBranding,
|
validateBranding,
|
||||||
|
validateRdclientBranding,
|
||||||
hashBranding,
|
hashBranding,
|
||||||
generateBundleId,
|
generateBundleId,
|
||||||
slugifyName,
|
slugifyName,
|
||||||
|
|||||||
@@ -920,6 +920,15 @@ function createSqliteAdapter(config) {
|
|||||||
CREATE INDEX IF NOT EXISTS idx_agent_bundle_builds_status ON agent_bundle_builds (status);
|
CREATE INDEX IF NOT EXISTS idx_agent_bundle_builds_status ON agent_bundle_builds (status);
|
||||||
`);
|
`);
|
||||||
migrateAgentBundleSlugsSqlite(db);
|
migrateAgentBundleSlugsSqlite(db);
|
||||||
|
try {
|
||||||
|
const cols = new Set(db.prepare('PRAGMA table_info(agent_bundles)').all().map(c => c.name));
|
||||||
|
if (!cols.has('product_type')) {
|
||||||
|
db.exec("ALTER TABLE agent_bundles ADD COLUMN product_type TEXT NOT NULL DEFAULT 'agent'");
|
||||||
|
console.log('[DB] Migration: added agent_bundles.product_type');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[DB] Migration agent_bundles.product_type error:', e.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Multi-tenancy tables ----------------------------------------------
|
// -- Multi-tenancy tables ----------------------------------------------
|
||||||
@@ -3074,12 +3083,12 @@ function createSqliteAdapter(config) {
|
|||||||
return excludeBundleId ? row.bundle_id !== excludeBundleId : true;
|
return excludeBundleId ? row.bundle_id !== excludeBundleId : true;
|
||||||
},
|
},
|
||||||
|
|
||||||
async createAgentBundle({ bundleId, slug, name, branding, brandingHash, createdBy }) {
|
async createAgentBundle({ bundleId, slug, name, branding, brandingHash, createdBy, productType }) {
|
||||||
const db = openMain();
|
const db = openMain();
|
||||||
const r = db.prepare(`
|
const r = db.prepare(`
|
||||||
INSERT INTO agent_bundles (bundle_id, slug, name, branding, branding_hash, created_by)
|
INSERT INTO agent_bundles (bundle_id, slug, name, branding, branding_hash, created_by, product_type)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(bundleId, slug || null, name, branding, brandingHash, createdBy || null);
|
`).run(bundleId, slug || null, name, branding, brandingHash, createdBy || null, productType || 'agent');
|
||||||
return db.prepare('SELECT * FROM agent_bundles WHERE id = ?').get(r.lastInsertRowid);
|
return db.prepare('SELECT * FROM agent_bundles WHERE id = ?').get(r.lastInsertRowid);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ function buildAnnouncement() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const host = process.env.PANEL_PUBLIC_HOST || config.host || 'localhost';
|
||||||
|
const port = config.port;
|
||||||
|
const protocol = config.httpsEnabled ? 'https' : 'http';
|
||||||
|
const panelUrl = process.env.PANEL_PUBLIC_URL
|
||||||
|
|| `${protocol}://${host}${(protocol === 'https' && port === 443) || (protocol === 'http' && port === 80) ? '' : `:${port}`}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'betterdesk-announce',
|
type: 'betterdesk-announce',
|
||||||
version: PROTOCOL_VERSION,
|
version: PROTOCOL_VERSION,
|
||||||
@@ -57,10 +63,11 @@ function buildAnnouncement() {
|
|||||||
version: config.appVersion,
|
version: config.appVersion,
|
||||||
port: config.port,
|
port: config.port,
|
||||||
apiPort: config.apiPort,
|
apiPort: config.apiPort,
|
||||||
protocol: config.httpsEnabled ? 'https' : 'http',
|
protocol,
|
||||||
publicKey,
|
publicKey,
|
||||||
addresses,
|
addresses,
|
||||||
discoveryPort: DISCOVERY_PORT,
|
discoveryPort: DISCOVERY_PORT,
|
||||||
|
panelUrl,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* BetterDesk Console — mDNS panel discovery (optional)
|
||||||
|
*
|
||||||
|
* Publishes `_betterdesk._tcp` on the LAN so RdClient desktop can browse
|
||||||
|
* for panels. Disable with PANEL_MDNS=off.
|
||||||
|
*
|
||||||
|
* UDP LAN discovery remains in lanDiscovery.js (port 21119).
|
||||||
|
*
|
||||||
|
* @module services/panelDiscovery
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('../config/config');
|
||||||
|
const { buildAnnouncement } = require('./lanDiscovery');
|
||||||
|
|
||||||
|
let bonjourInstance = null;
|
||||||
|
let published = null;
|
||||||
|
|
||||||
|
function panelPublicUrl() {
|
||||||
|
const ann = buildAnnouncement();
|
||||||
|
return ann.server.panelUrl || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPanelMdns() {
|
||||||
|
if (process.env.PANEL_MDNS === 'off' || process.env.PANEL_MDNS === '0') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Bonjour;
|
||||||
|
try {
|
||||||
|
Bonjour = require('bonjour-service').Bonjour;
|
||||||
|
} catch (_) {
|
||||||
|
console.warn('[panelDiscovery] bonjour-service not installed — mDNS disabled (UDP discovery still active)');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bonjourInstance) return bonjourInstance;
|
||||||
|
|
||||||
|
const panelUrl = panelPublicUrl();
|
||||||
|
if (!panelUrl) return null;
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = new URL(panelUrl);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = parsed.port
|
||||||
|
? parseInt(parsed.port, 10)
|
||||||
|
: (parsed.protocol === 'https:' ? 443 : 80);
|
||||||
|
|
||||||
|
bonjourInstance = new Bonjour();
|
||||||
|
published = bonjourInstance.publish({
|
||||||
|
name: config.appName || 'BetterDesk',
|
||||||
|
type: 'betterdesk',
|
||||||
|
protocol: 'tcp',
|
||||||
|
port,
|
||||||
|
txt: {
|
||||||
|
url: panelUrl.replace(/\/$/, ''),
|
||||||
|
version: String(config.appVersion || ''),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` mDNS discovery published _betterdesk._tcp port ${port} (${panelUrl})`);
|
||||||
|
return bonjourInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPanelMdns() {
|
||||||
|
if (published) {
|
||||||
|
try { published.stop(); } catch (_) { /* ignore */ }
|
||||||
|
published = null;
|
||||||
|
}
|
||||||
|
if (bonjourInstance) {
|
||||||
|
try { bonjourInstance.destroy(); } catch (_) { /* ignore */ }
|
||||||
|
bonjourInstance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
startPanelMdns,
|
||||||
|
stopPanelMdns,
|
||||||
|
};
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
/**
|
||||||
|
* BetterDesk Console — RdClient desktop build worker (Tauri)
|
||||||
|
*
|
||||||
|
* Builds branded RdClient installers when generator bundles have product_type=rdclient.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const fsp = fs.promises;
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
const db = require('./database');
|
||||||
|
const bundleService = require('./agentBundleService');
|
||||||
|
const config = require('../config/config');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const envFile = process.env.BETTERDESK_BUILD_ENV_FILE || '/etc/betterdesk/build.env';
|
||||||
|
if (fs.existsSync(envFile)) {
|
||||||
|
const txt = fs.readFileSync(envFile, 'utf8');
|
||||||
|
for (const line of txt.split(/\r?\n/)) {
|
||||||
|
const m = /^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
|
||||||
|
if (!m) continue;
|
||||||
|
const [, key, val] = m;
|
||||||
|
if (process.env[key] === undefined) process.env[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[rdclientBuildWorker] could not load build env file:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUILD_CACHE_DIR = process.env.BUILD_CACHE_DIR
|
||||||
|
|| path.join(config.dataDir || '/opt/BetterDeskConsole/data', 'build-cache');
|
||||||
|
const WORK_ROOT = path.join(BUILD_CACHE_DIR, 'rdclient-work');
|
||||||
|
const ARTIFACT_ROOT = process.env.RDCLIENT_ARTIFACT_DIR
|
||||||
|
|| path.join(config.dataDir || '/opt/BetterDeskConsole/data', 'rdclient-builds');
|
||||||
|
const POLL_INTERVAL_MS = parseInt(process.env.RDCLIENT_BUILD_POLL_MS || '8000', 10);
|
||||||
|
const BUILD_TIMEOUT_MS = parseInt(process.env.RDCLIENT_BUILD_TIMEOUT_MS || (45 * 60 * 1000), 10);
|
||||||
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
||||||
|
const SOURCE_ROOT = path.join(REPO_ROOT, 'rdclient-desktop');
|
||||||
|
|
||||||
|
const BUILD_PROFILES = {
|
||||||
|
'windows/x64/portable': { os: 'windows', bundles: [], artifact: 'exe' },
|
||||||
|
'windows/x64/installed': { os: 'windows', bundles: ['msi'], artifact: 'msi' },
|
||||||
|
'linux/x64/portable': { os: 'linux', bundles: [], artifact: 'tgz' },
|
||||||
|
'linux/x64/appimage': { os: 'linux', bundles: ['appimage'], artifact: 'appimage' },
|
||||||
|
'linux/x64/installed': { os: 'linux', bundles: ['deb'], artifact: 'deb' },
|
||||||
|
'linux/x64/rpm': { os: 'linux', bundles: ['rpm'], artifact: 'rpm' },
|
||||||
|
};
|
||||||
|
|
||||||
|
let _pollTimer = null;
|
||||||
|
let _running = false;
|
||||||
|
let _activeBuilds = 0;
|
||||||
|
|
||||||
|
function _run(cmd, args, opts = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(cmd, args, {
|
||||||
|
cwd: opts.cwd,
|
||||||
|
env: { ...process.env, ...(opts.env || {}) },
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
let out = '';
|
||||||
|
child.stdout.on('data', (d) => { out += d.toString(); });
|
||||||
|
child.stderr.on('data', (d) => { out += d.toString(); });
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
child.kill('SIGKILL');
|
||||||
|
reject(new Error(`timeout after ${BUILD_TIMEOUT_MS}ms`));
|
||||||
|
}, BUILD_TIMEOUT_MS);
|
||||||
|
child.on('error', (e) => { clearTimeout(timer); reject(e); });
|
||||||
|
child.on('close', (code) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (code === 0) resolve(out);
|
||||||
|
else reject(new Error(`${cmd} exited ${code}: ${out.slice(-4000)}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _copyDir(src, dest) {
|
||||||
|
await fsp.mkdir(dest, { recursive: true });
|
||||||
|
const entries = await fsp.readdir(src, { withFileTypes: true });
|
||||||
|
for (const ent of entries) {
|
||||||
|
const s = path.join(src, ent.name);
|
||||||
|
const d = path.join(dest, ent.name);
|
||||||
|
if (ent.name === 'node_modules' || ent.name === 'target') continue;
|
||||||
|
if (ent.isDirectory()) await _copyDir(s, d);
|
||||||
|
else await fsp.copyFile(s, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _findBundleForHash(hash) {
|
||||||
|
const all = await db.listAgentBundles({ includeRevoked: true });
|
||||||
|
return all.find((b) => b.branding_hash === hash) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _listPendingRdclientBuilds(limit) {
|
||||||
|
const bundles = await db.listAgentBundles();
|
||||||
|
const out = [];
|
||||||
|
for (const b of bundles) {
|
||||||
|
if (b.revoked || (b.product_type || 'agent') !== 'rdclient') continue;
|
||||||
|
const builds = await db.listAgentBundleBuildsForHash(b.branding_hash);
|
||||||
|
for (const r of builds) {
|
||||||
|
if (r.status === 'pending') out.push(r);
|
||||||
|
if (out.length >= limit) break;
|
||||||
|
}
|
||||||
|
if (out.length >= limit) break;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _hasRdclientBuildInProgress() {
|
||||||
|
if (_activeBuilds > 0) return true;
|
||||||
|
const bundles = await db.listAgentBundles();
|
||||||
|
for (const b of bundles) {
|
||||||
|
if ((b.product_type || 'agent') !== 'rdclient' || b.revoked) continue;
|
||||||
|
const builds = await db.listAgentBundleBuildsForHash(b.branding_hash);
|
||||||
|
if (builds.some((r) => r.status === 'building')) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enqueueBuildsForHash(brandingHash, { force = false } = {}) {
|
||||||
|
const platforms = bundleService.PLATFORMS || [];
|
||||||
|
for (const p of platforms) {
|
||||||
|
const existing = await db.getAgentBundleBuild({
|
||||||
|
brandingHash, platform: p.platform, arch: p.arch, format: p.format,
|
||||||
|
});
|
||||||
|
if (!force && existing && (existing.status === 'ready' || existing.status === 'building')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await db.upsertAgentBundleBuild({
|
||||||
|
brandingHash,
|
||||||
|
platform: p.platform,
|
||||||
|
arch: p.arch,
|
||||||
|
format: p.format,
|
||||||
|
status: 'pending',
|
||||||
|
artifactPath: existing?.artifact_path || null,
|
||||||
|
artifactSize: existing?.artifact_size || 0,
|
||||||
|
artifactSha256: existing?.artifact_sha256 || null,
|
||||||
|
errorMessage: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _materialiseWorkDir(hash, branding) {
|
||||||
|
const workDir = path.join(WORK_ROOT, hash.slice(0, 16));
|
||||||
|
if (fs.existsSync(workDir)) {
|
||||||
|
await fsp.rm(workDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
await _copyDir(SOURCE_ROOT, workDir);
|
||||||
|
const embed = {
|
||||||
|
server_url: branding.panel_url || branding.server_url || '',
|
||||||
|
bundle_id: branding.bundle_id || '',
|
||||||
|
};
|
||||||
|
await fsp.writeFile(
|
||||||
|
path.join(workDir, 'betterdesk-rdclient.json'),
|
||||||
|
JSON.stringify(embed, null, 2),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
return workDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _findArtifact(workDir, profile, key) {
|
||||||
|
const bundleDir = path.join(workDir, 'src-tauri', 'target', 'release', 'bundle');
|
||||||
|
const releaseDir = path.join(workDir, 'src-tauri', 'target', 'release');
|
||||||
|
if (profile.artifact === 'deb') {
|
||||||
|
const debDir = path.join(bundleDir, 'deb');
|
||||||
|
const files = fs.existsSync(debDir) ? await fsp.readdir(debDir) : [];
|
||||||
|
const deb = files.find((f) => f.endsWith('.deb'));
|
||||||
|
if (deb) return path.join(debDir, deb);
|
||||||
|
}
|
||||||
|
if (profile.artifact === 'rpm') {
|
||||||
|
const rpmDir = path.join(bundleDir, 'rpm');
|
||||||
|
const files = fs.existsSync(rpmDir) ? await fsp.readdir(rpmDir) : [];
|
||||||
|
const rpm = files.find((f) => f.endsWith('.rpm'));
|
||||||
|
if (rpm) return path.join(rpmDir, rpm);
|
||||||
|
}
|
||||||
|
if (profile.artifact === 'appimage') {
|
||||||
|
const aiDir = path.join(bundleDir, 'appimage');
|
||||||
|
const files = fs.existsSync(aiDir) ? await fsp.readdir(aiDir) : [];
|
||||||
|
const ai = files.find((f) => f.endsWith('.AppImage'));
|
||||||
|
if (ai) return path.join(aiDir, ai);
|
||||||
|
}
|
||||||
|
if (profile.artifact === 'msi') {
|
||||||
|
const msiDir = path.join(bundleDir, 'msi');
|
||||||
|
const files = fs.existsSync(msiDir) ? await fsp.readdir(msiDir) : [];
|
||||||
|
const msi = files.find((f) => f.endsWith('.msi'));
|
||||||
|
if (msi) return path.join(msiDir, msi);
|
||||||
|
}
|
||||||
|
if (profile.artifact === 'exe') {
|
||||||
|
const names = ['betterdesk-rdclient.exe', 'BetterDesk RdClient.exe', 'rdclient-desktop.exe'];
|
||||||
|
for (const n of names) {
|
||||||
|
const p = path.join(releaseDir, n);
|
||||||
|
if (fs.existsSync(p)) return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (profile.artifact === 'tgz') {
|
||||||
|
const names = ['betterdesk-rdclient', 'BetterDesk RdClient', 'rdclient-desktop'];
|
||||||
|
for (const n of names) {
|
||||||
|
const bin = path.join(releaseDir, n);
|
||||||
|
if (fs.existsSync(bin)) {
|
||||||
|
const stage = path.join(workDir, 'dist-portable');
|
||||||
|
await fsp.mkdir(stage, { recursive: true });
|
||||||
|
await fsp.copyFile(bin, path.join(stage, n));
|
||||||
|
const launcher = path.join(SOURCE_ROOT, 'scripts', 'rdclient-launcher.sh');
|
||||||
|
if (fs.existsSync(launcher)) {
|
||||||
|
await fsp.copyFile(launcher, path.join(stage, 'rdclient-launcher.sh'));
|
||||||
|
await fsp.chmod(path.join(stage, 'rdclient-launcher.sh'), 0o755);
|
||||||
|
}
|
||||||
|
const tarPath = path.join(ARTIFACT_ROOT, `${key.replace(/\//g, '-')}.tar.gz`);
|
||||||
|
await fsp.mkdir(path.dirname(tarPath), { recursive: true });
|
||||||
|
await _run('tar', ['-czf', tarPath, '-C', stage, '.']);
|
||||||
|
return tarPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`artifact not found for ${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _sha256(filePath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const h = crypto.createHash('sha256');
|
||||||
|
const s = fs.createReadStream(filePath);
|
||||||
|
s.on('error', reject);
|
||||||
|
s.on('data', (d) => h.update(d));
|
||||||
|
s.on('end', () => resolve(h.digest('hex')));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _runOne(buildRow) {
|
||||||
|
const key = `${buildRow.platform}/${buildRow.arch}/${buildRow.format}`;
|
||||||
|
const profile = BUILD_PROFILES[key];
|
||||||
|
if (!profile) throw new Error(`unsupported profile ${key}`);
|
||||||
|
|
||||||
|
const bundleRow = await _findBundleForHash(buildRow.branding_hash);
|
||||||
|
if (!bundleRow || (bundleRow.product_type || 'agent') !== 'rdclient') {
|
||||||
|
throw new Error('not an rdclient bundle');
|
||||||
|
}
|
||||||
|
|
||||||
|
const branding = JSON.parse(bundleRow.branding || '{}');
|
||||||
|
if (!branding.panel_url && !branding.server_url) {
|
||||||
|
throw new Error('panel_url missing in rdclient bundle branding');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[rdclientBuildWorker] build start ${key} hash=${buildRow.branding_hash.slice(0, 12)}`);
|
||||||
|
const workDir = await _materialiseWorkDir(buildRow.branding_hash, branding);
|
||||||
|
await _run('npm', ['ci'], { cwd: workDir });
|
||||||
|
|
||||||
|
const tauriArgs = ['run', 'tauri', 'build'];
|
||||||
|
if (profile.bundles.length) {
|
||||||
|
tauriArgs.push('--', '--bundles', profile.bundles.join(','));
|
||||||
|
}
|
||||||
|
await _run('npm', tauriArgs, { cwd: workDir });
|
||||||
|
|
||||||
|
const built = await _findArtifact(workDir, profile, key);
|
||||||
|
const destDir = path.join(ARTIFACT_ROOT, buildRow.branding_hash.slice(0, 16));
|
||||||
|
await fsp.mkdir(destDir, { recursive: true });
|
||||||
|
const destName = path.basename(built);
|
||||||
|
const destPath = path.join(destDir, destName);
|
||||||
|
if (built !== destPath) await fsp.copyFile(built, destPath);
|
||||||
|
const stat = await fsp.stat(destPath);
|
||||||
|
const sha = await _sha256(destPath);
|
||||||
|
|
||||||
|
await db.upsertAgentBundleBuild({
|
||||||
|
brandingHash: buildRow.branding_hash,
|
||||||
|
platform: buildRow.platform,
|
||||||
|
arch: buildRow.arch,
|
||||||
|
format: buildRow.format,
|
||||||
|
status: 'ready',
|
||||||
|
artifactPath: destPath,
|
||||||
|
artifactSize: stat.size,
|
||||||
|
artifactSha256: sha,
|
||||||
|
errorMessage: '',
|
||||||
|
});
|
||||||
|
console.log(`[rdclientBuildWorker] build ready ${key} (${(stat.size / 1024 / 1024).toFixed(2)} MB)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _tick() {
|
||||||
|
if (_running || _activeBuilds > 0) return;
|
||||||
|
if (await _hasRdclientBuildInProgress()) return;
|
||||||
|
_running = true;
|
||||||
|
try {
|
||||||
|
const pending = await _listPendingRdclientBuilds(1);
|
||||||
|
if (!pending.length) return;
|
||||||
|
const row = pending[0];
|
||||||
|
await db.upsertAgentBundleBuild({
|
||||||
|
brandingHash: row.branding_hash,
|
||||||
|
platform: row.platform,
|
||||||
|
arch: row.arch,
|
||||||
|
format: row.format,
|
||||||
|
status: 'building',
|
||||||
|
artifactPath: row.artifact_path || null,
|
||||||
|
artifactSize: row.artifact_size || 0,
|
||||||
|
artifactSha256: row.artifact_sha256 || null,
|
||||||
|
errorMessage: '',
|
||||||
|
});
|
||||||
|
_activeBuilds++;
|
||||||
|
try {
|
||||||
|
await _runOne(row);
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e.message || String(e);
|
||||||
|
console.error(`[rdclientBuildWorker] build FAILED ${row.platform}/${row.format}: ${msg}`);
|
||||||
|
await db.upsertAgentBundleBuild({
|
||||||
|
brandingHash: row.branding_hash,
|
||||||
|
platform: row.platform,
|
||||||
|
arch: row.arch,
|
||||||
|
format: row.format,
|
||||||
|
status: 'failed',
|
||||||
|
artifactPath: null,
|
||||||
|
artifactSize: 0,
|
||||||
|
artifactSha256: null,
|
||||||
|
errorMessage: msg.slice(0, 2000),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
_activeBuilds--;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startWorker() {
|
||||||
|
if (_pollTimer) return;
|
||||||
|
if (!fs.existsSync(SOURCE_ROOT)) {
|
||||||
|
console.warn('[rdclientBuildWorker] rdclient-desktop source not found — worker disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fsp.mkdir(WORK_ROOT, { recursive: true }).catch(() => {});
|
||||||
|
fsp.mkdir(ARTIFACT_ROOT, { recursive: true }).catch(() => {});
|
||||||
|
_pollTimer = setInterval(() => {
|
||||||
|
_tick().catch((e) => console.error('[rdclientBuildWorker] tick error:', e.message));
|
||||||
|
}, POLL_INTERVAL_MS);
|
||||||
|
console.log(`[rdclientBuildWorker] started (poll ${POLL_INTERVAL_MS}ms)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopWorker() {
|
||||||
|
if (_pollTimer) clearInterval(_pollTimer);
|
||||||
|
_pollTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
startWorker,
|
||||||
|
stopWorker,
|
||||||
|
enqueueBuildsForHash,
|
||||||
|
};
|
||||||
@@ -25,6 +25,10 @@
|
|||||||
<span class="material-icons">add</span>
|
<span class="material-icons">add</span>
|
||||||
${_('generator.new_bundle')}
|
${_('generator.new_bundle')}
|
||||||
</button>
|
</button>
|
||||||
|
<button id="gen-new-rdclient" class="btn btn-secondary btn-sm" title="${_('generator.rdclient_new_bundle')}">
|
||||||
|
<span class="material-icons">connected_tv</span>
|
||||||
|
${_('generator.rdclient_tab')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="gen-bundle-list" class="bundle-list">
|
<div id="gen-bundle-list" class="bundle-list">
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
cacheVersion: '<%= cacheVersion %>',
|
cacheVersion: '<%= cacheVersion %>',
|
||||||
csrfToken: '<%= typeof csrfToken !== "undefined" ? csrfToken : "" %>',
|
csrfToken: '<%= typeof csrfToken !== "undefined" ? csrfToken : "" %>',
|
||||||
user: <%- JSON.stringify(user || null) %>,
|
user: <%- JSON.stringify(user || null) %>,
|
||||||
branding: <%- JSON.stringify(branding || {}) %>
|
branding: <%- JSON.stringify(branding || {}) %>,
|
||||||
|
availableLanguages: <%- JSON.stringify(availableLanguageList || []) %>
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/i18n-client.js?v=<%= cacheVersion %>"></script>
|
<script src="/js/i18n-client.js?v=<%= cacheVersion %>"></script>
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
csrfToken: '<%= typeof csrfToken !== 'undefined' ? csrfToken : '' %>',
|
csrfToken: '<%= typeof csrfToken !== 'undefined' ? csrfToken : '' %>',
|
||||||
user: <%- JSON.stringify(user || null) %>,
|
user: <%- JSON.stringify(user || null) %>,
|
||||||
serverPubKey: '<%= typeof serverPubKey !== 'undefined' ? serverPubKey : '' %>',
|
serverPubKey: '<%= typeof serverPubKey !== 'undefined' ? serverPubKey : '' %>',
|
||||||
branding: <%- JSON.stringify(branding || {}) %>
|
branding: <%- JSON.stringify(branding || {}) %>,
|
||||||
|
availableLanguages: <%- JSON.stringify(availableLanguageList || []) %>
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
|
|
||||||
<!-- Translation helper -->
|
<!-- Translation helper -->
|
||||||
<script src="/js/i18n-client.js?v=<%= cacheVersion %>"></script>
|
<script src="/js/i18n-client.js?v=<%= cacheVersion %>"></script>
|
||||||
|
<script src="/js/rdclientSecureStore.js?v=<%= cacheVersion %>"></script>
|
||||||
|
|
||||||
<!-- Vendor libraries for remote client -->
|
<!-- Vendor libraries for remote client -->
|
||||||
<script src="/js/vendor/protobuf.min.js?v=<%= cacheVersion %>"></script>
|
<script src="/js/vendor/protobuf.min.js?v=<%= cacheVersion %>"></script>
|
||||||
|
|||||||
@@ -19,6 +19,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rdclient-login-shell">
|
<div class="rdclient-login-shell">
|
||||||
|
<div class="rdclient-login-lang">
|
||||||
|
<span class="material-icons" aria-hidden="true">language</span>
|
||||||
|
<select id="rdclient-language-select" data-language-select aria-label="<%= _('settings.language') %>">
|
||||||
|
<% (availableLanguageList || []).forEach(function(language) { %>
|
||||||
|
<option value="<%= language.code %>" <%= lang === language.code ? 'selected' : '' %>><%= language.native || language.name || language.code %></option>
|
||||||
|
<% }); %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="rdclient-login-brand" id="rdclient-login-brand">
|
<div class="rdclient-login-brand" id="rdclient-login-brand">
|
||||||
<span class="material-icons">connected_tv</span>
|
<span class="material-icons">connected_tv</span>
|
||||||
<span><%= appName %></span>
|
<span><%= appName %></span>
|
||||||
@@ -114,7 +122,8 @@
|
|||||||
translations: <%- JSON.stringify(translations || {}) %>,
|
translations: <%- JSON.stringify(translations || {}) %>,
|
||||||
branding: <%- JSON.stringify(branding || {}) %>,
|
branding: <%- JSON.stringify(branding || {}) %>,
|
||||||
returnUrl: <%- JSON.stringify(typeof returnUrl !== 'undefined' ? returnUrl : '/remote') %>,
|
returnUrl: <%- JSON.stringify(typeof returnUrl !== 'undefined' ? returnUrl : '/remote') %>,
|
||||||
sessionExpired: <%= sessionExpired ? 'true' : 'false' %>
|
sessionExpired: <%= sessionExpired ? 'true' : 'false' %>,
|
||||||
|
availableLanguages: <%- JSON.stringify(availableLanguageList || []) %>
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/i18n-client.js?v=<%= cacheVersion %>"></script>
|
<script src="/js/i18n-client.js?v=<%= cacheVersion %>"></script>
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rd-desk-header-actions">
|
<div class="rd-desk-header-actions">
|
||||||
|
<select id="rd-desk-lang" class="rd-desk-lang-select" data-language-select aria-label="${_('settings.language')}"></select>
|
||||||
|
<button type="button" class="rd-desk-icon-btn" id="rd-desk-settings" title="${_('rdclient_settings.open') || 'Settings'}" style="display:none;">
|
||||||
|
<span class="material-icons">settings</span>
|
||||||
|
</button>
|
||||||
<span class="rd-desk-user" id="rd-desk-user"></span>
|
<span class="rd-desk-user" id="rd-desk-user"></span>
|
||||||
<button type="button" class="rd-desk-icon-btn" id="rd-desk-refresh" title="${_('remote_dashboard.sync_now')}">
|
<button type="button" class="rd-desk-icon-btn" id="rd-desk-refresh" title="${_('remote_dashboard.sync_now')}">
|
||||||
<span class="material-icons">sync</span>
|
<span class="material-icons">sync</span>
|
||||||
@@ -39,6 +43,7 @@
|
|||||||
|
|
||||||
<div class="rd-desk-workspace">
|
<div class="rd-desk-workspace">
|
||||||
<aside class="rd-desk-sidebar" id="rd-desk-sidebar">
|
<aside class="rd-desk-sidebar" id="rd-desk-sidebar">
|
||||||
|
<div class="rd-desk-sidebar-scroll" id="rd-desk-sidebar-scroll">
|
||||||
<nav class="rd-desk-nav-section" data-collapsible="folders">
|
<nav class="rd-desk-nav-section" data-collapsible="folders">
|
||||||
<button type="button" class="rd-desk-nav-heading rd-desk-nav-heading-btn" id="rd-desk-nav-folders-toggle" aria-expanded="true">
|
<button type="button" class="rd-desk-nav-heading rd-desk-nav-heading-btn" id="rd-desk-nav-folders-toggle" aria-expanded="true">
|
||||||
<span>${_('remote_dashboard.nav_folders')}</span>
|
<span>${_('remote_dashboard.nav_folders')}</span>
|
||||||
@@ -84,6 +89,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="rd-desk-content">
|
<div class="rd-desk-content">
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<span class="toolbar-stats" id="toolbar-stats"></span>
|
<span class="toolbar-stats" id="toolbar-stats"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
|
<select id="viewer-language-select" class="toolbar-lang-select" data-language-select aria-label="${_('settings.language')}"></select>
|
||||||
<!-- Actions Menu -->
|
<!-- Actions Menu -->
|
||||||
<div class="toolbar-dropdown">
|
<div class="toolbar-dropdown">
|
||||||
<button class="toolbar-btn" id="btn-actions" title="${_('remote.actions')}">
|
<button class="toolbar-btn" id="btn-actions" title="${_('remote.actions')}">
|
||||||
@@ -249,6 +250,10 @@
|
|||||||
placeholder="${_('remote.password_placeholder')}" autocomplete="off">
|
placeholder="${_('remote.password_placeholder')}" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
<p class="login-error session-login-error" style="display:none;"></p>
|
<p class="login-error session-login-error" style="display:none;"></p>
|
||||||
|
<label class="form-check session-remember-peer" style="display:flex;align-items:center;gap:8px;margin:10px 0 0;font-size:0.875rem;">
|
||||||
|
<input type="checkbox" class="session-remember-peer-checkbox">
|
||||||
|
<span>${_('remote.remember_peer_password') || 'Remember device password on this device'}</span>
|
||||||
|
</label>
|
||||||
<button class="btn btn-primary btn-full session-btn-authenticate">
|
<button class="btn btn-primary btn-full session-btn-authenticate">
|
||||||
<span class="material-icons">login</span>
|
<span class="material-icons">login</span>
|
||||||
${_('remote.connect')}
|
${_('remote.connect')}
|
||||||
|
|||||||
Reference in New Issue
Block a user