mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-08 15:18:13 +03:00
Compare commits
113 Commits
779b7aaf02
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ad54fa846 | ||
|
|
383a5c3478 | ||
|
|
d4a1430c27 | ||
|
|
bfd31d21e4 | ||
|
|
590296b297 | ||
|
|
ee8cc0c06b | ||
|
|
99b565ef40 | ||
|
|
1e6a3dc644 | ||
|
|
5b7ad339b8 | ||
|
|
7308c448f1 | ||
|
|
c8ba99d1a1 | ||
|
|
5ea6714db8 | ||
|
|
3a1622e8b5 | ||
|
|
38f1300717 | ||
|
|
03e351ac61 | ||
|
|
6cb323725b | ||
|
|
5d0533f0d4 | ||
|
|
e0c5e1483e | ||
|
|
47e4c65d8e | ||
|
|
9bc1ce52af | ||
|
|
348d1b46e1 | ||
|
|
1a41b3ac11 | ||
|
|
b239535009 | ||
|
|
5fd20f808c | ||
|
|
803ac8cc4e | ||
|
|
4a50bc6fc2 | ||
|
|
e8a1b7fe21 | ||
|
|
ac124c0680 | ||
|
|
91aff3ffd1 | ||
|
|
642c281ad0 | ||
|
|
1e9c4d04f1 | ||
|
|
9f817714fe | ||
|
|
091f2c6135 | ||
|
|
91de51290d | ||
|
|
68fa0466c8 | ||
|
|
28e303576c | ||
|
|
2d41b3e80d | ||
|
|
ffd2d26c1a | ||
|
|
a8dc6fc632 | ||
|
|
771cb4ebd7 | ||
|
|
2f694c0eb2 | ||
|
|
8dea347a21 | ||
|
|
0cf3e8ed40 | ||
|
|
9d3bc7d9e6 | ||
|
|
e0427bdc77 | ||
|
|
9cf1338dc4 | ||
|
|
4e30ee8d1c | ||
|
|
cca6a5fe12 | ||
|
|
9e4b7fca4d | ||
|
|
d135c58ead | ||
|
|
de194417d4 | ||
|
|
d01ce3173f | ||
|
|
010a54d1c9 | ||
|
|
f557fc94fa | ||
|
|
f02cd9c0f6 | ||
|
|
170516572e | ||
|
|
285e29d2dc | ||
|
|
aab34b2338 | ||
|
|
ad1e5330e9 | ||
|
|
ca4647ddd6 | ||
|
|
7004acae46 | ||
|
|
899dd46f5b | ||
|
|
dba5fea66f | ||
|
|
c457b0e7d3 | ||
|
|
c0da4a6645 | ||
|
|
9d8df6a226 | ||
|
|
02da7132e7 | ||
|
|
e3b6e4eaf0 | ||
|
|
0388d00ad3 | ||
|
|
1e2d2c5146 | ||
|
|
96797742f2 | ||
|
|
682e347be0 | ||
|
|
b3f43f55c1 | ||
|
|
016a0b1141 | ||
|
|
fd7bcf54bd | ||
|
|
db3f5fe816 | ||
|
|
0d3016fcd8 | ||
|
|
1abc897c45 | ||
|
|
ab64a32f30 | ||
|
|
52b66e71d1 | ||
|
|
41ab5bbdd8 | ||
|
|
732b250815 | ||
|
|
157dbdc543 | ||
|
|
6ba23683d5 | ||
|
|
80a5865db3 | ||
|
|
9cb6f38aea | ||
|
|
cd7e3e4505 | ||
|
|
1833cb0655 | ||
|
|
e4208aa9cf | ||
|
|
bb3501a4f9 | ||
|
|
4abdb2e08b | ||
|
|
d49ae493b2 | ||
|
|
394079833e | ||
|
|
12d6789c2e | ||
|
|
34803f8e9b | ||
|
|
fd43184406 | ||
|
|
3cc3315081 | ||
|
|
6aee70fa18 | ||
|
|
82a9fd1540 | ||
|
|
dc760d6ca8 | ||
|
|
eb239501bc | ||
|
|
0016033937 | ||
|
|
50c62d5eac | ||
|
|
91ac48912e | ||
|
|
272a6604cd | ||
|
|
8a889d3ebb | ||
|
|
17a3f2ae52 | ||
|
|
6c3515588f | ||
|
|
4d2d2118a2 | ||
|
|
483fe80308 | ||
|
|
34ceeac36e | ||
|
|
20f11018ce | ||
|
|
9345fb754a |
@@ -1,56 +0,0 @@
|
|||||||
You are an expert in prompt engineering, specializing in optimizing AI code assistant instructions. Your task is to analyze and improve the instructions for Claude Code.
|
|
||||||
Follow these steps carefully:
|
|
||||||
|
|
||||||
1. Analysis Phase:
|
|
||||||
Review the chat history in your context window.
|
|
||||||
|
|
||||||
Then, examine the current Claude instructions, commands and config
|
|
||||||
<claude_instructions>
|
|
||||||
/CLAUDE.md
|
|
||||||
/.claude/commands/*
|
|
||||||
**/CLAUDE.md
|
|
||||||
.claude/settings.json
|
|
||||||
.claude/settings.local.json
|
|
||||||
</claude_instructions>
|
|
||||||
|
|
||||||
Analyze the chat history, instructions, commands and config to identify areas that could be improved. Look for:
|
|
||||||
- Inconsistencies in Claude's responses
|
|
||||||
- Misunderstandings of user requests
|
|
||||||
- Areas where Claude could provide more detailed or accurate information
|
|
||||||
- Opportunities to enhance Claude's ability to handle specific types of queries or tasks
|
|
||||||
- New commands or improvements to a commands name, function or response
|
|
||||||
- Permissions and MCPs we've approved locally that we should add to the config, especially if we've added new tools or require them for the command to work
|
|
||||||
|
|
||||||
2. Interaction Phase:
|
|
||||||
Present your findings and improvement ideas to the human. For each suggestion:
|
|
||||||
a) Explain the current issue you've identified
|
|
||||||
b) Propose a specific change or addition to the instructions
|
|
||||||
c) Describe how this change would improve Claude's performance
|
|
||||||
|
|
||||||
Wait for feedback from the human on each suggestion before proceeding. If the human approves a change, move it to the implementation phase. If not, refine your suggestion or move on to the next idea.
|
|
||||||
|
|
||||||
3. Implementation Phase:
|
|
||||||
For each approved change:
|
|
||||||
a) Clearly state the section of the instructions you're modifying
|
|
||||||
b) Present the new or modified text for that section
|
|
||||||
c) Explain how this change addresses the issue identified in the analysis phase
|
|
||||||
|
|
||||||
4. Output Format:
|
|
||||||
Present your final output in the following structure:
|
|
||||||
|
|
||||||
<analysis>
|
|
||||||
[List the issues identified and potential improvements]
|
|
||||||
</analysis>
|
|
||||||
|
|
||||||
<improvements>
|
|
||||||
[For each approved improvement:
|
|
||||||
1. Section being modified
|
|
||||||
2. New or modified instruction text
|
|
||||||
3. Explanation of how this addresses the identified issue]
|
|
||||||
</improvements>
|
|
||||||
|
|
||||||
<final_instructions>
|
|
||||||
[Present the complete, updated set of instructions for Claude, incorporating all approved changes]
|
|
||||||
</final_instructions>
|
|
||||||
|
|
||||||
Remember, your goal is to enhance Claude's performance and consistency while maintaining the core functionality and purpose of the AI assistant. Be thorough in your analysis, clear in your explanations, and precise in your implementations.
|
|
||||||
4
.github/workflows/flutter-build.yml
vendored
4
.github/workflows/flutter-build.yml
vendored
@@ -39,8 +39,8 @@ env:
|
|||||||
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
||||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||||
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
||||||
VERSION: "1.4.5"
|
VERSION: "1.4.6"
|
||||||
NDK_VERSION: "r27c"
|
NDK_VERSION: "r28c"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||||
|
|||||||
2
.github/workflows/playground.yml
vendored
2
.github/workflows/playground.yml
vendored
@@ -17,7 +17,7 @@ env:
|
|||||||
TAG_NAME: "nightly"
|
TAG_NAME: "nightly"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||||
VERSION: "1.4.5"
|
VERSION: "1.4.6"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
|
|||||||
15
.github/workflows/winget.yml
vendored
15
.github/workflows/winget.yml
vendored
@@ -1,15 +0,0 @@
|
|||||||
name: Publish to WinGet
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [released]
|
|
||||||
workflow_dispatch:
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: vedantmgoyal9/winget-releaser@main
|
|
||||||
with:
|
|
||||||
identifier: RustDesk.RustDesk
|
|
||||||
version: "1.4.5"
|
|
||||||
release-tag: "1.4.5"
|
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@
|
|||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
libsciter-gtk.so
|
libsciter-gtk.so
|
||||||
src/ui/inline.rs
|
src/ui/inline.rs
|
||||||
extractor
|
extractor
|
||||||
|
|||||||
62
AGENTS.md
Normal file
62
AGENTS.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# RustDesk Guide
|
||||||
|
|
||||||
|
## Project Layout
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
* `src/` Rust app
|
||||||
|
* `src/server/` audio / clipboard / input / video / network
|
||||||
|
* `src/platform/` platform-specific code
|
||||||
|
* `src/ui/` legacy Sciter UI (deprecated)
|
||||||
|
* `flutter/` current UI
|
||||||
|
* `libs/hbb_common/` config / proto / shared utils
|
||||||
|
* `libs/scrap/` screen capture
|
||||||
|
* `libs/enigo/` input control
|
||||||
|
* `libs/clipboard/` clipboard
|
||||||
|
* `libs/hbb_common/src/config.rs` all options
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server
|
||||||
|
- **Screen Capture**: Platform-specific screen capture in `libs/scrap/`
|
||||||
|
- **Input Handling**: Cross-platform input simulation in `libs/enigo/`
|
||||||
|
- **Audio/Video Services**: Real-time audio/video streaming in `src/server/`
|
||||||
|
- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/`
|
||||||
|
|
||||||
|
### UI Architecture
|
||||||
|
- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/`
|
||||||
|
- **Modern UI**: Flutter-based - files in `flutter/`
|
||||||
|
- Desktop: `flutter/lib/desktop/`
|
||||||
|
- Mobile: `flutter/lib/mobile/`
|
||||||
|
- Shared: `flutter/lib/common/` and `flutter/lib/models/`
|
||||||
|
|
||||||
|
## Rust Rules
|
||||||
|
|
||||||
|
* Avoid `unwrap()` / `expect()` in production code.
|
||||||
|
* Exceptions:
|
||||||
|
|
||||||
|
* tests;
|
||||||
|
* lock acquisition where failure means poisoning, not normal control flow.
|
||||||
|
* Otherwise prefer `Result` + `?` or explicit handling.
|
||||||
|
* Do not ignore errors silently.
|
||||||
|
* Avoid unnecessary `.clone()`.
|
||||||
|
* Prefer borrowing when practical.
|
||||||
|
* Do not add dependencies unless needed.
|
||||||
|
* Keep code simple and idiomatic.
|
||||||
|
|
||||||
|
## Tokio Rules
|
||||||
|
|
||||||
|
* Assume a Tokio runtime already exists.
|
||||||
|
* Never create nested runtimes.
|
||||||
|
* Never call `Runtime::block_on()` inside Tokio / async code.
|
||||||
|
* Do not hide runtime creation inside helpers or libraries.
|
||||||
|
* Do not hold locks across `.await`.
|
||||||
|
* Prefer `.await`, `tokio::spawn`, channels.
|
||||||
|
* Use `spawn_blocking` or dedicated threads for blocking work.
|
||||||
|
* Do not use `std::thread::sleep()` in async code.
|
||||||
|
|
||||||
|
## Editing Hygiene
|
||||||
|
|
||||||
|
* Change only what is required.
|
||||||
|
* Prefer the smallest valid diff.
|
||||||
|
* Do not refactor unrelated code.
|
||||||
|
* Do not make formatting-only changes.
|
||||||
|
* Keep naming/style consistent with nearby code.
|
||||||
92
CLAUDE.md
92
CLAUDE.md
@@ -1,91 +1 @@
|
|||||||
# CLAUDE.md
|
AGENTS.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Development Commands
|
|
||||||
|
|
||||||
### Build Commands
|
|
||||||
- `cargo run` - Build and run the desktop application (requires libsciter library)
|
|
||||||
- `python3 build.py --flutter` - Build Flutter version (desktop)
|
|
||||||
- `python3 build.py --flutter --release` - Build Flutter version in release mode
|
|
||||||
- `python3 build.py --hwcodec` - Build with hardware codec support
|
|
||||||
- `python3 build.py --vram` - Build with VRAM feature (Windows only)
|
|
||||||
- `cargo build --release` - Build Rust binary in release mode
|
|
||||||
- `cargo build --features hwcodec` - Build with specific features
|
|
||||||
|
|
||||||
### Flutter Mobile Commands
|
|
||||||
- `cd flutter && flutter build android` - Build Android APK
|
|
||||||
- `cd flutter && flutter build ios` - Build iOS app
|
|
||||||
- `cd flutter && flutter run` - Run Flutter app in development mode
|
|
||||||
- `cd flutter && flutter test` - Run Flutter tests
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
- `cargo test` - Run Rust tests
|
|
||||||
- `cd flutter && flutter test` - Run Flutter tests
|
|
||||||
|
|
||||||
### Platform-Specific Build Scripts
|
|
||||||
- `flutter/build_android.sh` - Android build script
|
|
||||||
- `flutter/build_ios.sh` - iOS build script
|
|
||||||
- `flutter/build_fdroid.sh` - F-Droid build script
|
|
||||||
|
|
||||||
## Project Architecture
|
|
||||||
|
|
||||||
### Directory Structure
|
|
||||||
- **`src/`** - Main Rust application code
|
|
||||||
- `src/ui/` - Legacy Sciter UI (deprecated, use Flutter instead)
|
|
||||||
- `src/server/` - Audio/clipboard/input/video services and network connections
|
|
||||||
- `src/client.rs` - Peer connection handling
|
|
||||||
- `src/platform/` - Platform-specific code
|
|
||||||
- **`flutter/`** - Flutter UI code for desktop and mobile
|
|
||||||
- **`libs/`** - Core libraries
|
|
||||||
- `libs/hbb_common/` - Video codec, config, network wrapper, protobuf, file transfer utilities
|
|
||||||
- `libs/scrap/` - Screen capture functionality
|
|
||||||
- `libs/enigo/` - Platform-specific keyboard/mouse control
|
|
||||||
- `libs/clipboard/` - Cross-platform clipboard implementation
|
|
||||||
|
|
||||||
### Key Components
|
|
||||||
- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server
|
|
||||||
- **Screen Capture**: Platform-specific screen capture in `libs/scrap/`
|
|
||||||
- **Input Handling**: Cross-platform input simulation in `libs/enigo/`
|
|
||||||
- **Audio/Video Services**: Real-time audio/video streaming in `src/server/`
|
|
||||||
- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/`
|
|
||||||
|
|
||||||
### UI Architecture
|
|
||||||
- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/`
|
|
||||||
- **Modern UI**: Flutter-based - files in `flutter/`
|
|
||||||
- Desktop: `flutter/lib/desktop/`
|
|
||||||
- Mobile: `flutter/lib/mobile/`
|
|
||||||
- Shared: `flutter/lib/common/` and `flutter/lib/models/`
|
|
||||||
|
|
||||||
## Important Build Notes
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
- Requires vcpkg for C++ dependencies: `libvpx`, `libyuv`, `opus`, `aom`
|
|
||||||
- Set `VCPKG_ROOT` environment variable
|
|
||||||
- Download appropriate Sciter library for legacy UI support
|
|
||||||
|
|
||||||
### Ignore Patterns
|
|
||||||
When working with files, ignore these directories:
|
|
||||||
- `target/` - Rust build artifacts
|
|
||||||
- `flutter/build/` - Flutter build output
|
|
||||||
- `flutter/.dart_tool/` - Flutter tooling files
|
|
||||||
|
|
||||||
### Cross-Platform Considerations
|
|
||||||
- Windows builds require additional DLLs and virtual display drivers
|
|
||||||
- macOS builds need proper signing and notarization for distribution
|
|
||||||
- Linux builds support multiple package formats (deb, rpm, AppImage)
|
|
||||||
- Mobile builds require platform-specific toolchains (Android SDK, Xcode)
|
|
||||||
|
|
||||||
### Feature Flags
|
|
||||||
- `hwcodec` - Hardware video encoding/decoding
|
|
||||||
- `vram` - VRAM optimization (Windows only)
|
|
||||||
- `flutter` - Enable Flutter UI
|
|
||||||
- `unix-file-copy-paste` - Unix file clipboard support
|
|
||||||
- `screencapturekit` - macOS ScreenCaptureKit (macOS only)
|
|
||||||
|
|
||||||
### Config
|
|
||||||
All configurations or options are under `libs/hbb_common/src/config.rs` file, 4 types:
|
|
||||||
- Settings
|
|
||||||
- Local
|
|
||||||
- Display
|
|
||||||
- Built-in
|
|
||||||
|
|||||||
358
Cargo.lock
generated
358
Cargo.lock
generated
@@ -33,6 +33,12 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aead"
|
name = "aead"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -293,8 +299,8 @@ dependencies = [
|
|||||||
"image 0.25.1",
|
"image 0.25.1",
|
||||||
"log",
|
"log",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit 0.2.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde 1.0.228",
|
"serde 1.0.228",
|
||||||
@@ -637,7 +643,7 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"libc",
|
"libc",
|
||||||
"miniz_oxide",
|
"miniz_oxide 0.7.4",
|
||||||
"object",
|
"object",
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
@@ -860,6 +866,15 @@ dependencies = [
|
|||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block2"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||||
|
dependencies = [
|
||||||
|
"objc2 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blocking"
|
name = "blocking"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
@@ -1182,7 +1197,7 @@ dependencies = [
|
|||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits 0.2.19",
|
"num-traits 0.2.19",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-link",
|
"windows-link 0.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1290,8 +1305,8 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit 0.2.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@@ -2216,6 +2231,15 @@ dependencies = [
|
|||||||
"dirs-sys 0.4.1",
|
"dirs-sys 0.4.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys 0.5.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-next"
|
name = "dirs-next"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -2233,7 +2257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"redox_users",
|
"redox_users 0.4.5",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2245,10 +2269,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users",
|
"redox_users 0.4.5",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users 0.5.2",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-sys-next"
|
name = "dirs-sys-next"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -2256,7 +2292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"redox_users",
|
"redox_users 0.4.5",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2266,6 +2302,16 @@ 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 = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dispatch2"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"objc2 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -2715,7 +2761,7 @@ dependencies = [
|
|||||||
"flume",
|
"flume",
|
||||||
"half",
|
"half",
|
||||||
"lebe",
|
"lebe",
|
||||||
"miniz_oxide",
|
"miniz_oxide 0.7.4",
|
||||||
"rayon-core",
|
"rayon-core",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"zune-inflate",
|
"zune-inflate",
|
||||||
@@ -2801,12 +2847,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.0.30"
|
version = "1.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"miniz_oxide",
|
"miniz_oxide 0.8.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4041,7 +4087,7 @@ dependencies = [
|
|||||||
"gif",
|
"gif",
|
||||||
"jpeg-decoder",
|
"jpeg-decoder",
|
||||||
"num-traits 0.2.19",
|
"num-traits 0.2.19",
|
||||||
"png",
|
"png 0.17.13",
|
||||||
"qoi",
|
"qoi",
|
||||||
"tiff",
|
"tiff",
|
||||||
]
|
]
|
||||||
@@ -4055,7 +4101,7 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"num-traits 0.2.19",
|
"num-traits 0.2.19",
|
||||||
"png",
|
"png 0.17.13",
|
||||||
"tiff",
|
"tiff",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4766,6 +4812,16 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.11"
|
version = "0.8.11"
|
||||||
@@ -4816,21 +4872,23 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "muda"
|
name = "muda"
|
||||||
version = "0.13.5"
|
version = "0.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145"
|
checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa 0.25.0",
|
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dpi",
|
"dpi",
|
||||||
"gtk",
|
"gtk",
|
||||||
"keyboard-types",
|
"keyboard-types",
|
||||||
"libxdo",
|
"libxdo",
|
||||||
"objc",
|
"objc2 0.6.4",
|
||||||
|
"objc2-app-kit 0.3.2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-foundation 0.3.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png 0.17.13",
|
||||||
"thiserror 1.0.61",
|
"thiserror 2.0.17",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5374,7 +5432,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
|
checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"objc-sys 0.3.5",
|
"objc-sys 0.3.5",
|
||||||
"objc2-encode 4.0.3",
|
"objc2-encode 4.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||||
|
dependencies = [
|
||||||
|
"objc2-encode 4.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5389,10 +5456,22 @@ dependencies = [
|
|||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-core-data",
|
"objc2-core-data",
|
||||||
"objc2-core-image",
|
"objc2-core-image",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
"objc2-quartz-core",
|
"objc2-quartz-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-app-kit"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"objc2 0.6.4",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-foundation 0.3.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-cloud-kit"
|
name = "objc2-cloud-kit"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -5403,7 +5482,7 @@ dependencies = [
|
|||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-core-location",
|
"objc2-core-location",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5414,7 +5493,7 @@ checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5426,7 +5505,28 @@ dependencies = [
|
|||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-foundation"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"dispatch2",
|
||||||
|
"objc2 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-graphics"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5437,7 +5537,7 @@ checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
"objc2-metal",
|
"objc2-metal",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5450,7 +5550,7 @@ dependencies = [
|
|||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-contacts",
|
"objc2-contacts",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5464,9 +5564,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-encode"
|
name = "objc2-encode"
|
||||||
version = "4.0.3"
|
version = "4.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8"
|
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-foundation"
|
name = "objc2-foundation"
|
||||||
@@ -5481,6 +5581,18 @@ dependencies = [
|
|||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-foundation"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"block2 0.6.2",
|
||||||
|
"objc2 0.6.4",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-link-presentation"
|
name = "objc2-link-presentation"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -5489,8 +5601,8 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit 0.2.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5502,7 +5614,7 @@ dependencies = [
|
|||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5514,7 +5626,7 @@ dependencies = [
|
|||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
"objc2-metal",
|
"objc2-metal",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5525,7 +5637,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
|
checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5541,7 +5653,7 @@ dependencies = [
|
|||||||
"objc2-core-data",
|
"objc2-core-data",
|
||||||
"objc2-core-image",
|
"objc2-core-image",
|
||||||
"objc2-core-location",
|
"objc2-core-location",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
"objc2-link-presentation",
|
"objc2-link-presentation",
|
||||||
"objc2-quartz-core",
|
"objc2-quartz-core",
|
||||||
"objc2-symbols",
|
"objc2-symbols",
|
||||||
@@ -5557,7 +5669,7 @@ checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5570,7 +5682,7 @@ dependencies = [
|
|||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-core-location",
|
"objc2-core-location",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6178,7 +6290,20 @@ dependencies = [
|
|||||||
"crc32fast",
|
"crc32fast",
|
||||||
"fdeflate",
|
"fdeflate",
|
||||||
"flate2",
|
"flate2",
|
||||||
"miniz_oxide",
|
"miniz_oxide 0.7.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "png"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"crc32fast",
|
||||||
|
"fdeflate",
|
||||||
|
"flate2",
|
||||||
|
"miniz_oxide 0.8.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6863,6 +6988,17 @@ dependencies = [
|
|||||||
"thiserror 1.0.61",
|
"thiserror 1.0.61",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.15",
|
||||||
|
"libredox",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -7134,7 +7270,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.4.5"
|
version = "1.4.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-wakelock",
|
"android-wakelock",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
@@ -7249,7 +7385,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.4.5"
|
version = "1.4.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@@ -7981,8 +8117,8 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit 0.2.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
"objc2-quartz-core",
|
"objc2-quartz-core",
|
||||||
"raw-window-handle 0.6.2",
|
"raw-window-handle 0.6.2",
|
||||||
"redox_syscall 0.5.2",
|
"redox_syscall 0.5.2",
|
||||||
@@ -8312,7 +8448,7 @@ dependencies = [
|
|||||||
"objc",
|
"objc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"png",
|
"png 0.17.13",
|
||||||
"raw-window-handle 0.6.2",
|
"raw-window-handle 0.6.2",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
"tao-macros",
|
"tao-macros",
|
||||||
@@ -8566,7 +8702,7 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"log",
|
"log",
|
||||||
"png",
|
"png 0.17.13",
|
||||||
"tiny-skia-path",
|
"tiny-skia-path",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8939,21 +9075,22 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tray-icon"
|
name = "tray-icon"
|
||||||
version = "0.14.3"
|
version = "0.21.3"
|
||||||
source = "git+https://github.com/tauri-apps/tray-icon#d4078696edba67b0ab42cef67e6a421a0332c96f"
|
source = "git+https://github.com/tauri-apps/tray-icon#0a5835b0e6828e37a1f781de9c2d671ae7a939e6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-graphics 0.23.2",
|
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dirs 5.0.1",
|
"dirs 6.0.0",
|
||||||
"libappindicator",
|
"libappindicator",
|
||||||
"muda",
|
"muda",
|
||||||
"objc2 0.5.2",
|
"objc2 0.6.4",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit 0.3.2",
|
||||||
"objc2-foundation",
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-foundation 0.3.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png 0.18.1",
|
||||||
"thiserror 1.0.61",
|
"thiserror 2.0.17",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10058,7 +10195,7 @@ dependencies = [
|
|||||||
"windows-collections",
|
"windows-collections",
|
||||||
"windows-core 0.61.0",
|
"windows-core 0.61.0",
|
||||||
"windows-future",
|
"windows-future",
|
||||||
"windows-link",
|
"windows-link 0.1.1",
|
||||||
"windows-numerics",
|
"windows-numerics",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -10107,7 +10244,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement 0.60.0",
|
"windows-implement 0.60.0",
|
||||||
"windows-interface 0.59.1",
|
"windows-interface 0.59.1",
|
||||||
"windows-link",
|
"windows-link 0.1.1",
|
||||||
"windows-result 0.3.2",
|
"windows-result 0.3.2",
|
||||||
"windows-strings 0.4.0",
|
"windows-strings 0.4.0",
|
||||||
]
|
]
|
||||||
@@ -10119,7 +10256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
|
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core 0.61.0",
|
"windows-core 0.61.0",
|
||||||
"windows-link",
|
"windows-link 0.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10172,6 +10309,12 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-numerics"
|
name = "windows-numerics"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -10179,7 +10322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core 0.61.0",
|
"windows-core 0.61.0",
|
||||||
"windows-link",
|
"windows-link 0.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10197,7 +10340,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
|
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10217,7 +10360,7 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10226,7 +10369,7 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
|
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10256,6 +10399,24 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.53.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-targets"
|
name = "windows-targets"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -10295,13 +10456,30 @@ dependencies = [
|
|||||||
"windows_aarch64_gnullvm 0.52.6",
|
"windows_aarch64_gnullvm 0.52.6",
|
||||||
"windows_aarch64_msvc 0.52.6",
|
"windows_aarch64_msvc 0.52.6",
|
||||||
"windows_i686_gnu 0.52.6",
|
"windows_i686_gnu 0.52.6",
|
||||||
"windows_i686_gnullvm",
|
"windows_i686_gnullvm 0.52.6",
|
||||||
"windows_i686_msvc 0.52.6",
|
"windows_i686_msvc 0.52.6",
|
||||||
"windows_x86_64_gnu 0.52.6",
|
"windows_x86_64_gnu 0.52.6",
|
||||||
"windows_x86_64_gnullvm 0.52.6",
|
"windows_x86_64_gnullvm 0.52.6",
|
||||||
"windows_x86_64_msvc 0.52.6",
|
"windows_x86_64_msvc 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.53.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
"windows_aarch64_gnullvm 0.53.1",
|
||||||
|
"windows_aarch64_msvc 0.53.1",
|
||||||
|
"windows_i686_gnu 0.53.1",
|
||||||
|
"windows_i686_gnullvm 0.53.1",
|
||||||
|
"windows_i686_msvc 0.53.1",
|
||||||
|
"windows_x86_64_gnu 0.53.1",
|
||||||
|
"windows_x86_64_gnullvm 0.53.1",
|
||||||
|
"windows_x86_64_msvc 0.53.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-version"
|
name = "windows-version"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -10338,6 +10516,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.32.0"
|
version = "0.32.0"
|
||||||
@@ -10368,6 +10552,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.32.0"
|
version = "0.32.0"
|
||||||
@@ -10398,12 +10588,24 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnullvm"
|
name = "windows_i686_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.32.0"
|
version = "0.32.0"
|
||||||
@@ -10434,6 +10636,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.32.0"
|
version = "0.32.0"
|
||||||
@@ -10464,6 +10672,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -10482,6 +10696,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.32.0"
|
version = "0.32.0"
|
||||||
@@ -10512,6 +10732,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winit"
|
name = "winit"
|
||||||
version = "0.30.9"
|
version = "0.30.9"
|
||||||
@@ -10536,8 +10762,8 @@ dependencies = [
|
|||||||
"memmap2",
|
"memmap2",
|
||||||
"ndk 0.9.0",
|
"ndk 0.9.0",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit 0.2.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
"objc2-ui-kit",
|
"objc2-ui-kit",
|
||||||
"orbclient",
|
"orbclient",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.4.5"
|
version = "1.4.6"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
@@ -160,7 +160,7 @@ piet-coregraphics = "0.6"
|
|||||||
foreign-types = "0.3"
|
foreign-types = "0.3"
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
|
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
|
||||||
tray-icon = { git = "https://github.com/tauri-apps/tray-icon" }
|
tray-icon = { git = "https://github.com/tauri-apps/tray-icon", version = "0.21.3" }
|
||||||
tao = { git = "https://github.com/rustdesk-org/tao", branch = "dev" }
|
tao = { git = "https://github.com/rustdesk-org/tao", branch = "dev" }
|
||||||
image = "0.24"
|
image = "0.24"
|
||||||
|
|
||||||
@@ -245,3 +245,6 @@ panic = 'abort'
|
|||||||
strip = true
|
strip = true
|
||||||
#opt-level = 'z' # only have smaller size after strip
|
#opt-level = 'z' # only have smaller size after strip
|
||||||
rpath = true
|
rpath = true
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
debug = 1
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.4.5
|
version: 1.4.6
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.4.5
|
version: 1.4.6
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
2
build.py
2
build.py
@@ -512,7 +512,7 @@ def main():
|
|||||||
system2('pip3 install -r requirements.txt')
|
system2('pip3 install -r requirements.txt')
|
||||||
system2(
|
system2(
|
||||||
f'python3 ./generate.py -f ../../{res_dir} -o . -e ../../{res_dir}/rustdesk-{version}-win7-install.exe')
|
f'python3 ./generate.py -f ../../{res_dir} -o . -e ../../{res_dir}/rustdesk-{version}-win7-install.exe')
|
||||||
system2('mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..')
|
system2(f'mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..')
|
||||||
elif os.path.isfile('/usr/bin/pacman'):
|
elif os.path.isfile('/usr/bin/pacman'):
|
||||||
# pacman -S -needed base-devel
|
# pacman -S -needed base-devel
|
||||||
system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version)
|
system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version)
|
||||||
|
|||||||
143
docs/CODE_OF_CONDUCT-FR.md
Normal file
143
docs/CODE_OF_CONDUCT-FR.md
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
|
||||||
|
# Code de conduite des contributeurs
|
||||||
|
|
||||||
|
## Notre engagement
|
||||||
|
|
||||||
|
En tant que membres, contributeurs et responsables, nous nous engageons à faire
|
||||||
|
de la participation à notre communauté une expérience exempte de harcèlement pour
|
||||||
|
tous, indépendamment de l'âge, de la taille corporelle, du handicap visible ou
|
||||||
|
invisible, de l'origine ethnique, des caractéristiques sexuelles, de l'identité
|
||||||
|
et de l'expression de genre, du niveau d'expérience, de l'éducation, du statut
|
||||||
|
socio-économique, de la nationalité, de l'apparence personnelle, de la race, de
|
||||||
|
la religion ou de l'identité et de l'orientation sexuelle.
|
||||||
|
|
||||||
|
Nous nous engageons à agir et à interagir de manière à contribuer à une
|
||||||
|
communauté ouverte, accueillante, diversifiée, inclusive et saine.
|
||||||
|
|
||||||
|
## Nos standards
|
||||||
|
|
||||||
|
Exemples de comportements qui contribuent à un environnement positif pour notre
|
||||||
|
communauté :
|
||||||
|
|
||||||
|
* Faire preuve d'empathie et de bienveillance envers les autres
|
||||||
|
* Respecter les opinions, les points de vue et les expériences différents
|
||||||
|
* Donner et accepter gracieusement les retours constructifs
|
||||||
|
* Assumer ses responsabilités, s'excuser auprès des personnes affectées par nos
|
||||||
|
erreurs et apprendre de l'expérience
|
||||||
|
* Se concentrer sur ce qui est le mieux non seulement pour nous en tant
|
||||||
|
qu'individus, mais pour l'ensemble de la communauté
|
||||||
|
|
||||||
|
Exemples de comportements inacceptables :
|
||||||
|
|
||||||
|
* L'utilisation de langage ou d'images à caractère sexuel, et les attentions ou
|
||||||
|
avances sexuelles de quelque nature que ce soit
|
||||||
|
* Le trolling, les commentaires insultants ou désobligeants, et les attaques
|
||||||
|
personnelles ou politiques
|
||||||
|
* Le harcèlement public ou privé
|
||||||
|
* La publication d'informations privées d'autrui, telles qu'une adresse physique
|
||||||
|
ou électronique, sans autorisation explicite
|
||||||
|
* Tout autre comportement qui pourrait raisonnablement être considéré comme
|
||||||
|
inapproprié dans un cadre professionnel
|
||||||
|
|
||||||
|
## Responsabilités en matière d'application
|
||||||
|
|
||||||
|
Les responsables de la communauté sont chargés de clarifier et d'appliquer nos
|
||||||
|
standards de comportement acceptable et prendront des mesures correctives
|
||||||
|
appropriées et équitables en réponse à tout comportement qu'ils jugent
|
||||||
|
inapproprié, menaçant, offensant ou nuisible.
|
||||||
|
|
||||||
|
Les responsables de la communauté ont le droit et la responsabilité de
|
||||||
|
supprimer, modifier ou rejeter les commentaires, commits, code, modifications
|
||||||
|
du wiki, issues et autres contributions qui ne sont pas conformes à ce Code de
|
||||||
|
conduite, et communiqueront les raisons de leurs décisions de modération le cas
|
||||||
|
échéant.
|
||||||
|
|
||||||
|
## Portée
|
||||||
|
|
||||||
|
Ce Code de conduite s'applique dans tous les espaces communautaires, et
|
||||||
|
s'applique également lorsqu'une personne représente officiellement la communauté
|
||||||
|
dans les espaces publics. Les exemples de représentation de notre communauté
|
||||||
|
incluent l'utilisation d'une adresse e-mail officielle, la publication via un
|
||||||
|
compte de réseau social officiel, ou le fait d'agir en tant que représentant
|
||||||
|
désigné lors d'un événement en ligne ou hors ligne.
|
||||||
|
|
||||||
|
## Application
|
||||||
|
|
||||||
|
Les cas de comportements abusifs, harcelants ou autrement inacceptables peuvent
|
||||||
|
être signalés aux responsables de la communauté chargés de l'application à
|
||||||
|
[info@rustdesk.com](mailto:info@rustdesk.com).
|
||||||
|
Toutes les plaintes seront examinées et feront l'objet d'une enquête rapide et
|
||||||
|
équitable.
|
||||||
|
|
||||||
|
Tous les responsables de la communauté sont tenus de respecter la vie privée et
|
||||||
|
la sécurité de la personne ayant signalé un incident.
|
||||||
|
|
||||||
|
## Directives d'application
|
||||||
|
|
||||||
|
Les responsables de la communauté suivront ces Directives d'impact communautaire
|
||||||
|
pour déterminer les conséquences de toute action qu'ils jugent en violation de ce
|
||||||
|
Code de conduite :
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Impact communautaire** : Utilisation d'un langage inapproprié ou autre
|
||||||
|
comportement jugé non professionnel ou indésirable dans la communauté.
|
||||||
|
|
||||||
|
**Conséquence** : Un avertissement écrit et privé de la part des responsables de
|
||||||
|
la communauté, expliquant la nature de la violation et pourquoi le comportement
|
||||||
|
était inapproprié. Des excuses publiques peuvent être demandées.
|
||||||
|
|
||||||
|
### 2. Avertissement
|
||||||
|
|
||||||
|
**Impact communautaire** : Une violation par un incident isolé ou une série
|
||||||
|
d'actions.
|
||||||
|
|
||||||
|
**Conséquence** : Un avertissement avec des conséquences en cas de comportement
|
||||||
|
répété. Aucune interaction avec les personnes impliquées, y compris les
|
||||||
|
interactions non sollicitées avec les personnes chargées d'appliquer le Code de
|
||||||
|
conduite, pendant une période déterminée. Cela inclut d'éviter les interactions
|
||||||
|
dans les espaces communautaires ainsi que dans les canaux externes comme les
|
||||||
|
réseaux sociaux. Le non-respect de ces conditions peut entraîner une exclusion
|
||||||
|
temporaire ou permanente.
|
||||||
|
|
||||||
|
### 3. Exclusion temporaire
|
||||||
|
|
||||||
|
**Impact communautaire** : Une violation grave des standards communautaires, y
|
||||||
|
compris un comportement inapproprié persistant.
|
||||||
|
|
||||||
|
**Conséquence** : Une exclusion temporaire de toute interaction ou communication
|
||||||
|
publique avec la communauté pendant une période déterminée. Aucune interaction
|
||||||
|
publique ou privée avec les personnes impliquées, y compris les interactions non
|
||||||
|
sollicitées avec les personnes chargées d'appliquer le Code de conduite, n'est
|
||||||
|
autorisée pendant cette période. Le non-respect de ces conditions peut entraîner
|
||||||
|
une exclusion permanente.
|
||||||
|
|
||||||
|
### 4. Exclusion permanente
|
||||||
|
|
||||||
|
**Impact communautaire** : Démontrer un schéma de violation des standards
|
||||||
|
communautaires, y compris un comportement inapproprié persistant, le harcèlement
|
||||||
|
d'une personne, ou une agression envers des catégories de personnes ou leur
|
||||||
|
dénigrement.
|
||||||
|
|
||||||
|
**Conséquence** : Une exclusion permanente de toute interaction publique au sein
|
||||||
|
de la communauté.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
Ce Code de conduite est adapté du [Contributor Covenant][homepage], version 2.0,
|
||||||
|
disponible à l'adresse
|
||||||
|
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||||
|
|
||||||
|
Les Directives d'impact communautaire ont été inspirées par
|
||||||
|
[l'échelle d'application du code de conduite de Mozilla][Mozilla CoC].
|
||||||
|
|
||||||
|
Pour des réponses aux questions fréquentes sur ce code de conduite, consultez la
|
||||||
|
FAQ à l'adresse [https://www.contributor-covenant.org/faq][FAQ]. Des traductions
|
||||||
|
sont disponibles à l'adresse
|
||||||
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
55
docs/CONTRIBUTING-FR.md
Normal file
55
docs/CONTRIBUTING-FR.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
# Contribuer à RustDesk
|
||||||
|
|
||||||
|
RustDesk accueille les contributions de tous. Voici les directives si vous
|
||||||
|
envisagez de nous aider :
|
||||||
|
|
||||||
|
## Contributions
|
||||||
|
|
||||||
|
Les contributions à RustDesk ou à ses dépendances doivent être soumises sous
|
||||||
|
forme de pull requests GitHub. Chaque pull request sera examinée par un
|
||||||
|
contributeur principal (une personne ayant la permission d'intégrer des
|
||||||
|
correctifs) et sera soit intégrée dans la branche principale, soit accompagnée
|
||||||
|
de retours sur les modifications requises. Toutes les contributions doivent
|
||||||
|
suivre ce format, même celles des contributeurs principaux.
|
||||||
|
|
||||||
|
Si vous souhaitez travailler sur une issue, veuillez d'abord la revendiquer en
|
||||||
|
commentant sur l'issue GitHub indiquant que vous souhaitez la traiter. Cela
|
||||||
|
permet d'éviter les efforts en double de la part des contributeurs sur la même
|
||||||
|
issue.
|
||||||
|
|
||||||
|
## Liste de vérification pour les pull requests
|
||||||
|
|
||||||
|
- Partez de la branche master et, si nécessaire, effectuez un rebase sur la
|
||||||
|
branche master actuelle avant de soumettre votre pull request. Si elle ne
|
||||||
|
fusionne pas proprement avec master, il vous sera peut-être demandé de
|
||||||
|
rebaser vos modifications.
|
||||||
|
|
||||||
|
- Les commits doivent être aussi petits que possible, tout en s'assurant que
|
||||||
|
chaque commit est correct de manière indépendante (c.-à-d. que chaque commit
|
||||||
|
doit compiler et passer les tests).
|
||||||
|
|
||||||
|
- Les commits doivent être accompagnés d'une signature Developer Certificate of
|
||||||
|
Origin (http://developercertificate.org), indiquant que vous (et votre
|
||||||
|
employeur le cas échéant) acceptez d'être liés par les termes de la
|
||||||
|
[licence du projet](../LICENCE). Dans git, il s'agit de l'option `-s` de
|
||||||
|
`git commit`.
|
||||||
|
|
||||||
|
- Si votre correctif n'est pas examiné ou si vous avez besoin qu'une personne
|
||||||
|
spécifique l'examine, vous pouvez @-mentionner un relecteur pour demander une
|
||||||
|
revue dans la pull request ou un commentaire, ou vous pouvez demander une
|
||||||
|
revue par [e-mail](mailto:info@rustdesk.com).
|
||||||
|
|
||||||
|
- Ajoutez des tests relatifs au bug corrigé ou à la nouvelle fonctionnalité.
|
||||||
|
|
||||||
|
Pour des instructions git spécifiques, consultez le
|
||||||
|
[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
|
||||||
|
|
||||||
|
## Conduite
|
||||||
|
|
||||||
|
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
|
||||||
|
Les contributeurs de RustDesk se retrouvent fréquemment sur
|
||||||
|
[Discord](https://discord.gg/nDceKgxnkV).
|
||||||
@@ -34,9 +34,9 @@ Les versions de bureau utilisent [sciter](https://sciter.com/) pour l'interface
|
|||||||
- Installez [vcpkg](https://github.com/microsoft/vcpkg), et définissez correctement la variable d'environnement `VCPKG_ROOT`.
|
- Installez [vcpkg](https://github.com/microsoft/vcpkg), et définissez correctement la variable d'environnement `VCPKG_ROOT`.
|
||||||
|
|
||||||
- Windows : vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
- Windows : vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
||||||
- Linux/Osx : vcpkg install libvpx libyuv opus aom
|
- Linux/macOS : vcpkg install libvpx libyuv opus aom
|
||||||
|
|
||||||
- Exécuter `cargo run`
|
- Exécutez `cargo run`
|
||||||
|
|
||||||
## Comment compiler/build sous Linux
|
## Comment compiler/build sous Linux
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ cd rustdesk
|
|||||||
mkdir -p target/debug
|
mkdir -p target/debug
|
||||||
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||||
mv libsciter-gtk.so target/debug
|
mv libsciter-gtk.so target/debug
|
||||||
Exécution du cargo
|
cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
## Comment construire avec Docker
|
## Comment construire avec Docker
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ target/release/rustdesk
|
|||||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графический пользовательский интерфейс на Sciter (устаревшее)
|
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графический пользовательский интерфейс на Sciter (устаревшее)
|
||||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервисы аудио, буфера обмена, ввода, видео и сетевых подключений
|
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервисы аудио, буфера обмена, ввода, видео и сетевых подключений
|
||||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: одноранговое соединение
|
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: одноранговое соединение
|
||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером Rustdesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером RustDesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для ПК-версии и мобильных устройств
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для ПК-версии и мобильных устройств
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript для Web-клиента Flutter
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript для Web-клиента Flutter
|
||||||
|
|||||||
16
docs/SECURITY-FR.md
Normal file
16
docs/SECURITY-FR.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
# Politique de sécurité
|
||||||
|
|
||||||
|
## Signaler une vulnérabilité
|
||||||
|
|
||||||
|
Nous accordons une très grande importance à la sécurité du projet. Nous
|
||||||
|
encourageons tous les utilisateurs à nous signaler toute vulnérabilité qu'ils
|
||||||
|
découvrent.
|
||||||
|
|
||||||
|
Si vous trouvez une vulnérabilité de sécurité dans le projet RustDesk, veuillez
|
||||||
|
la signaler de manière responsable en envoyant un e-mail à info@rustdesk.com.
|
||||||
|
|
||||||
|
À ce stade, nous n'avons pas de programme de bug bounty. Nous sommes une petite
|
||||||
|
équipe qui s'attaque à un grand défi. Nous vous encourageons vivement à signaler
|
||||||
|
toute vulnérabilité de manière responsable afin que nous puissions continuer à
|
||||||
|
développer une application sécurisée pour l'ensemble de la communauté.
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<li> Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs. </li>
|
<li> Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs. </li>
|
||||||
<li> Own your data, easily set up self-hosting solution on your infrastructure. </li>
|
<li> Own your data, easily set up self-hosting solution on your infrastructure. </li>
|
||||||
<li> P2P connection with end-to-end encryption based on NaCl. </li>
|
<li> P2P connection with end-to-end encryption based on NaCl. </li>
|
||||||
<li> No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand. </li>
|
<li> No administrative privileges or installation needed for Windows, elevate privilege locally or from remote on demand. </li>
|
||||||
<li> We like to keep things simple and will strive to make simpler where possible. </li>
|
<li> We like to keep things simple and will strive to make simpler where possible. </li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
@@ -56,4 +56,4 @@
|
|||||||
<control>pointing</control>
|
<control>pointing</control>
|
||||||
</supports>
|
</supports>
|
||||||
<content_rating type="oars-1.1"/>
|
<content_rating type="oars-1.1"/>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -62,7 +62,13 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
audioRecorder = builder.build()
|
val recorder = try {
|
||||||
|
builder.build()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(logTag, "createAudioRecorder failed", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
audioRecorder = recorder
|
||||||
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
|
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,7 +311,10 @@ class FloatingWindowService : Service(), View.OnTouchListener {
|
|||||||
popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard"))
|
popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard"))
|
||||||
}
|
}
|
||||||
val idStopService = 2
|
val idStopService = 2
|
||||||
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
|
val hideStopService = FFI.getBuildinOption("hide-stop-service") == "Y"
|
||||||
|
if (!hideStopService) {
|
||||||
|
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
|
||||||
|
}
|
||||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||||
when (menuItem.itemId) {
|
when (menuItem.itemId) {
|
||||||
idShowRustDesk -> {
|
idShowRustDesk -> {
|
||||||
@@ -389,4 +392,3 @@ class FloatingWindowService : Service(), View.OnTouchListener {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ object FFI {
|
|||||||
external fun setFrameRawEnable(name: String, value: Boolean)
|
external fun setFrameRawEnable(name: String, value: Boolean)
|
||||||
external fun setCodecInfo(info: String)
|
external fun setCodecInfo(info: String)
|
||||||
external fun getLocalOption(key: String): String
|
external fun getLocalOption(key: String): String
|
||||||
|
external fun getBuildinOption(key: String): String
|
||||||
external fun onClipboardUpdate(clips: ByteBuffer)
|
external fun onClipboardUpdate(clips: ByteBuffer)
|
||||||
external fun isServiceClipboardEnabled(): Boolean
|
external fun isServiceClipboardEnabled(): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
1
flutter/assets/auth-microsoft.svg
Normal file
1
flutter/assets/auth-microsoft.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><rect x="4" y="4" width="19" height="19" fill="#f25022"/><rect x="25" y="4" width="19" height="19" fill="#7fba00"/><rect x="4" y="25" width="19" height="19" fill="#00a4ef"/><rect x="25" y="25" width="19" height="19" fill="#ffb900"/></svg>
|
||||||
|
After Width: | Height: | Size: 321 B |
@@ -7,7 +7,7 @@
|
|||||||
# 2024, Vasyl Gello <vasek.gello@gmail.com>
|
# 2024, Vasyl Gello <vasek.gello@gmail.com>
|
||||||
#
|
#
|
||||||
|
|
||||||
# The script is invoked by F-Droid builder system ste-by-step.
|
# The script is invoked by F-Droid builder system step-by-step.
|
||||||
#
|
#
|
||||||
# It accepts the following arguments:
|
# It accepts the following arguments:
|
||||||
#
|
#
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64
|
# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64
|
||||||
# - The build step to execute:
|
# - The build step to execute:
|
||||||
#
|
#
|
||||||
# + sudo-deps: as root, install needed Debian packages into builder VM
|
|
||||||
# + prebuild: patch sources and do other stuff before the build
|
# + prebuild: patch sources and do other stuff before the build
|
||||||
# + build: perform actual build of APK file
|
# + build: perform actual build of APK file
|
||||||
#
|
#
|
||||||
@@ -184,13 +183,9 @@ prebuild)
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Map NDK version to revision
|
# Map NDK version to revision
|
||||||
|
NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json |
|
||||||
NDK_VERSION="$(wget \
|
jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" |
|
||||||
-qO- \
|
sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')"
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
||||||
'https://api.github.com/repos/android/ndk/releases' |
|
|
||||||
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
|
||||||
|
|
||||||
if [ -z "${NDK_VERSION}" ]; then
|
if [ -z "${NDK_VERSION}" ]; then
|
||||||
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||||
@@ -316,6 +311,18 @@ prebuild)
|
|||||||
# `FLUTTER_BRIDGE_VERSION` an restore the pubspec later
|
# `FLUTTER_BRIDGE_VERSION` an restore the pubspec later
|
||||||
|
|
||||||
if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then
|
if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then
|
||||||
|
# Find first libclang.so and set BRIDGE_LLVM_PATH
|
||||||
|
|
||||||
|
BRIDGE_LLVM_PATH="$(find /usr/lib/ -name libclang.so | head -n1)"
|
||||||
|
|
||||||
|
if [ -z "${BRIDGE_LLVM_PATH}" ]; then
|
||||||
|
echo 'ERROR: Can not find libclang.so for bridge generator!' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")"
|
||||||
|
BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")"
|
||||||
|
|
||||||
# Install Flutter bridge version
|
# Install Flutter bridge version
|
||||||
|
|
||||||
prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter"
|
prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter"
|
||||||
@@ -344,7 +351,8 @@ prebuild)
|
|||||||
|
|
||||||
flutter_rust_bridge_codegen \
|
flutter_rust_bridge_codegen \
|
||||||
--rust-input ./src/flutter_ffi.rs \
|
--rust-input ./src/flutter_ffi.rs \
|
||||||
--dart-output ./flutter/lib/generated_bridge.dart
|
--dart-output ./flutter/lib/generated_bridge.dart \
|
||||||
|
--llvm-path "${BRIDGE_LLVM_PATH}"
|
||||||
|
|
||||||
# Add bridge files to save-list
|
# Add bridge files to save-list
|
||||||
|
|
||||||
@@ -355,13 +363,15 @@ prebuild)
|
|||||||
git checkout '*'
|
git checkout '*'
|
||||||
git clean -dffx
|
git clean -dffx
|
||||||
git reset
|
git reset
|
||||||
|
|
||||||
|
unset BRIDGE_LLVM_PATH
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install Flutter version for RustDesk library build
|
# Install Flutter version for RustDesk library build
|
||||||
|
|
||||||
prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter"
|
prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter"
|
||||||
|
|
||||||
# gms is not in thoes files now, but we still keep the following line for future reference(maybe).
|
# gms is not in these files now, but we still keep the following line for future reference(maybe).
|
||||||
|
|
||||||
sed \
|
sed \
|
||||||
-i \
|
-i \
|
||||||
@@ -414,13 +424,9 @@ build)
|
|||||||
.github/workflows/flutter-build.yml)"
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
# Map NDK version to revision
|
# Map NDK version to revision
|
||||||
|
NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json |
|
||||||
NDK_VERSION="$(wget \
|
jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" |
|
||||||
-qO- \
|
sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')"
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
||||||
'https://api.github.com/repos/android/ndk/releases' |
|
|
||||||
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
|
||||||
|
|
||||||
if [ -z "${NDK_VERSION}" ]; then
|
if [ -z "${NDK_VERSION}" ]; then
|
||||||
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||||
|
|||||||
@@ -2365,6 +2365,19 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
|||||||
id = uri.path.substring("/new/".length);
|
id = uri.path.substring("/new/".length);
|
||||||
} else if (uri.authority == "config") {
|
} else if (uri.authority == "config") {
|
||||||
if (isAndroid || isIOS) {
|
if (isAndroid || isIOS) {
|
||||||
|
final allowDeepLinkServerSettings =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionAllowDeepLinkServerSettings) ==
|
||||||
|
'Y';
|
||||||
|
if (!allowDeepLinkServerSettings) {
|
||||||
|
debugPrint(
|
||||||
|
"Ignore rustdesk://config because $kOptionAllowDeepLinkServerSettings is not enabled.");
|
||||||
|
// Keep the user-facing error generic; detailed rejection reason is in debug logs.
|
||||||
|
// Delay toast to avoid missing overlay during cold-start deeplink handling.
|
||||||
|
Timer(Duration(seconds: 1), () {
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
final config = uri.path.substring("/".length);
|
final config = uri.path.substring("/".length);
|
||||||
// add a timer to make showToast work
|
// add a timer to make showToast work
|
||||||
Timer(Duration(seconds: 1), () {
|
Timer(Duration(seconds: 1), () {
|
||||||
@@ -2374,11 +2387,24 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
|||||||
return null;
|
return null;
|
||||||
} else if (uri.authority == "password") {
|
} else if (uri.authority == "password") {
|
||||||
if (isAndroid || isIOS) {
|
if (isAndroid || isIOS) {
|
||||||
|
final allowDeepLinkPassword =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionAllowDeepLinkPassword) == 'Y';
|
||||||
|
if (!allowDeepLinkPassword) {
|
||||||
|
debugPrint(
|
||||||
|
"Ignore rustdesk://password because $kOptionAllowDeepLinkPassword is not enabled.");
|
||||||
|
// Keep the user-facing error generic; detailed rejection reason is in debug logs.
|
||||||
|
// Delay toast to avoid missing overlay during cold-start deeplink handling.
|
||||||
|
Timer(Duration(seconds: 1), () {
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
final password = uri.path.substring("/".length);
|
final password = uri.path.substring("/".length);
|
||||||
if (password.isNotEmpty) {
|
if (password.isNotEmpty) {
|
||||||
Timer(Duration(seconds: 1), () async {
|
Timer(Duration(seconds: 1), () async {
|
||||||
await bind.mainSetPermanentPassword(password: password);
|
final ok =
|
||||||
showToast(translate('Successful'));
|
await bind.mainSetPermanentPasswordWithResult(password: password);
|
||||||
|
showToast(translate(ok ? 'Successful' : 'Failed'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3063,6 +3089,11 @@ Future<void> start_service(bool is_start) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> canBeBlocked() async {
|
Future<bool> canBeBlocked() async {
|
||||||
|
if (isWeb) {
|
||||||
|
// Web can only act as a controller, never as a controlled side,
|
||||||
|
// so it should never be blocked by a remote session.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// First check control permission
|
// First check control permission
|
||||||
final controlPermission = await bind.mainGetCommon(
|
final controlPermission = await bind.mainGetCommon(
|
||||||
key: "is-remote-modify-enabled-by-control-permissions");
|
key: "is-remote-modify-enabled-by-control-permissions");
|
||||||
@@ -4113,3 +4144,43 @@ String mouseButtonsToPeer(int buttons) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build an avatar widget from an avatar URL or data URI string.
|
||||||
|
/// Returns [fallback] if avatar is empty or cannot be decoded.
|
||||||
|
/// [borderRadius] defaults to [size]/2 (circle).
|
||||||
|
Widget? buildAvatarWidget({
|
||||||
|
required String avatar,
|
||||||
|
required double size,
|
||||||
|
double? borderRadius,
|
||||||
|
Widget? fallback,
|
||||||
|
}) {
|
||||||
|
final trimmed = avatar.trim();
|
||||||
|
if (trimmed.isEmpty) return fallback;
|
||||||
|
|
||||||
|
ImageProvider? imageProvider;
|
||||||
|
if (trimmed.startsWith('data:image/')) {
|
||||||
|
final comma = trimmed.indexOf(',');
|
||||||
|
if (comma > 0) {
|
||||||
|
try {
|
||||||
|
imageProvider = MemoryImage(base64Decode(trimmed.substring(comma + 1)));
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
} else if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||||||
|
imageProvider = NetworkImage(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageProvider == null) return fallback;
|
||||||
|
|
||||||
|
final radius = borderRadius ?? size / 2;
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(radius),
|
||||||
|
child: Image(
|
||||||
|
image: imageProvider,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) =>
|
||||||
|
fallback ?? SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ enum UserStatus { kDisabled, kNormal, kUnverified }
|
|||||||
// Is all the fields of the user needed?
|
// Is all the fields of the user needed?
|
||||||
class UserPayload {
|
class UserPayload {
|
||||||
String name = '';
|
String name = '';
|
||||||
|
String displayName = '';
|
||||||
|
String avatar = '';
|
||||||
String email = '';
|
String email = '';
|
||||||
String note = '';
|
String note = '';
|
||||||
String? verifier;
|
String? verifier;
|
||||||
@@ -33,6 +35,8 @@ class UserPayload {
|
|||||||
|
|
||||||
UserPayload.fromJson(Map<String, dynamic> json)
|
UserPayload.fromJson(Map<String, dynamic> json)
|
||||||
: name = json['name'] ?? '',
|
: name = json['name'] ?? '',
|
||||||
|
displayName = json['display_name'] ?? '',
|
||||||
|
avatar = json['avatar'] ?? '',
|
||||||
email = json['email'] ?? '',
|
email = json['email'] ?? '',
|
||||||
note = json['note'] ?? '',
|
note = json['note'] ?? '',
|
||||||
verifier = json['verifier'],
|
verifier = json['verifier'],
|
||||||
@@ -46,6 +50,8 @@ class UserPayload {
|
|||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final Map<String, dynamic> map = {
|
final Map<String, dynamic> map = {
|
||||||
'name': name,
|
'name': name,
|
||||||
|
'display_name': displayName,
|
||||||
|
'avatar': avatar,
|
||||||
'status': status == UserStatus.kDisabled
|
'status': status == UserStatus.kDisabled
|
||||||
? 0
|
? 0
|
||||||
: status == UserStatus.kUnverified
|
: status == UserStatus.kUnverified
|
||||||
@@ -58,9 +64,14 @@ class UserPayload {
|
|||||||
Map<String, dynamic> toGroupCacheJson() {
|
Map<String, dynamic> toGroupCacheJson() {
|
||||||
final Map<String, dynamic> map = {
|
final Map<String, dynamic> map = {
|
||||||
'name': name,
|
'name': name,
|
||||||
|
'display_name': displayName,
|
||||||
};
|
};
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get displayNameOrName {
|
||||||
|
return displayName.trim().isEmpty ? name : displayName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PeerPayload {
|
class PeerPayload {
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
const LinearProgressIndicator(),
|
const LinearProgressIndicator(),
|
||||||
buildErrorBanner(context,
|
buildErrorBanner(context,
|
||||||
loading: gFFI.abModel.currentAbLoading,
|
loading: gFFI.abModel.currentAbLoading,
|
||||||
err: gFFI.abModel.currentAbPullError,
|
err: gFFI.abModel.abPullError,
|
||||||
retry: null,
|
retry: null,
|
||||||
close: () => gFFI.abModel.currentAbPullError.value = ''),
|
close: gFFI.abModel.clearPullErrors),
|
||||||
buildErrorBanner(context,
|
buildErrorBanner(context,
|
||||||
loading: gFFI.abModel.currentAbLoading,
|
loading: gFFI.abModel.currentAbLoading,
|
||||||
err: gFFI.abModel.currentAbPushError,
|
err: gFFI.abModel.currentAbPushError,
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ const kOpSvgList = [
|
|||||||
'okta',
|
'okta',
|
||||||
'facebook',
|
'facebook',
|
||||||
'azure',
|
'azure',
|
||||||
'auth0'
|
'auth0',
|
||||||
|
'microsoft'
|
||||||
];
|
];
|
||||||
|
|
||||||
class _IconOP extends StatelessWidget {
|
class _IconOP extends StatelessWidget {
|
||||||
@@ -224,21 +225,59 @@ class _WidgetOPState extends State<WidgetOP> {
|
|||||||
return Offstage(
|
return Offstage(
|
||||||
offstage:
|
offstage:
|
||||||
_failedMsg.isEmpty && widget.curOP.value != widget.config.op,
|
_failedMsg.isEmpty && widget.curOP.value != widget.config.op,
|
||||||
child: RichText(
|
child: Column(
|
||||||
text: TextSpan(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
text: '$_stateMsg ',
|
children: [
|
||||||
style:
|
if (_stateMsg.isNotEmpty && _failedMsg.isEmpty)
|
||||||
DefaultTextStyle.of(context).style.copyWith(fontSize: 12),
|
Padding(
|
||||||
children: <TextSpan>[
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
TextSpan(
|
child: SelectableText(
|
||||||
text: _failedMsg,
|
translate(_stateMsg),
|
||||||
style: DefaultTextStyle.of(context).style.copyWith(
|
style: DefaultTextStyle.of(context)
|
||||||
fontSize: 14,
|
.style
|
||||||
color: Colors.red,
|
.copyWith(fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
if (_failedMsg.isNotEmpty)
|
||||||
),
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Builder(builder: (context) {
|
||||||
|
final errorColor =
|
||||||
|
Theme.of(context).colorScheme.error;
|
||||||
|
final bgColor = Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.errorContainer
|
||||||
|
.withOpacity(0.3);
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8.0, vertical: 6.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor,
|
||||||
|
borderRadius: BorderRadius.circular(4.0),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline,
|
||||||
|
color: errorColor, size: 16),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Flexible(
|
||||||
|
child: SelectableText(
|
||||||
|
translate(_failedMsg),
|
||||||
|
style: DefaultTextStyle.of(context)
|
||||||
|
.style
|
||||||
|
.copyWith(
|
||||||
|
fontSize: 13,
|
||||||
|
color: errorColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -158,12 +158,18 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
return Obx(() {
|
return Obx(() {
|
||||||
final userItems = gFFI.groupModel.users.where((p0) {
|
final userItems = gFFI.groupModel.users.where((p0) {
|
||||||
if (searchAccessibleItemNameText.isNotEmpty) {
|
if (searchAccessibleItemNameText.isNotEmpty) {
|
||||||
return p0.name
|
final search = searchAccessibleItemNameText.value.toLowerCase();
|
||||||
.toLowerCase()
|
return p0.name.toLowerCase().contains(search) ||
|
||||||
.contains(searchAccessibleItemNameText.value.toLowerCase());
|
p0.displayNameOrName.toLowerCase().contains(search);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
// Count occurrences of each displayNameOrName to detect duplicates
|
||||||
|
final displayNameCount = <String, int>{};
|
||||||
|
for (final u in userItems) {
|
||||||
|
final dn = u.displayNameOrName;
|
||||||
|
displayNameCount[dn] = (displayNameCount[dn] ?? 0) + 1;
|
||||||
|
}
|
||||||
final deviceGroupItems = gFFI.groupModel.deviceGroups.where((p0) {
|
final deviceGroupItems = gFFI.groupModel.deviceGroups.where((p0) {
|
||||||
if (searchAccessibleItemNameText.isNotEmpty) {
|
if (searchAccessibleItemNameText.isNotEmpty) {
|
||||||
return p0.name
|
return p0.name
|
||||||
@@ -177,7 +183,8 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
itemCount: deviceGroupItems.length + userItems.length,
|
itemCount: deviceGroupItems.length + userItems.length,
|
||||||
itemBuilder: (context, index) => index < deviceGroupItems.length
|
itemBuilder: (context, index) => index < deviceGroupItems.length
|
||||||
? _buildDeviceGroupItem(deviceGroupItems[index])
|
? _buildDeviceGroupItem(deviceGroupItems[index])
|
||||||
: _buildUserItem(userItems[index - deviceGroupItems.length]));
|
: _buildUserItem(userItems[index - deviceGroupItems.length],
|
||||||
|
displayNameCount));
|
||||||
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||||
return Obx(() => stateGlobal.isPortrait.isFalse
|
return Obx(() => stateGlobal.isPortrait.isFalse
|
||||||
? listView(false)
|
? listView(false)
|
||||||
@@ -185,8 +192,14 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUserItem(UserPayload user) {
|
Widget _buildUserItem(UserPayload user, Map<String, int> displayNameCount) {
|
||||||
final username = user.name;
|
final username = user.name;
|
||||||
|
final dn = user.displayNameOrName;
|
||||||
|
final isDuplicate = (displayNameCount[dn] ?? 0) > 1;
|
||||||
|
final displayName =
|
||||||
|
isDuplicate && user.displayName.trim().isNotEmpty
|
||||||
|
? '${user.displayName} (@$username)'
|
||||||
|
: dn;
|
||||||
return InkWell(onTap: () {
|
return InkWell(onTap: () {
|
||||||
isSelectedDeviceGroup.value = false;
|
isSelectedDeviceGroup.value = false;
|
||||||
if (selectedAccessibleItemName.value != username) {
|
if (selectedAccessibleItemName.value != username) {
|
||||||
@@ -222,14 +235,14 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
username.characters.first.toUpperCase(),
|
displayName.characters.first.toUpperCase(),
|
||||||
style: TextStyle(color: Colors.white),
|
style: TextStyle(color: Colors.white),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).marginOnly(right: 4),
|
).marginOnly(right: 4),
|
||||||
if (isMe) Flexible(child: Text(username)),
|
if (isMe) Flexible(child: Text(displayName)),
|
||||||
if (isMe)
|
if (isMe)
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -246,7 +259,7 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!isMe) Expanded(child: Text(username)),
|
if (!isMe) Expanded(child: Text(displayName)),
|
||||||
],
|
],
|
||||||
).paddingSymmetric(vertical: 4),
|
).paddingSymmetric(vertical: 4),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -570,11 +570,14 @@ class MyGroupPeerView extends BasePeersView {
|
|||||||
static bool filter(Peer peer) {
|
static bool filter(Peer peer) {
|
||||||
final model = gFFI.groupModel;
|
final model = gFFI.groupModel;
|
||||||
if (model.searchAccessibleItemNameText.isNotEmpty) {
|
if (model.searchAccessibleItemNameText.isNotEmpty) {
|
||||||
final text = model.searchAccessibleItemNameText.value;
|
final text = model.searchAccessibleItemNameText.value.toLowerCase();
|
||||||
final searchPeersOfUser = peer.loginName.contains(text) &&
|
final searchPeersOfUser = model.users.any((user) =>
|
||||||
model.users.any((user) => user.name == peer.loginName);
|
user.name == peer.loginName &&
|
||||||
final searchPeersOfDeviceGroup = peer.device_group_name.contains(text) &&
|
(user.name.toLowerCase().contains(text) ||
|
||||||
model.deviceGroups.any((g) => g.name == peer.device_group_name);
|
user.displayNameOrName.toLowerCase().contains(text)));
|
||||||
|
final searchPeersOfDeviceGroup =
|
||||||
|
peer.device_group_name.toLowerCase().contains(text) &&
|
||||||
|
model.deviceGroups.any((g) => g.name == peer.device_group_name);
|
||||||
if (!searchPeersOfUser && !searchPeersOfDeviceGroup) {
|
if (!searchPeersOfUser && !searchPeersOfDeviceGroup) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class RawKeyFocusScope extends StatelessWidget {
|
|||||||
// https://github.com/flutter/flutter/issues/154053
|
// https://github.com/flutter/flutter/issues/154053
|
||||||
final useRawKeyEvents = isLinux && !isWeb;
|
final useRawKeyEvents = isLinux && !isWeb;
|
||||||
// FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events,
|
// FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events,
|
||||||
// while `Alt` and `Control` are seperated key events for en-US input method.
|
// while `Alt` and `Control` are separated key events for en-US input method.
|
||||||
return FocusScope(
|
return FocusScope(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
child: Focus(
|
child: Focus(
|
||||||
@@ -532,7 +532,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
// Official
|
// Official
|
||||||
TapGestureRecognizer:
|
TapGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||||
() => TapGestureRecognizer(), (instance) {
|
() => TapGestureRecognizer(
|
||||||
|
supportedDevices: kTouchBasedDeviceKinds,
|
||||||
|
), (instance) {
|
||||||
instance
|
instance
|
||||||
..onTapDown = onTapDown
|
..onTapDown = onTapDown
|
||||||
..onTapUp = onTapUp
|
..onTapUp = onTapUp
|
||||||
@@ -540,14 +542,18 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
}),
|
}),
|
||||||
DoubleTapGestureRecognizer:
|
DoubleTapGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
|
||||||
() => DoubleTapGestureRecognizer(), (instance) {
|
() => DoubleTapGestureRecognizer(
|
||||||
|
supportedDevices: kTouchBasedDeviceKinds,
|
||||||
|
), (instance) {
|
||||||
instance
|
instance
|
||||||
..onDoubleTapDown = onDoubleTapDown
|
..onDoubleTapDown = onDoubleTapDown
|
||||||
..onDoubleTap = onDoubleTap;
|
..onDoubleTap = onDoubleTap;
|
||||||
}),
|
}),
|
||||||
LongPressGestureRecognizer:
|
LongPressGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
||||||
() => LongPressGestureRecognizer(), (instance) {
|
() => LongPressGestureRecognizer(
|
||||||
|
supportedDevices: kTouchBasedDeviceKinds,
|
||||||
|
), (instance) {
|
||||||
instance
|
instance
|
||||||
..onLongPressDown = onLongPressDown
|
..onLongPressDown = onLongPressDown
|
||||||
..onLongPressUp = onLongPressUp
|
..onLongPressUp = onLongPressUp
|
||||||
@@ -557,7 +563,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
// Customized
|
// Customized
|
||||||
HoldTapMoveGestureRecognizer:
|
HoldTapMoveGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
|
||||||
() => HoldTapMoveGestureRecognizer(),
|
() => HoldTapMoveGestureRecognizer(
|
||||||
|
supportedDevices: kTouchBasedDeviceKinds,
|
||||||
|
),
|
||||||
(instance) => instance
|
(instance) => instance
|
||||||
..onHoldDragStart = onHoldDragStart
|
..onHoldDragStart = onHoldDragStart
|
||||||
..onHoldDragUpdate = onHoldDragUpdate
|
..onHoldDragUpdate = onHoldDragUpdate
|
||||||
@@ -565,14 +573,18 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
..onHoldDragEnd = onHoldDragEnd),
|
..onHoldDragEnd = onHoldDragEnd),
|
||||||
DoubleFinerTapGestureRecognizer:
|
DoubleFinerTapGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
|
||||||
() => DoubleFinerTapGestureRecognizer(), (instance) {
|
() => DoubleFinerTapGestureRecognizer(
|
||||||
|
supportedDevices: kTouchBasedDeviceKinds,
|
||||||
|
), (instance) {
|
||||||
instance
|
instance
|
||||||
..onDoubleFinerTap = onDoubleFinerTap
|
..onDoubleFinerTap = onDoubleFinerTap
|
||||||
..onDoubleFinerTapDown = onDoubleFinerTapDown;
|
..onDoubleFinerTapDown = onDoubleFinerTapDown;
|
||||||
}),
|
}),
|
||||||
CustomTouchGestureRecognizer:
|
CustomTouchGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
|
||||||
() => CustomTouchGestureRecognizer(), (instance) {
|
() => CustomTouchGestureRecognizer(
|
||||||
|
supportedDevices: kTouchBasedDeviceKinds,
|
||||||
|
), (instance) {
|
||||||
instance.onOneFingerPanStart =
|
instance.onOneFingerPanStart =
|
||||||
(DragStartDetails d) => onOneFingerPanStart(context, d);
|
(DragStartDetails d) => onOneFingerPanStart(context, d);
|
||||||
instance
|
instance
|
||||||
|
|||||||
@@ -275,7 +275,6 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
isDesktop &&
|
isDesktop &&
|
||||||
ffiModel.keyboard &&
|
ffiModel.keyboard &&
|
||||||
pi.platform != kPeerPlatformAndroid &&
|
pi.platform != kPeerPlatformAndroid &&
|
||||||
pi.platform != kPeerPlatformMacOS &&
|
|
||||||
versionCmp(pi.version, '1.2.0') >= 0 &&
|
versionCmp(pi.version, '1.2.0') >= 0 &&
|
||||||
bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) {
|
bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) {
|
||||||
v.add(TTextMenu(
|
v.add(TTextMenu(
|
||||||
@@ -760,9 +759,18 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
final ffiModel = ffi.ffiModel;
|
final ffiModel = ffi.ffiModel;
|
||||||
final pi = ffiModel.pi;
|
final pi = ffiModel.pi;
|
||||||
final sessionId = ffi.sessionId;
|
final sessionId = ffi.sessionId;
|
||||||
|
final hasPrivacyModePermission = ffiModel.permissions['privacy_mode'] != false;
|
||||||
|
|
||||||
|
// Backend revocation already attempts to turn privacy mode off.
|
||||||
|
// Still keep this menu when privacy mode is active, so users can turn it off
|
||||||
|
// if there is a sync delay, version mismatch, or off attempt failure.
|
||||||
|
if (!hasPrivacyModePermission && privacyModeState.isEmpty) {
|
||||||
|
return []; // No permission and not active, hide options.
|
||||||
|
}
|
||||||
|
|
||||||
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
|
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
|
||||||
final enabled = !ffi.ffiModel.viewOnly;
|
final enabled =
|
||||||
|
!ffiModel.viewOnly && (hasPrivacyModePermission || privacyModeState.isNotEmpty);
|
||||||
return TToggleMenu(
|
return TToggleMenu(
|
||||||
value: privacyModeState.isNotEmpty,
|
value: privacyModeState.isNotEmpty,
|
||||||
onChanged: enabled
|
onChanged: enabled
|
||||||
@@ -811,18 +819,29 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
return privacyModeImpls.map((e) {
|
final visibleImpls = hasPrivacyModePermission
|
||||||
|
? privacyModeImpls
|
||||||
|
: privacyModeImpls.where((e) {
|
||||||
|
final implKey = (e as List<dynamic>)[0] as String;
|
||||||
|
return privacyModeState.value == implKey;
|
||||||
|
}).toList();
|
||||||
|
return visibleImpls.map((e) {
|
||||||
final implKey = (e as List<dynamic>)[0] as String;
|
final implKey = (e as List<dynamic>)[0] as String;
|
||||||
final implName = (e)[1] as String;
|
final implName = (e)[1] as String;
|
||||||
|
final enabled = !ffiModel.viewOnly &&
|
||||||
|
(hasPrivacyModePermission || privacyModeState.value == implKey);
|
||||||
return TToggleMenu(
|
return TToggleMenu(
|
||||||
child: Text(translate(implName)),
|
child: Text(translate(implName)),
|
||||||
value: privacyModeState.value == implKey,
|
value: privacyModeState.value == implKey,
|
||||||
onChanged: (value) {
|
onChanged: enabled
|
||||||
if (value == null) return;
|
? (value) {
|
||||||
togglePrivacyModeTime = DateTime.now();
|
if (value == null) return;
|
||||||
bind.sessionTogglePrivacyMode(
|
if (value && !hasPrivacyModePermission) return;
|
||||||
sessionId: sessionId, implKey: implKey, on: value);
|
togglePrivacyModeTime = DateTime.now();
|
||||||
});
|
bind.sessionTogglePrivacyMode(
|
||||||
|
sessionId: sessionId, implKey: implKey, on: value);
|
||||||
|
}
|
||||||
|
: null);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ const String kOptionTerminalPersistent = "terminal-persistent";
|
|||||||
const String kOptionEnableTunnel = "enable-tunnel";
|
const String kOptionEnableTunnel = "enable-tunnel";
|
||||||
const String kOptionEnableRemoteRestart = "enable-remote-restart";
|
const String kOptionEnableRemoteRestart = "enable-remote-restart";
|
||||||
const String kOptionEnableBlockInput = "enable-block-input";
|
const String kOptionEnableBlockInput = "enable-block-input";
|
||||||
|
const String kOptionEnablePrivacyMode = "enable-privacy-mode";
|
||||||
|
const String kOptionEnablePermChangeInAcceptWindow =
|
||||||
|
"enable-perm-change-in-accept-window";
|
||||||
const String kOptionAllowRemoteConfigModification =
|
const String kOptionAllowRemoteConfigModification =
|
||||||
"allow-remote-config-modification";
|
"allow-remote-config-modification";
|
||||||
const String kOptionVerificationMethod = "verification-method";
|
const String kOptionVerificationMethod = "verification-method";
|
||||||
@@ -175,6 +178,7 @@ const String kOptionEnableFlutterHttpOnRust = "enable-flutter-http-on-rust";
|
|||||||
const String kOptionHideServerSetting = "hide-server-settings";
|
const String kOptionHideServerSetting = "hide-server-settings";
|
||||||
const String kOptionHideProxySetting = "hide-proxy-settings";
|
const String kOptionHideProxySetting = "hide-proxy-settings";
|
||||||
const String kOptionHideWebSocketSetting = "hide-websocket-settings";
|
const String kOptionHideWebSocketSetting = "hide-websocket-settings";
|
||||||
|
const String kOptionHideStopService = "hide-stop-service";
|
||||||
const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings";
|
const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings";
|
||||||
const String kOptionHideSecuritySetting = "hide-security-settings";
|
const String kOptionHideSecuritySetting = "hide-security-settings";
|
||||||
const String kOptionHideNetworkSetting = "hide-network-settings";
|
const String kOptionHideNetworkSetting = "hide-network-settings";
|
||||||
@@ -186,6 +190,9 @@ const String kOptionDisableChangeId = "disable-change-id";
|
|||||||
const String kOptionDisableUnlockPin = "disable-unlock-pin";
|
const String kOptionDisableUnlockPin = "disable-unlock-pin";
|
||||||
const kHideUsernameOnCard = "hide-username-on-card";
|
const kHideUsernameOnCard = "hide-username-on-card";
|
||||||
const String kOptionHideHelpCards = "hide-help-cards";
|
const String kOptionHideHelpCards = "hide-help-cards";
|
||||||
|
const String kOptionAllowDeepLinkPassword = "allow-deep-link-password";
|
||||||
|
const String kOptionAllowDeepLinkServerSettings =
|
||||||
|
"allow-deep-link-server-settings";
|
||||||
|
|
||||||
const String kOptionToggleViewOnly = "view-only";
|
const String kOptionToggleViewOnly = "view-only";
|
||||||
const String kOptionToggleShowMyCursor = "show-my-cursor";
|
const String kOptionToggleShowMyCursor = "show-my-cursor";
|
||||||
|
|||||||
@@ -908,12 +908,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||||
final pw = await bind.mainGetPermanentPassword();
|
final p0 = TextEditingController(text: "");
|
||||||
final p0 = TextEditingController(text: pw);
|
final p1 = TextEditingController(text: "");
|
||||||
final p1 = TextEditingController(text: pw);
|
|
||||||
var errMsg0 = "";
|
var errMsg0 = "";
|
||||||
var errMsg1 = "";
|
var errMsg1 = "";
|
||||||
final RxString rxPass = pw.trim().obs;
|
final localPasswordSet =
|
||||||
|
(await bind.mainGetCommon(key: "local-permanent-password-set")) == "true";
|
||||||
|
final permanentPasswordSet =
|
||||||
|
(await bind.mainGetCommon(key: "permanent-password-set")) == "true";
|
||||||
|
final presetPassword = permanentPasswordSet && !localPasswordSet;
|
||||||
|
var canSubmit = false;
|
||||||
|
final RxString rxPass = "".obs;
|
||||||
final rules = [
|
final rules = [
|
||||||
DigitValidationRule(),
|
DigitValidationRule(),
|
||||||
UppercaseValidationRule(),
|
UppercaseValidationRule(),
|
||||||
@@ -922,9 +927,21 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
MinCharactersValidationRule(8),
|
MinCharactersValidationRule(8),
|
||||||
];
|
];
|
||||||
final maxLength = bind.mainMaxEncryptLen();
|
final maxLength = bind.mainMaxEncryptLen();
|
||||||
|
final statusTip = localPasswordSet
|
||||||
|
? translate('password-hidden-tip')
|
||||||
|
: (presetPassword ? translate('preset-password-in-use-tip') : '');
|
||||||
|
final showStatusTipOnMobile =
|
||||||
|
statusTip.isNotEmpty && !isDesktop && !isWebDesktop;
|
||||||
|
|
||||||
gFFI.dialogManager.show((setState, close, context) {
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
submit() {
|
updateCanSubmit() {
|
||||||
|
canSubmit = p0.text.trim().isNotEmpty || p1.text.trim().isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() async {
|
||||||
|
if (!canSubmit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
errMsg0 = "";
|
errMsg0 = "";
|
||||||
errMsg1 = "";
|
errMsg1 = "";
|
||||||
@@ -947,7 +964,13 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bind.mainSetPermanentPassword(password: pass);
|
final ok = await bind.mainSetPermanentPasswordWithResult(password: pass);
|
||||||
|
if (!ok) {
|
||||||
|
setState(() {
|
||||||
|
errMsg0 = '${translate('Prompt')}: ${translate("Failed")}';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (pass.isNotEmpty) {
|
if (pass.isNotEmpty) {
|
||||||
notEmptyCallback?.call();
|
notEmptyCallback?.call();
|
||||||
}
|
}
|
||||||
@@ -955,14 +978,20 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate("Set Password")),
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.key, color: MyTheme.accent),
|
||||||
|
Text(translate("Set Password")).paddingOnly(left: 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
content: ConstrainedBox(
|
content: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(minWidth: 500),
|
constraints: const BoxConstraints(minWidth: 500),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
height: 8.0,
|
height: showStatusTipOnMobile ? 0.0 : 6.0,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -978,6 +1007,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
rxPass.value = value.trim();
|
rxPass.value = value.trim();
|
||||||
setState(() {
|
setState(() {
|
||||||
errMsg0 = '';
|
errMsg0 = '';
|
||||||
|
updateCanSubmit();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
maxLength: maxLength,
|
maxLength: maxLength,
|
||||||
@@ -989,9 +1019,9 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(child: PasswordStrengthIndicator(password: rxPass)),
|
Expanded(child: PasswordStrengthIndicator(password: rxPass)),
|
||||||
],
|
],
|
||||||
).marginSymmetric(vertical: 8),
|
).marginOnly(top: 2, bottom: showStatusTipOnMobile ? 2 : 8),
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
height: 8.0,
|
height: showStatusTipOnMobile ? 0.0 : 8.0,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -1005,6 +1035,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
errMsg1 = '';
|
errMsg1 = '';
|
||||||
|
updateCanSubmit();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
maxLength: maxLength,
|
maxLength: maxLength,
|
||||||
@@ -1012,11 +1043,23 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(
|
if (statusTip.isNotEmpty)
|
||||||
height: 8.0,
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.info, color: Colors.amber, size: 18)
|
||||||
|
.marginOnly(right: 6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
statusTip,
|
||||||
|
style: const TextStyle(fontSize: 13, height: 1.1),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
).marginOnly(top: 6, bottom: 2),
|
||||||
|
SizedBox(
|
||||||
|
height: showStatusTipOnMobile ? 0.0 : 8.0,
|
||||||
),
|
),
|
||||||
Obx(() => Wrap(
|
Obx(() => Wrap(
|
||||||
runSpacing: 8,
|
runSpacing: showStatusTipOnMobile ? 2.0 : 8.0,
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: rules.map((e) {
|
children: rules.map((e) {
|
||||||
var checked = e.validate(rxPass.value.trim());
|
var checked = e.validate(rxPass.value.trim());
|
||||||
@@ -1036,11 +1079,67 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: (() {
|
||||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
final cancelButton = dialogButton(
|
||||||
dialogButton("OK", onPressed: submit),
|
"Cancel",
|
||||||
],
|
icon: Icon(Icons.close_rounded),
|
||||||
onSubmit: submit,
|
onPressed: close,
|
||||||
|
isOutline: true,
|
||||||
|
);
|
||||||
|
final removeButton = dialogButton(
|
||||||
|
"Remove",
|
||||||
|
icon: Icon(Icons.delete_outline_rounded),
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
errMsg0 = "";
|
||||||
|
errMsg1 = "";
|
||||||
|
});
|
||||||
|
final ok =
|
||||||
|
await bind.mainSetPermanentPasswordWithResult(password: "");
|
||||||
|
if (!ok) {
|
||||||
|
setState(() {
|
||||||
|
errMsg0 = '${translate('Prompt')}: ${translate("Failed")}';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
buttonStyle: ButtonStyle(
|
||||||
|
backgroundColor: MaterialStatePropertyAll(Colors.red)),
|
||||||
|
);
|
||||||
|
final okButton = dialogButton(
|
||||||
|
"OK",
|
||||||
|
icon: Icon(Icons.done_rounded),
|
||||||
|
onPressed: canSubmit ? submit : null,
|
||||||
|
);
|
||||||
|
if (!isDesktop && !isWebDesktop && localPasswordSet) {
|
||||||
|
return [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
cancelButton,
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
removeButton,
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
okButton,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
cancelButton,
|
||||||
|
if (localPasswordSet) removeButton,
|
||||||
|
okButton,
|
||||||
|
];
|
||||||
|
})(),
|
||||||
|
onSubmit: canSubmit ? submit : null,
|
||||||
onCancel: close,
|
onCancel: close,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -458,23 +458,31 @@ class _GeneralState extends State<_General> {
|
|||||||
return const Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _Card(title: 'Service', children: [
|
final hideStopService =
|
||||||
Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
|
bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y';
|
||||||
() async {
|
|
||||||
serviceBtnEnabled.value = false;
|
return Obx(() {
|
||||||
await start_service(serviceStop.value);
|
if (hideStopService && !serviceStop.value) {
|
||||||
// enable the button after 1 second
|
return const Offstage();
|
||||||
Future.delayed(const Duration(seconds: 1), () {
|
}
|
||||||
serviceBtnEnabled.value = true;
|
|
||||||
});
|
return _Card(title: 'Service', children: [
|
||||||
}();
|
_Button(serviceStop.value ? 'Start' : 'Stop', () {
|
||||||
}, enabled: serviceBtnEnabled.value))
|
() async {
|
||||||
]);
|
serviceBtnEnabled.value = false;
|
||||||
|
await start_service(serviceStop.value);
|
||||||
|
// enable the button after 1 second
|
||||||
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
|
serviceBtnEnabled.value = true;
|
||||||
|
});
|
||||||
|
}();
|
||||||
|
}, enabled: serviceBtnEnabled.value)
|
||||||
|
]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget other() {
|
Widget other() {
|
||||||
final showAutoUpdate =
|
final showAutoUpdate = isWindows && bind.mainIsInstalled();
|
||||||
isWindows && bind.mainIsInstalled() && !bind.isCustomClient();
|
|
||||||
final children = <Widget>[
|
final children = <Widget>[
|
||||||
if (!isWeb && !bind.isIncomingOnly())
|
if (!isWeb && !bind.isIncomingOnly())
|
||||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||||
@@ -1054,6 +1062,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
_OptionCheckBox(context, 'Enable blocking user input',
|
_OptionCheckBox(context, 'Enable blocking user input',
|
||||||
kOptionEnableBlockInput,
|
kOptionEnableBlockInput,
|
||||||
enabled: enabled, fakeValue: fakeValue),
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
|
if (bind.mainSupportedPrivacyModeImpls() != '[]')
|
||||||
|
_OptionCheckBox(
|
||||||
|
context, 'Enable privacy mode', kOptionEnablePrivacyMode,
|
||||||
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
_OptionCheckBox(context, 'Enable remote configuration modification',
|
_OptionCheckBox(context, 'Enable remote configuration modification',
|
||||||
kOptionAllowRemoteConfigModification,
|
kOptionAllowRemoteConfigModification,
|
||||||
enabled: enabled, fakeValue: fakeValue),
|
enabled: enabled, fakeValue: fakeValue),
|
||||||
@@ -1101,8 +1113,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
if (value ==
|
if (value ==
|
||||||
passwordValues[passwordKeys
|
passwordValues[passwordKeys
|
||||||
.indexOf(kUsePermanentPassword)] &&
|
.indexOf(kUsePermanentPassword)] &&
|
||||||
(await bind.mainGetPermanentPassword())
|
(await bind.mainGetCommon(
|
||||||
.isEmpty) {
|
key: "permanent-password-set")) !=
|
||||||
|
"true") {
|
||||||
if (isChangePermanentPasswordDisabled()) {
|
if (isChangePermanentPasswordDisabled()) {
|
||||||
await callback();
|
await callback();
|
||||||
return;
|
return;
|
||||||
@@ -2016,7 +2029,9 @@ class _AccountState extends State<_Account> {
|
|||||||
|
|
||||||
Widget accountAction() {
|
Widget accountAction() {
|
||||||
return Obx(() => _Button(
|
return Obx(() => _Button(
|
||||||
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
|
gFFI.userModel.userName.value.isEmpty
|
||||||
|
? 'Login'
|
||||||
|
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})',
|
||||||
() => {
|
() => {
|
||||||
gFFI.userModel.userName.value.isEmpty
|
gFFI.userModel.userName.value.isEmpty
|
||||||
? loginDialog()
|
? loginDialog()
|
||||||
@@ -2025,24 +2040,65 @@ class _AccountState extends State<_Account> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget useInfo() {
|
Widget useInfo() {
|
||||||
text(String key, String value) {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: SelectionArea(child: Text('${translate(key)}: $value'))
|
|
||||||
.marginSymmetric(vertical: 4),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Obx(() => Offstage(
|
return Obx(() => Offstage(
|
||||||
offstage: gFFI.userModel.userName.value.isEmpty,
|
offstage: gFFI.userModel.userName.value.isEmpty,
|
||||||
child: Column(
|
child: Container(
|
||||||
children: [
|
padding: const EdgeInsets.all(12),
|
||||||
text('Username', gFFI.userModel.userName.value),
|
decoration: BoxDecoration(
|
||||||
// text('Group', gFFI.groupModel.groupName.value),
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
],
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Builder(builder: (context) {
|
||||||
|
final avatarWidget = _buildUserAvatar();
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (avatarWidget != null) avatarWidget,
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
gFFI.userModel.displayNameOrUserName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
SelectionArea(
|
||||||
|
child: Text(
|
||||||
|
'@${gFFI.userModel.userName.value}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color:
|
||||||
|
Theme.of(context).textTheme.bodySmall?.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)).marginOnly(left: 18, top: 16);
|
)).marginOnly(left: 18, top: 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget? _buildUserAvatar() {
|
||||||
|
// Resolve relative avatar path at display time
|
||||||
|
final avatar =
|
||||||
|
bind.mainResolveAvatarUrl(avatar: gFFI.userModel.avatar.value);
|
||||||
|
return buildAvatarWidget(
|
||||||
|
avatar: avatar,
|
||||||
|
size: 44,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Checkbox extends StatefulWidget {
|
class _Checkbox extends StatefulWidget {
|
||||||
@@ -2130,7 +2186,9 @@ class _PluginState extends State<_Plugin> {
|
|||||||
|
|
||||||
Widget accountAction() {
|
Widget accountAction() {
|
||||||
return Obx(() => _Button(
|
return Obx(() => _Button(
|
||||||
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
|
gFFI.userModel.userName.value.isEmpty
|
||||||
|
? 'Login'
|
||||||
|
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})',
|
||||||
() => {
|
() => {
|
||||||
gFFI.userModel.userName.value.isEmpty
|
gFFI.userModel.userName.value.isEmpty
|
||||||
? loginDialog()
|
? loginDialog()
|
||||||
|
|||||||
@@ -462,23 +462,7 @@ class _CmHeaderState extends State<_CmHeader>
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
_buildClientAvatar().marginOnly(right: 10.0),
|
||||||
width: 70,
|
|
||||||
height: 70,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: str2color(client.name),
|
|
||||||
borderRadius: BorderRadius.circular(15.0),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
client.name[0],
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 55,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).marginOnly(right: 10.0),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
@@ -582,6 +566,36 @@ class _CmHeaderState extends State<_CmHeader>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
Widget _buildClientAvatar() {
|
||||||
|
return buildAvatarWidget(
|
||||||
|
avatar: client.avatar,
|
||||||
|
size: 70,
|
||||||
|
borderRadius: 15,
|
||||||
|
fallback: _buildInitialAvatar(),
|
||||||
|
) ??
|
||||||
|
_buildInitialAvatar();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInitialAvatar() {
|
||||||
|
return Container(
|
||||||
|
width: 70,
|
||||||
|
height: 70,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: str2color(client.name),
|
||||||
|
borderRadius: BorderRadius.circular(15.0),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
client.name.isNotEmpty ? client.name[0] : '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 55,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PrivilegeBoard extends StatefulWidget {
|
class _PrivilegeBoard extends StatefulWidget {
|
||||||
@@ -596,19 +610,24 @@ class _PrivilegeBoard extends StatefulWidget {
|
|||||||
class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||||
late final client = widget.client;
|
late final client = widget.client;
|
||||||
Widget buildPermissionIcon(bool enabled, IconData iconData,
|
Widget buildPermissionIcon(bool enabled, IconData iconData,
|
||||||
Function(bool)? onTap, String tooltipText) {
|
Function(bool)? onTap, String tooltipText,
|
||||||
|
{required bool canModify}) {
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: "$tooltipText: ${enabled ? "ON" : "OFF"}",
|
message: "$tooltipText: ${enabled ? "ON" : "OFF"}",
|
||||||
waitDuration: Duration.zero,
|
waitDuration: Duration.zero,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: enabled ? MyTheme.accent : Colors.grey[700],
|
color: enabled
|
||||||
|
? (canModify ? MyTheme.accent : MyTheme.accent.withOpacity(0.6))
|
||||||
|
: Colors.grey[700],
|
||||||
borderRadius: BorderRadius.circular(10.0),
|
borderRadius: BorderRadius.circular(10.0),
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8.0),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () =>
|
onTap: canModify
|
||||||
checkClickTime(widget.client.id, () => onTap?.call(!enabled)),
|
? () =>
|
||||||
|
checkClickTime(widget.client.id, () => onTap?.call(!enabled))
|
||||||
|
: null,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
@@ -629,6 +648,9 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final crossAxisCount = 4;
|
final crossAxisCount = 4;
|
||||||
final spacing = 10.0;
|
final spacing = 10.0;
|
||||||
|
final canModifyPermission =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionEnablePermChangeInAcceptWindow) !=
|
||||||
|
'N';
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 160.0,
|
height: 160.0,
|
||||||
@@ -675,6 +697,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable audio'),
|
translate('Enable audio'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
buildPermissionIcon(
|
buildPermissionIcon(
|
||||||
client.recording,
|
client.recording,
|
||||||
@@ -689,6 +712,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable recording session'),
|
translate('Enable recording session'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
@@ -705,6 +729,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable keyboard/mouse'),
|
translate('Enable keyboard/mouse'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
buildPermissionIcon(
|
buildPermissionIcon(
|
||||||
client.clipboard,
|
client.clipboard,
|
||||||
@@ -719,6 +744,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable clipboard'),
|
translate('Enable clipboard'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
buildPermissionIcon(
|
buildPermissionIcon(
|
||||||
client.audio,
|
client.audio,
|
||||||
@@ -733,6 +759,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable audio'),
|
translate('Enable audio'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
buildPermissionIcon(
|
buildPermissionIcon(
|
||||||
client.file,
|
client.file,
|
||||||
@@ -747,6 +774,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable file copy and paste'),
|
translate('Enable file copy and paste'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
buildPermissionIcon(
|
buildPermissionIcon(
|
||||||
client.restart,
|
client.restart,
|
||||||
@@ -761,6 +789,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable remote restart'),
|
translate('Enable remote restart'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
buildPermissionIcon(
|
buildPermissionIcon(
|
||||||
client.recording,
|
client.recording,
|
||||||
@@ -775,6 +804,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable recording session'),
|
translate('Enable recording session'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
),
|
),
|
||||||
// only windows support block input
|
// only windows support block input
|
||||||
if (isWindows)
|
if (isWindows)
|
||||||
@@ -791,6 +821,23 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
translate('Enable blocking user input'),
|
translate('Enable blocking user input'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
|
),
|
||||||
|
if (bind.mainSupportedPrivacyModeImpls() != '[]')
|
||||||
|
buildPermissionIcon(
|
||||||
|
client.privacyMode,
|
||||||
|
Icons.visibility_off,
|
||||||
|
(enabled) {
|
||||||
|
bind.cmSwitchPermission(
|
||||||
|
connId: client.id,
|
||||||
|
name: "privacy_mode",
|
||||||
|
enabled: enabled);
|
||||||
|
setState(() {
|
||||||
|
client.privacyMode = enabled;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
translate('Enable privacy mode'),
|
||||||
|
canModify: canModifyPermission,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
@@ -15,6 +16,7 @@ class TerminalPage extends StatefulWidget {
|
|||||||
required this.tabController,
|
required this.tabController,
|
||||||
required this.isSharedPassword,
|
required this.isSharedPassword,
|
||||||
required this.terminalId,
|
required this.terminalId,
|
||||||
|
required this.tabKey,
|
||||||
this.forceRelay,
|
this.forceRelay,
|
||||||
this.connToken,
|
this.connToken,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@@ -25,6 +27,8 @@ class TerminalPage extends StatefulWidget {
|
|||||||
final bool? isSharedPassword;
|
final bool? isSharedPassword;
|
||||||
final String? connToken;
|
final String? connToken;
|
||||||
final int terminalId;
|
final int terminalId;
|
||||||
|
/// Tab key for focus management, passed from parent to avoid duplicate construction
|
||||||
|
final String tabKey;
|
||||||
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
|
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
|
||||||
|
|
||||||
FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi;
|
FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi;
|
||||||
@@ -42,11 +46,16 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
late FFI _ffi;
|
late FFI _ffi;
|
||||||
late TerminalModel _terminalModel;
|
late TerminalModel _terminalModel;
|
||||||
double? _cellHeight;
|
double? _cellHeight;
|
||||||
|
final FocusNode _terminalFocusNode = FocusNode(canRequestFocus: false);
|
||||||
|
StreamSubscription<DesktopTabState>? _tabStateSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
// Listen for tab selection changes to request focus
|
||||||
|
_tabStateSubscription = widget.tabController.state.listen(_onTabStateChanged);
|
||||||
|
|
||||||
// Use shared FFI instance from connection manager
|
// Use shared FFI instance from connection manager
|
||||||
_ffi = TerminalConnectionManager.getConnection(
|
_ffi = TerminalConnectionManager.getConnection(
|
||||||
peerId: widget.id,
|
peerId: widget.id,
|
||||||
@@ -64,6 +73,13 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
_terminalModel.onResizeExternal = (w, h, pw, ph) {
|
_terminalModel.onResizeExternal = (w, h, pw, ph) {
|
||||||
_cellHeight = ph * 1.0;
|
_cellHeight = ph * 1.0;
|
||||||
|
|
||||||
|
// Enable focus once terminal has valid dimensions (first valid resize)
|
||||||
|
if (!_terminalFocusNode.canRequestFocus && w > 0 && h > 0) {
|
||||||
|
_terminalFocusNode.canRequestFocus = true;
|
||||||
|
// Auto-focus if this tab is currently selected
|
||||||
|
_requestFocusIfSelected();
|
||||||
|
}
|
||||||
|
|
||||||
// Schedule the setState for the next frame
|
// Schedule the setState for the next frame
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -99,14 +115,42 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
// Cancel tab state subscription to prevent memory leak
|
||||||
|
_tabStateSubscription?.cancel();
|
||||||
// Unregister terminal model from FFI
|
// Unregister terminal model from FFI
|
||||||
_ffi.unregisterTerminalModel(widget.terminalId);
|
_ffi.unregisterTerminalModel(widget.terminalId);
|
||||||
_terminalModel.dispose();
|
_terminalModel.dispose();
|
||||||
|
_terminalFocusNode.dispose();
|
||||||
// Release connection reference instead of closing directly
|
// Release connection reference instead of closing directly
|
||||||
TerminalConnectionManager.releaseConnection(widget.id);
|
TerminalConnectionManager.releaseConnection(widget.id);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onTabStateChanged(DesktopTabState state) {
|
||||||
|
// Check if this tab is now selected and request focus
|
||||||
|
if (state.selected >= 0 && state.selected < state.tabs.length) {
|
||||||
|
final selectedTab = state.tabs[state.selected];
|
||||||
|
if (selectedTab.key == widget.tabKey && mounted) {
|
||||||
|
_requestFocusIfSelected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _requestFocusIfSelected() {
|
||||||
|
if (!mounted || !_terminalFocusNode.canRequestFocus) return;
|
||||||
|
// Use post-frame callback to ensure widget is fully laid out in focus tree
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// Re-check conditions after frame: mounted, focusable, still selected, not already focused
|
||||||
|
if (!mounted || !_terminalFocusNode.canRequestFocus || _terminalFocusNode.hasFocus) return;
|
||||||
|
final state = widget.tabController.state.value;
|
||||||
|
if (state.selected >= 0 && state.selected < state.tabs.length) {
|
||||||
|
if (state.tabs[state.selected].key == widget.tabKey) {
|
||||||
|
_terminalFocusNode.requestFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// This method ensures that the number of visible rows is an integer by computing the
|
// This method ensures that the number of visible rows is an integer by computing the
|
||||||
// extra space left after dividing the available height by the height of a single
|
// extra space left after dividing the available height by the height of a single
|
||||||
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
|
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
|
||||||
@@ -131,7 +175,9 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
return TerminalView(
|
return TerminalView(
|
||||||
_terminalModel.terminal,
|
_terminalModel.terminal,
|
||||||
controller: _terminalModel.terminalController,
|
controller: _terminalModel.terminalController,
|
||||||
autofocus: true,
|
focusNode: _terminalFocusNode,
|
||||||
|
// Note: autofocus is not used here because focus is managed manually
|
||||||
|
// via _onTabStateChanged() to handle tab switching properly.
|
||||||
backgroundOpacity: 0.7,
|
backgroundOpacity: 0.7,
|
||||||
padding: _calculatePadding(heightPx),
|
padding: _calculatePadding(heightPx),
|
||||||
onSecondaryTapDown: (details, offset) async {
|
onSecondaryTapDown: (details, offset) async {
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
static const IconData selectedIcon = Icons.terminal;
|
static const IconData selectedIcon = Icons.terminal;
|
||||||
static const IconData unselectedIcon = Icons.terminal_outlined;
|
static const IconData unselectedIcon = Icons.terminal_outlined;
|
||||||
int _nextTerminalId = 1;
|
int _nextTerminalId = 1;
|
||||||
|
// Lightweight idempotency guard for async close operations
|
||||||
|
final Set<String> _closingTabs = {};
|
||||||
|
// When true, all session cleanup should persist (window-level close in progress)
|
||||||
|
bool _windowClosing = false;
|
||||||
|
|
||||||
_TerminalTabPageState(Map<String, dynamic> params) {
|
_TerminalTabPageState(Map<String, dynamic> params) {
|
||||||
Get.put(DesktopTabController(tabType: DesktopTabType.terminal));
|
Get.put(DesktopTabController(tabType: DesktopTabType.terminal));
|
||||||
@@ -70,28 +74,12 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
label: tabLabel,
|
label: tabLabel,
|
||||||
selectedIcon: selectedIcon,
|
selectedIcon: selectedIcon,
|
||||||
unselectedIcon: unselectedIcon,
|
unselectedIcon: unselectedIcon,
|
||||||
onTabCloseButton: () async {
|
onTabCloseButton: () => _closeTab(tabKey),
|
||||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
|
||||||
id: tabKey,
|
|
||||||
tabController: tabController,
|
|
||||||
)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Close the terminal session first
|
|
||||||
final ffi = TerminalConnectionManager.getExistingConnection(peerId);
|
|
||||||
if (ffi != null) {
|
|
||||||
final terminalModel = ffi.terminalModels[terminalId];
|
|
||||||
if (terminalModel != null) {
|
|
||||||
await terminalModel.closeTerminal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Then close the tab
|
|
||||||
tabController.closeBy(tabKey);
|
|
||||||
},
|
|
||||||
page: TerminalPage(
|
page: TerminalPage(
|
||||||
key: ValueKey(tabKey),
|
key: ValueKey(tabKey),
|
||||||
id: peerId,
|
id: peerId,
|
||||||
terminalId: terminalId,
|
terminalId: terminalId,
|
||||||
|
tabKey: tabKey,
|
||||||
password: password,
|
password: password,
|
||||||
isSharedPassword: isSharedPassword,
|
isSharedPassword: isSharedPassword,
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
@@ -101,6 +89,159 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unified tab close handler for all close paths (button, shortcut, programmatic).
|
||||||
|
/// Shows audit dialog, cleans up session if not persistent, then removes the UI tab.
|
||||||
|
Future<void> _closeTab(String tabKey) async {
|
||||||
|
// Idempotency guard: skip if already closing this tab
|
||||||
|
if (_closingTabs.contains(tabKey)) return;
|
||||||
|
_closingTabs.add(tabKey);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Snapshot peerTabCount BEFORE any await to avoid race with concurrent
|
||||||
|
// _closeAllTabs clearing tabController (which would make the live count
|
||||||
|
// drop to 0 and incorrectly trigger session persistence).
|
||||||
|
// Note: the snapshot may become stale if other individual tabs are closed
|
||||||
|
// during the audit dialog, but this is an acceptable trade-off.
|
||||||
|
int? snapshotPeerTabCount;
|
||||||
|
final parsed = _parseTabKey(tabKey);
|
||||||
|
if (parsed != null) {
|
||||||
|
final (peerId, _) = parsed;
|
||||||
|
snapshotPeerTabCount = tabController.state.value.tabs.where((t) {
|
||||||
|
final p = _parseTabKey(t.key);
|
||||||
|
return p != null && p.$1 == peerId;
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||||
|
id: tabKey,
|
||||||
|
tabController: tabController,
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close terminal session if not in persistent mode.
|
||||||
|
// Wrapped separately so session cleanup failure never blocks UI tab removal.
|
||||||
|
try {
|
||||||
|
await _closeTerminalSessionIfNeeded(tabKey,
|
||||||
|
peerTabCount: snapshotPeerTabCount);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[TerminalTabPage] Session cleanup failed for $tabKey: $e');
|
||||||
|
}
|
||||||
|
// Always close the tab from UI, regardless of session cleanup result
|
||||||
|
tabController.closeBy(tabKey);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[TerminalTabPage] Error closing tab $tabKey: $e');
|
||||||
|
} finally {
|
||||||
|
_closingTabs.remove(tabKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close all tabs with session cleanup.
|
||||||
|
/// Used for window-level close operations (onDestroy, handleWindowCloseButton).
|
||||||
|
/// UI tabs are removed immediately; session cleanup runs in parallel with a
|
||||||
|
/// bounded timeout so window close is not blocked indefinitely.
|
||||||
|
Future<void> _closeAllTabs() async {
|
||||||
|
_windowClosing = true;
|
||||||
|
final tabKeys = tabController.state.value.tabs.map((t) => t.key).toList();
|
||||||
|
// Remove all UI tabs immediately (same instant behavior as the old tabController.clear())
|
||||||
|
tabController.clear();
|
||||||
|
// Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout).
|
||||||
|
// Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls.
|
||||||
|
final futures = tabKeys
|
||||||
|
.where((tabKey) => !_closingTabs.contains(tabKey))
|
||||||
|
.map((tabKey) async {
|
||||||
|
try {
|
||||||
|
await _closeTerminalSessionIfNeeded(tabKey, persistAll: true);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[TerminalTabPage] Session cleanup failed for $tabKey: $e');
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
if (futures.isNotEmpty) {
|
||||||
|
await Future.wait(futures).timeout(
|
||||||
|
const Duration(seconds: 4),
|
||||||
|
onTimeout: () {
|
||||||
|
debugPrint(
|
||||||
|
'[TerminalTabPage] Session cleanup timed out for batch close');
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the terminal session on server side based on persistent mode.
|
||||||
|
///
|
||||||
|
/// [persistAll] controls behavior when persistent mode is enabled:
|
||||||
|
/// - `true` (window close): persist all sessions, don't close any.
|
||||||
|
/// - `false` (tab close): only persist the last session for the peer,
|
||||||
|
/// close others so only the most recent disconnected session survives.
|
||||||
|
///
|
||||||
|
/// Note: if [_windowClosing] is true, persistAll is forced to true so that
|
||||||
|
/// in-flight _closeTab() calls don't accidentally close sessions that the
|
||||||
|
/// window-close flow intends to preserve.
|
||||||
|
Future<void> _closeTerminalSessionIfNeeded(String tabKey,
|
||||||
|
{bool persistAll = false, int? peerTabCount}) async {
|
||||||
|
// If window close is in progress, override to persist all sessions
|
||||||
|
// even if this call originated from an individual tab close.
|
||||||
|
if (_windowClosing) {
|
||||||
|
persistAll = true;
|
||||||
|
}
|
||||||
|
final parsed = _parseTabKey(tabKey);
|
||||||
|
if (parsed == null) return;
|
||||||
|
final (peerId, terminalId) = parsed;
|
||||||
|
|
||||||
|
final ffi = TerminalConnectionManager.getExistingConnection(peerId);
|
||||||
|
if (ffi == null) return;
|
||||||
|
|
||||||
|
final isPersistent = bind.sessionGetToggleOptionSync(
|
||||||
|
sessionId: ffi.sessionId,
|
||||||
|
arg: kOptionTerminalPersistent,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isPersistent) {
|
||||||
|
if (persistAll) {
|
||||||
|
// Window close: persist all sessions
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Tab close: only persist if this is the last tab for this peer.
|
||||||
|
// Use the snapshot value if provided (avoids race with concurrent tab removal).
|
||||||
|
final effectivePeerTabCount = peerTabCount ??
|
||||||
|
tabController.state.value.tabs.where((t) {
|
||||||
|
final p = _parseTabKey(t.key);
|
||||||
|
return p != null && p.$1 == peerId;
|
||||||
|
}).length;
|
||||||
|
if (effectivePeerTabCount <= 1) {
|
||||||
|
// Last tab for this peer — persist the session
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Not the last tab — fall through to close the session
|
||||||
|
}
|
||||||
|
|
||||||
|
final terminalModel = ffi.terminalModels[terminalId];
|
||||||
|
if (terminalModel != null) {
|
||||||
|
// closeTerminal() has internal 3s timeout, no need for external timeout
|
||||||
|
await terminalModel.closeTerminal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse tabKey (format: "peerId_terminalId") into its components.
|
||||||
|
/// Note: peerId may contain underscores, so we use lastIndexOf('_').
|
||||||
|
/// Returns null if tabKey format is invalid.
|
||||||
|
(String peerId, int terminalId)? _parseTabKey(String tabKey) {
|
||||||
|
final lastUnderscore = tabKey.lastIndexOf('_');
|
||||||
|
if (lastUnderscore <= 0) {
|
||||||
|
debugPrint('[TerminalTabPage] Invalid tabKey format: $tabKey');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final terminalIdStr = tabKey.substring(lastUnderscore + 1);
|
||||||
|
final terminalId = int.tryParse(terminalIdStr);
|
||||||
|
if (terminalId == null) {
|
||||||
|
debugPrint('[TerminalTabPage] Invalid terminalId in tabKey: $tabKey');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final peerId = tabKey.substring(0, lastUnderscore);
|
||||||
|
return (peerId, terminalId);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _tabMenuBuilder(String peerId, CancelFunc cancelFunc) {
|
Widget _tabMenuBuilder(String peerId, CancelFunc cancelFunc) {
|
||||||
final List<MenuEntryBase<String>> menu = [];
|
final List<MenuEntryBase<String>> menu = [];
|
||||||
const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0);
|
const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0);
|
||||||
@@ -184,7 +325,8 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
} else if (call.method == kWindowEventRestoreTerminalSessions) {
|
} else if (call.method == kWindowEventRestoreTerminalSessions) {
|
||||||
_restoreSessions(call.arguments);
|
_restoreSessions(call.arguments);
|
||||||
} else if (call.method == "onDestroy") {
|
} else if (call.method == "onDestroy") {
|
||||||
tabController.clear();
|
// Clean up sessions before window destruction (bounded wait)
|
||||||
|
await _closeAllTabs();
|
||||||
} else if (call.method == kWindowActionRebuild) {
|
} else if (call.method == kWindowActionRebuild) {
|
||||||
reloadCurrentWindow();
|
reloadCurrentWindow();
|
||||||
} else if (call.method == kWindowEventActiveSession) {
|
} else if (call.method == kWindowEventActiveSession) {
|
||||||
@@ -194,7 +336,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
final currentTab = tabController.state.value.selectedTabInfo;
|
final currentTab = tabController.state.value.selectedTabInfo;
|
||||||
assert(call.arguments is String,
|
assert(call.arguments is String,
|
||||||
"Expected String arguments for kWindowEventActiveSession, got ${call.arguments.runtimeType}");
|
"Expected String arguments for kWindowEventActiveSession, got ${call.arguments.runtimeType}");
|
||||||
if (currentTab.key.startsWith(call.arguments)) {
|
// Use lastIndexOf to handle peerIds containing underscores
|
||||||
|
final lastUnderscore = currentTab.key.lastIndexOf('_');
|
||||||
|
if (lastUnderscore > 0 &&
|
||||||
|
currentTab.key.substring(0, lastUnderscore) == call.arguments) {
|
||||||
windowOnTop(windowId());
|
windowOnTop(windowId());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -265,7 +410,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
// macOS: Cmd+W (standard for close tab)
|
// macOS: Cmd+W (standard for close tab)
|
||||||
final currentTab = tabController.state.value.selectedTabInfo;
|
final currentTab = tabController.state.value.selectedTabInfo;
|
||||||
if (tabController.state.value.tabs.length > 1) {
|
if (tabController.state.value.tabs.length > 1) {
|
||||||
tabController.closeBy(currentTab.key);
|
_closeTab(currentTab.key);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else if (!isMacOS &&
|
} else if (!isMacOS &&
|
||||||
@@ -274,7 +419,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
// Other platforms: Ctrl+Shift+W (to avoid conflict with Ctrl+W word delete)
|
// Other platforms: Ctrl+Shift+W (to avoid conflict with Ctrl+W word delete)
|
||||||
final currentTab = tabController.state.value.selectedTabInfo;
|
final currentTab = tabController.state.value.selectedTabInfo;
|
||||||
if (tabController.state.value.tabs.length > 1) {
|
if (tabController.state.value.tabs.length > 1) {
|
||||||
tabController.closeBy(currentTab.key);
|
_closeTab(currentTab.key);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,7 +474,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
void _addNewTerminal(String peerId, {int? terminalId}) {
|
void _addNewTerminal(String peerId, {int? terminalId}) {
|
||||||
// Find first tab for this peer to get connection parameters
|
// Find first tab for this peer to get connection parameters
|
||||||
final firstTab = tabController.state.value.tabs.firstWhere(
|
final firstTab = tabController.state.value.tabs.firstWhere(
|
||||||
(tab) => tab.key.startsWith('$peerId\_'),
|
(tab) {
|
||||||
|
final last = tab.key.lastIndexOf('_');
|
||||||
|
return last > 0 && tab.key.substring(0, last) == peerId;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (firstTab.page is TerminalPage) {
|
if (firstTab.page is TerminalPage) {
|
||||||
final page = firstTab.page as TerminalPage;
|
final page = firstTab.page as TerminalPage;
|
||||||
@@ -350,11 +498,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
|
|
||||||
void _addNewTerminalForCurrentPeer({int? terminalId}) {
|
void _addNewTerminalForCurrentPeer({int? terminalId}) {
|
||||||
final currentTab = tabController.state.value.selectedTabInfo;
|
final currentTab = tabController.state.value.selectedTabInfo;
|
||||||
final parts = currentTab.key.split('_');
|
final parsed = _parseTabKey(currentTab.key);
|
||||||
if (parts.isNotEmpty) {
|
if (parsed == null) return;
|
||||||
final peerId = parts[0];
|
final (peerId, _) = parsed;
|
||||||
_addNewTerminal(peerId, terminalId: terminalId);
|
_addNewTerminal(peerId, terminalId: terminalId);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -368,10 +515,9 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
selectedBorderColor: MyTheme.accent,
|
selectedBorderColor: MyTheme.accent,
|
||||||
labelGetter: DesktopTab.tablabelGetter,
|
labelGetter: DesktopTab.tablabelGetter,
|
||||||
tabMenuBuilder: (key) {
|
tabMenuBuilder: (key) {
|
||||||
// Extract peerId from tab key (format: "peerId_terminalId")
|
final parsed = _parseTabKey(key);
|
||||||
final parts = key.split('_');
|
if (parsed == null) return Container();
|
||||||
if (parts.isEmpty) return Container();
|
final (peerId, _) = parsed;
|
||||||
final peerId = parts[0];
|
|
||||||
return _tabMenuBuilder(peerId, () {});
|
return _tabMenuBuilder(peerId, () {});
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
@@ -426,7 +572,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (connLength <= 1) {
|
if (connLength <= 1) {
|
||||||
tabController.clear();
|
await _closeAllTabs();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
final bool res;
|
final bool res;
|
||||||
@@ -437,7 +583,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
res = await closeConfirmDialog();
|
res = await closeConfirmDialog();
|
||||||
}
|
}
|
||||||
if (res) {
|
if (res) {
|
||||||
tabController.clear();
|
await _closeAllTabs();
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -996,10 +996,10 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
toggles(),
|
toggles(),
|
||||||
];
|
];
|
||||||
// privacy mode
|
// privacy mode
|
||||||
|
final privacyModeState = PrivacyModeState.find(id);
|
||||||
if (ffi.connType == ConnType.defaultConn &&
|
if (ffi.connType == ConnType.defaultConn &&
|
||||||
ffiModel.keyboard &&
|
(pi.features.privacyMode || privacyModeState.isNotEmpty) &&
|
||||||
pi.features.privacyMode) {
|
(ffiModel.keyboard || privacyModeState.isNotEmpty)) {
|
||||||
final privacyModeState = PrivacyModeState.find(id);
|
|
||||||
final privacyModeList =
|
final privacyModeList =
|
||||||
toolbarPrivacyMode(privacyModeState, context, id, ffi);
|
toolbarPrivacyMode(privacyModeState, context, id, ffi);
|
||||||
if (privacyModeList.length == 1) {
|
if (privacyModeList.length == 1) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui' as ui;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -65,9 +64,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
bool _showGestureHelp = false;
|
bool _showGestureHelp = false;
|
||||||
String _value = '';
|
String _value = '';
|
||||||
Orientation? _currentOrientation;
|
Orientation? _currentOrientation;
|
||||||
double _viewInsetsBottom = 0;
|
|
||||||
final _uniqueKey = UniqueKey();
|
final _uniqueKey = UniqueKey();
|
||||||
Timer? _timerDidChangeMetrics;
|
|
||||||
Timer? _iosKeyboardWorkaroundTimer;
|
Timer? _iosKeyboardWorkaroundTimer;
|
||||||
|
|
||||||
final _blockableOverlayState = BlockableOverlayState();
|
final _blockableOverlayState = BlockableOverlayState();
|
||||||
@@ -140,7 +137,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
_physicalFocusNode.dispose();
|
_physicalFocusNode.dispose();
|
||||||
await gFFI.close();
|
await gFFI.close();
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timerDidChangeMetrics?.cancel();
|
|
||||||
_iosKeyboardWorkaroundTimer?.cancel();
|
_iosKeyboardWorkaroundTimer?.cancel();
|
||||||
gFFI.dialogManager.dismissAll();
|
gFFI.dialogManager.dismissAll();
|
||||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||||
@@ -167,26 +163,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
gFFI.invokeMethod("try_sync_clipboard");
|
gFFI.invokeMethod("try_sync_clipboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeMetrics() {
|
|
||||||
// If the soft keyboard is visible and the canvas has been changed(panned or scaled)
|
|
||||||
// Don't try reset the view style and focus the cursor.
|
|
||||||
if (gFFI.cursorModel.lastKeyboardIsVisible &&
|
|
||||||
gFFI.canvasModel.isMobileCanvasChanged) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom;
|
|
||||||
_timerDidChangeMetrics?.cancel();
|
|
||||||
_timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async {
|
|
||||||
// We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`.
|
|
||||||
if (newBottom != _viewInsetsBottom) {
|
|
||||||
gFFI.canvasModel.mobileFocusCanvasCursor();
|
|
||||||
_viewInsetsBottom = newBottom;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// to-do: It should be better to use transparent color instead of the bgColor.
|
// to-do: It should be better to use transparent color instead of the bgColor.
|
||||||
// But for now, the transparent color will cause the canvas to be white.
|
// But for now, the transparent color will cause the canvas to be white.
|
||||||
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
|
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
|
||||||
@@ -450,12 +426,10 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
color: MyTheme.canvasColor,
|
color: MyTheme.canvasColor,
|
||||||
child: inputModel.isPhysicalMouse.value
|
child: RawTouchGestureDetectorRegion(
|
||||||
? getBodyForMobile()
|
child: getBodyForMobile(),
|
||||||
: RawTouchGestureDetectorRegion(
|
ffi: gFFI,
|
||||||
child: getBodyForMobile(),
|
),
|
||||||
ffi: gFFI,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -1209,7 +1183,8 @@ void showOptions(
|
|||||||
List<TToggleMenu> privacyModeList = [];
|
List<TToggleMenu> privacyModeList = [];
|
||||||
// privacy mode
|
// privacy mode
|
||||||
final privacyModeState = PrivacyModeState.find(id);
|
final privacyModeState = PrivacyModeState.find(id);
|
||||||
if (gFFI.ffiModel.keyboard && gFFI.ffiModel.pi.features.privacyMode) {
|
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
|
||||||
|
privacyModeState.isNotEmpty) {
|
||||||
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
|
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
|
||||||
if (privacyModeList.length == 1) {
|
if (privacyModeList.length == 1) {
|
||||||
displayToggles.add(privacyModeList[0]);
|
displayToggles.add(privacyModeList[0]);
|
||||||
|
|||||||
@@ -150,7 +150,8 @@ class _DropDownAction extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (value == kUsePermanentPassword &&
|
if (value == kUsePermanentPassword &&
|
||||||
(await bind.mainGetPermanentPassword()).isEmpty) {
|
(await bind.mainGetCommon(key: "permanent-password-set")) !=
|
||||||
|
"true") {
|
||||||
if (isChangePermanentPasswordDisabled()) {
|
if (isChangePermanentPasswordDisabled()) {
|
||||||
callback();
|
callback();
|
||||||
return;
|
return;
|
||||||
@@ -582,10 +583,20 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final serverModel = Provider.of<ServerModel>(context);
|
final serverModel = Provider.of<ServerModel>(context);
|
||||||
final hasAudioPermission = androidVersion >= 30;
|
final hasAudioPermission = androidVersion >= 30;
|
||||||
|
final hideStopService = isAndroid &&
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y';
|
||||||
|
final allowPermChangeInAcceptWindow = option2bool(
|
||||||
|
kOptionEnablePermChangeInAcceptWindow,
|
||||||
|
bind.mainGetBuildinOption(
|
||||||
|
key: kOptionEnablePermChangeInAcceptWindow,
|
||||||
|
));
|
||||||
|
final permissionChangeLocked = isAndroid &&
|
||||||
|
serverModel.clients.any((c) => !c.disconnected) &&
|
||||||
|
!allowPermChangeInAcceptWindow;
|
||||||
return PaddingCard(
|
return PaddingCard(
|
||||||
title: translate("Permissions"),
|
title: translate("Permissions"),
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
serverModel.mediaOk
|
serverModel.mediaOk && !hideStopService
|
||||||
? ElevatedButton.icon(
|
? ElevatedButton.icon(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
@@ -595,21 +606,30 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
|||||||
label: Text(translate("Stop service")))
|
label: Text(translate("Stop service")))
|
||||||
.marginOnly(bottom: 8)
|
.marginOnly(bottom: 8)
|
||||||
: SizedBox.shrink(),
|
: SizedBox.shrink(),
|
||||||
|
if (!hideStopService || !serverModel.mediaOk)
|
||||||
|
PermissionRow(
|
||||||
|
translate("Screen Capture"),
|
||||||
|
serverModel.mediaOk,
|
||||||
|
!serverModel.mediaOk &&
|
||||||
|
gFFI.userModel.userName.value.isEmpty &&
|
||||||
|
bind.mainGetLocalOption(key: "show-scam-warning") != "N"
|
||||||
|
? () => showScamWarning(context, serverModel)
|
||||||
|
: serverModel.toggleService),
|
||||||
PermissionRow(
|
PermissionRow(
|
||||||
translate("Screen Capture"),
|
translate("Input Control"),
|
||||||
serverModel.mediaOk,
|
serverModel.inputOk,
|
||||||
!serverModel.mediaOk &&
|
serverModel.toggleInput,
|
||||||
gFFI.userModel.userName.value.isEmpty &&
|
),
|
||||||
bind.mainGetLocalOption(key: "show-scam-warning") != "N"
|
PermissionRow(
|
||||||
? () => showScamWarning(context, serverModel)
|
translate("Transfer file"),
|
||||||
: serverModel.toggleService),
|
serverModel.fileOk,
|
||||||
PermissionRow(translate("Input Control"), serverModel.inputOk,
|
serverModel.toggleFile,
|
||||||
serverModel.toggleInput),
|
enabled: !permissionChangeLocked,
|
||||||
PermissionRow(translate("Transfer file"), serverModel.fileOk,
|
),
|
||||||
serverModel.toggleFile),
|
|
||||||
hasAudioPermission
|
hasAudioPermission
|
||||||
? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
|
? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
|
||||||
serverModel.toggleAudio)
|
serverModel.toggleAudio,
|
||||||
|
enabled: !permissionChangeLocked)
|
||||||
: Row(children: [
|
: Row(children: [
|
||||||
Icon(Icons.info_outline).marginOnly(right: 15),
|
Icon(Icons.info_outline).marginOnly(right: 15),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -618,19 +638,25 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
|||||||
style: const TextStyle(color: MyTheme.darkGray),
|
style: const TextStyle(color: MyTheme.darkGray),
|
||||||
))
|
))
|
||||||
]),
|
]),
|
||||||
PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk,
|
PermissionRow(
|
||||||
serverModel.toggleClipboard),
|
translate("Enable clipboard"),
|
||||||
|
serverModel.clipboardOk,
|
||||||
|
serverModel.toggleClipboard,
|
||||||
|
enabled: !permissionChangeLocked,
|
||||||
|
),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PermissionRow extends StatelessWidget {
|
class PermissionRow extends StatelessWidget {
|
||||||
const PermissionRow(this.name, this.isOk, this.onPressed, {Key? key})
|
const PermissionRow(this.name, this.isOk, this.onPressed,
|
||||||
|
{Key? key, this.enabled = true})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final bool isOk;
|
final bool isOk;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -639,9 +665,11 @@ class PermissionRow extends StatelessWidget {
|
|||||||
contentPadding: EdgeInsets.all(0),
|
contentPadding: EdgeInsets.all(0),
|
||||||
title: Text(name),
|
title: Text(name),
|
||||||
value: isOk,
|
value: isOk,
|
||||||
onChanged: (bool value) {
|
onChanged: enabled
|
||||||
onPressed();
|
? (bool value) {
|
||||||
});
|
onPressed();
|
||||||
|
}
|
||||||
|
: null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -841,13 +869,7 @@ class ClientInfo extends StatelessWidget {
|
|||||||
flex: -1,
|
flex: -1,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(right: 12),
|
padding: const EdgeInsets.only(right: 12),
|
||||||
child: CircleAvatar(
|
child: _buildAvatar(context))),
|
||||||
backgroundColor: str2color(
|
|
||||||
client.name,
|
|
||||||
Theme.of(context).brightness == Brightness.light
|
|
||||||
? 255
|
|
||||||
: 150),
|
|
||||||
child: Text(client.name[0])))),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -860,6 +882,20 @@ class ClientInfo extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildAvatar(BuildContext context) {
|
||||||
|
final fallback = CircleAvatar(
|
||||||
|
backgroundColor: str2color(client.name,
|
||||||
|
Theme.of(context).brightness == Brightness.light ? 255 : 150),
|
||||||
|
child: Text(client.name.isNotEmpty ? client.name[0] : '?'),
|
||||||
|
);
|
||||||
|
return buildAvatarWidget(
|
||||||
|
avatar: client.avatar,
|
||||||
|
size: 40,
|
||||||
|
fallback: fallback,
|
||||||
|
) ??
|
||||||
|
fallback;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void androidChannelInit() {
|
void androidChannelInit() {
|
||||||
|
|||||||
@@ -617,7 +617,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
onToggle: (bool v) async {
|
onToggle: (bool v) async {
|
||||||
await mainSetLocalBoolOption(kOptionEnableShowTerminalExtraKeys, v);
|
await mainSetLocalBoolOption(kOptionEnableShowTerminalExtraKeys, v);
|
||||||
final newValue =
|
final newValue =
|
||||||
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||||||
setState(() {
|
setState(() {
|
||||||
_showTerminalExtraKeys = newValue;
|
_showTerminalExtraKeys = newValue;
|
||||||
});
|
});
|
||||||
@@ -688,8 +688,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
||||||
? translate('Login')
|
? translate('Login')
|
||||||
: '${translate('Logout')} (${gFFI.userModel.userName.value})')),
|
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})')),
|
||||||
leading: Icon(Icons.person),
|
leading: Obx(() {
|
||||||
|
final avatar = bind.mainResolveAvatarUrl(
|
||||||
|
avatar: gFFI.userModel.avatar.value);
|
||||||
|
return buildAvatarWidget(
|
||||||
|
avatar: avatar,
|
||||||
|
size: 28,
|
||||||
|
borderRadius: null,
|
||||||
|
fallback: Icon(Icons.person),
|
||||||
|
) ??
|
||||||
|
Icon(Icons.person);
|
||||||
|
}),
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
if (gFFI.userModel.userName.value.isEmpty) {
|
if (gFFI.userModel.userName.value.isEmpty) {
|
||||||
loginDialog();
|
loginDialog();
|
||||||
@@ -829,10 +839,12 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
if (!incomingOnly)
|
if (!incomingOnly)
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('keep-awake-during-outgoing-sessions-label')),
|
title:
|
||||||
|
Text(translate('keep-awake-during-outgoing-sessions-label')),
|
||||||
initialValue: _preventSleepWhileConnected,
|
initialValue: _preventSleepWhileConnected,
|
||||||
onToggle: (v) async {
|
onToggle: (v) async {
|
||||||
await mainSetLocalBoolOption(kOptionKeepAwakeDuringOutgoingSessions, v);
|
await mainSetLocalBoolOption(
|
||||||
|
kOptionKeepAwakeDuringOutgoingSessions, v);
|
||||||
setState(() {
|
setState(() {
|
||||||
_preventSleepWhileConnected = v;
|
_preventSleepWhileConnected = v;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -83,7 +83,10 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
// Register this terminal model with FFI for event routing
|
// Register this terminal model with FFI for event routing
|
||||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||||
|
|
||||||
_showTerminalExtraKeys = mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
// Web desktop users have full hardware keyboard access, so the on-screen
|
||||||
|
// terminal extra keys bar is unnecessary and disabled.
|
||||||
|
_showTerminalExtraKeys = !isWebDesktop &&
|
||||||
|
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||||||
// Initialize terminal connection
|
// Initialize terminal connection
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_ffi.dialogManager
|
_ffi.dialogManager
|
||||||
|
|||||||
@@ -259,13 +259,11 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
|||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
color: MyTheme.canvasColor,
|
color: MyTheme.canvasColor,
|
||||||
child: inputModel.isPhysicalMouse.value
|
child: RawTouchGestureDetectorRegion(
|
||||||
? getBodyForMobile()
|
child: getBodyForMobile(),
|
||||||
: RawTouchGestureDetectorRegion(
|
ffi: gFFI,
|
||||||
child: getBodyForMobile(),
|
isCamera: true,
|
||||||
ffi: gFFI,
|
),
|
||||||
isCamera: true,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,100 +12,6 @@ void _showSuccess() {
|
|||||||
showToast(translate("Successful"));
|
showToast(translate("Successful"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showError() {
|
|
||||||
showToast(translate("Error"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
|
|
||||||
final pw = await bind.mainGetPermanentPassword();
|
|
||||||
final p0 = TextEditingController(text: pw);
|
|
||||||
final p1 = TextEditingController(text: pw);
|
|
||||||
var validateLength = false;
|
|
||||||
var validateSame = false;
|
|
||||||
dialogManager.show((setState, close, context) {
|
|
||||||
submit() async {
|
|
||||||
close();
|
|
||||||
dialogManager.showLoading(translate("Waiting"));
|
|
||||||
if (await gFFI.serverModel.setPermanentPassword(p0.text)) {
|
|
||||||
dialogManager.dismissAll();
|
|
||||||
_showSuccess();
|
|
||||||
} else {
|
|
||||||
dialogManager.dismissAll();
|
|
||||||
_showError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return CustomAlertDialog(
|
|
||||||
title: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
|
||||||
Text(translate('Set your own password')).paddingOnly(left: 10),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
content: Form(
|
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
||||||
TextFormField(
|
|
||||||
autofocus: true,
|
|
||||||
obscureText: true,
|
|
||||||
keyboardType: TextInputType.visiblePassword,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: translate('Password'),
|
|
||||||
),
|
|
||||||
controller: p0,
|
|
||||||
validator: (v) {
|
|
||||||
if (v == null) return null;
|
|
||||||
final val = v.trim().length > 5;
|
|
||||||
if (validateLength != val) {
|
|
||||||
// use delay to make setState success
|
|
||||||
Future.delayed(Duration(microseconds: 1),
|
|
||||||
() => setState(() => validateLength = val));
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
? null
|
|
||||||
: translate('Too short, at least 6 characters.');
|
|
||||||
},
|
|
||||||
).workaroundFreezeLinuxMint(),
|
|
||||||
TextFormField(
|
|
||||||
obscureText: true,
|
|
||||||
keyboardType: TextInputType.visiblePassword,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: translate('Confirmation'),
|
|
||||||
),
|
|
||||||
controller: p1,
|
|
||||||
validator: (v) {
|
|
||||||
if (v == null) return null;
|
|
||||||
final val = p0.text == v;
|
|
||||||
if (validateSame != val) {
|
|
||||||
Future.delayed(Duration(microseconds: 1),
|
|
||||||
() => setState(() => validateSame = val));
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
? null
|
|
||||||
: translate('The confirmation is not identical.');
|
|
||||||
},
|
|
||||||
).workaroundFreezeLinuxMint(),
|
|
||||||
])),
|
|
||||||
onCancel: close,
|
|
||||||
onSubmit: (validateLength && validateSame) ? submit : null,
|
|
||||||
actions: [
|
|
||||||
dialogButton(
|
|
||||||
'Cancel',
|
|
||||||
icon: Icon(Icons.close_rounded),
|
|
||||||
onPressed: close,
|
|
||||||
isOutline: true,
|
|
||||||
),
|
|
||||||
dialogButton(
|
|
||||||
'OK',
|
|
||||||
icon: Icon(Icons.done_rounded),
|
|
||||||
onPressed: (validateLength && validateSame) ? submit : null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void setTemporaryPasswordLengthDialog(
|
void setTemporaryPasswordLengthDialog(
|
||||||
OverlayDialogManager dialogManager) async {
|
OverlayDialogManager dialogManager) async {
|
||||||
List<String> lengths = ['6', '8', '10'];
|
List<String> lengths = ['6', '8', '10'];
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||||
@@ -53,7 +52,9 @@ class AbModel {
|
|||||||
|
|
||||||
RxBool get currentAbLoading => current.abLoading;
|
RxBool get currentAbLoading => current.abLoading;
|
||||||
bool get currentAbEmpty => current.peers.isEmpty && current.tags.isEmpty;
|
bool get currentAbEmpty => current.peers.isEmpty && current.tags.isEmpty;
|
||||||
RxString get currentAbPullError => current.pullError;
|
final _listPullError = ''.obs;
|
||||||
|
RxString get abPullError =>
|
||||||
|
_listPullError.value.isNotEmpty ? _listPullError : current.pullError;
|
||||||
RxString get currentAbPushError => current.pushError;
|
RxString get currentAbPushError => current.pushError;
|
||||||
String? _personalAbGuid;
|
String? _personalAbGuid;
|
||||||
RxBool legacyMode = false.obs;
|
RxBool legacyMode = false.obs;
|
||||||
@@ -68,6 +69,7 @@ class AbModel {
|
|||||||
var _syncFromRecentLock = false;
|
var _syncFromRecentLock = false;
|
||||||
var _timerCounter = 0;
|
var _timerCounter = 0;
|
||||||
var _cacheLoadOnceFlag = false;
|
var _cacheLoadOnceFlag = false;
|
||||||
|
var _pulledOnce = false;
|
||||||
var listInitialized = false;
|
var listInitialized = false;
|
||||||
var _maxPeerOneAb = 0;
|
var _maxPeerOneAb = 0;
|
||||||
|
|
||||||
@@ -97,10 +99,17 @@ class AbModel {
|
|||||||
print("reset ab model");
|
print("reset ab model");
|
||||||
addressbooks.clear();
|
addressbooks.clear();
|
||||||
_currentName.value = '';
|
_currentName.value = '';
|
||||||
|
_listPullError.value = '';
|
||||||
|
_pulledOnce = false;
|
||||||
await bind.mainClearAb();
|
await bind.mainClearAb();
|
||||||
listInitialized = false;
|
listInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearPullErrors() {
|
||||||
|
_listPullError.value = '';
|
||||||
|
current.pullError.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
// #region ab
|
// #region ab
|
||||||
/// Pulls the address book data from the server.
|
/// Pulls the address book data from the server.
|
||||||
///
|
///
|
||||||
@@ -110,31 +119,41 @@ class AbModel {
|
|||||||
var _pulling = false;
|
var _pulling = false;
|
||||||
Future<void> pullAb(
|
Future<void> pullAb(
|
||||||
{required ForcePullAb? force, required bool quiet}) async {
|
{required ForcePullAb? force, required bool quiet}) async {
|
||||||
|
if (bind.isDisableAb()) return;
|
||||||
|
if (!gFFI.userModel.isLogin) return;
|
||||||
|
if (gFFI.userModel.networkError.isNotEmpty) return;
|
||||||
if (_pulling) return;
|
if (_pulling) return;
|
||||||
|
if (force == null && _pulledOnce) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_pulling = true;
|
_pulling = true;
|
||||||
|
if (!quiet) {
|
||||||
|
_listPullError.value = '';
|
||||||
|
current.pullError.value = '';
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await _pullAb(force: force, quiet: quiet);
|
await _pullAb(force: force, quiet: quiet);
|
||||||
_refreshTab();
|
_refreshTab();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
_pulling = false;
|
_pulling = false;
|
||||||
|
_pulledOnce = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pullAb(
|
Future<void> _pullAb(
|
||||||
{required ForcePullAb? force, required bool quiet}) async {
|
{required ForcePullAb? force, required bool quiet}) async {
|
||||||
if (bind.isDisableAb()) return;
|
|
||||||
if (!gFFI.userModel.isLogin) return;
|
|
||||||
if (gFFI.userModel.networkError.isNotEmpty) return;
|
|
||||||
if (force == null && listInitialized && current.initialized) return;
|
if (force == null && listInitialized && current.initialized) return;
|
||||||
debugPrint("pullAb, force: $force, quiet: $quiet");
|
debugPrint("pullAb, force: $force, quiet: $quiet");
|
||||||
if (!listInitialized || force == ForcePullAb.listAndCurrent) {
|
if (!listInitialized || force == ForcePullAb.listAndCurrent) {
|
||||||
try {
|
try {
|
||||||
// Read personal guid every time to avoid upgrading the server without closing the main window
|
// Read personal guid every time to avoid upgrading the server without closing the main window
|
||||||
_personalAbGuid = null;
|
_personalAbGuid = null;
|
||||||
await _getPersonalAbGuid();
|
// `true`: continue init. `false`: stop, error already recorded.
|
||||||
// Determine legacy mode based on whether _personalAbGuid is null
|
if (!await _getPersonalAbGuid(quiet: quiet)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
legacyMode.value = _personalAbGuid == null;
|
legacyMode.value = _personalAbGuid == null;
|
||||||
if (!legacyMode.value && _maxPeerOneAb == 0) {
|
if (!legacyMode.value && _maxPeerOneAb == 0) {
|
||||||
await _getAbSettings();
|
await _getAbSettings(quiet: quiet);
|
||||||
}
|
}
|
||||||
if (_personalAbGuid != null) {
|
if (_personalAbGuid != null) {
|
||||||
debugPrint("pull ab list");
|
debugPrint("pull ab list");
|
||||||
@@ -142,7 +161,7 @@ class AbModel {
|
|||||||
abProfiles.add(AbProfile(_personalAbGuid!, _personalAddressBookName,
|
abProfiles.add(AbProfile(_personalAbGuid!, _personalAddressBookName,
|
||||||
gFFI.userModel.userName.value, null, ShareRule.read.value, null));
|
gFFI.userModel.userName.value, null, ShareRule.read.value, null));
|
||||||
// get all address book name
|
// get all address book name
|
||||||
await _getSharedAbProfiles(abProfiles);
|
await _getSharedAbProfiles(abProfiles, quiet: quiet);
|
||||||
addressbooks.removeWhere((key, value) =>
|
addressbooks.removeWhere((key, value) =>
|
||||||
abProfiles.firstWhereOrNull((e) => e.name == key) == null);
|
abProfiles.firstWhereOrNull((e) => e.name == key) == null);
|
||||||
for (int i = 0; i < abProfiles.length; i++) {
|
for (int i = 0; i < abProfiles.length; i++) {
|
||||||
@@ -182,6 +201,7 @@ class AbModel {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("pull ab list error: $e");
|
debugPrint("pull ab list error: $e");
|
||||||
|
_setListPullError(e, quiet: quiet);
|
||||||
}
|
}
|
||||||
} else if (listInitialized &&
|
} else if (listInitialized &&
|
||||||
(!current.initialized || force == ForcePullAb.current)) {
|
(!current.initialized || force == ForcePullAb.current)) {
|
||||||
@@ -197,14 +217,26 @@ class AbModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _getAbSettings() async {
|
void _setListPullError(Object err, {required bool quiet, int? statusCode}) {
|
||||||
|
if (!quiet) {
|
||||||
|
_listPullError.value =
|
||||||
|
'${translate('pull_ab_failed_tip')}: ${translate(err.toString())}';
|
||||||
|
}
|
||||||
|
if (statusCode == 401) {
|
||||||
|
gFFI.userModel.reset(resetOther: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _getAbSettings({required bool quiet}) async {
|
||||||
|
int? statusCode;
|
||||||
try {
|
try {
|
||||||
final api = "${await bind.mainGetApiServer()}/api/ab/settings";
|
final api = "${await bind.mainGetApiServer()}/api/ab/settings";
|
||||||
var headers = getHttpHeaders();
|
var headers = getHttpHeaders();
|
||||||
headers['Content-Type'] = "application/json";
|
headers['Content-Type'] = "application/json";
|
||||||
_setEmptyBody(headers);
|
_setEmptyBody(headers);
|
||||||
final resp = await http.post(Uri.parse(api), headers: headers);
|
final resp = await http.post(Uri.parse(api), headers: headers);
|
||||||
if (resp.statusCode == 404) {
|
statusCode = resp.statusCode;
|
||||||
|
if (statusCode == 404) {
|
||||||
debugPrint("HTTP 404, api server doesn't support shared address book");
|
debugPrint("HTTP 404, api server doesn't support shared address book");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -213,46 +245,57 @@ class AbModel {
|
|||||||
if (json.containsKey('error')) {
|
if (json.containsKey('error')) {
|
||||||
throw json['error'];
|
throw json['error'];
|
||||||
}
|
}
|
||||||
if (resp.statusCode != 200) {
|
if (statusCode != 200) {
|
||||||
throw 'HTTP ${resp.statusCode}';
|
throw 'HTTP $statusCode';
|
||||||
}
|
}
|
||||||
_maxPeerOneAb = json['max_peer_one_ab'] ?? 0;
|
_maxPeerOneAb = json['max_peer_one_ab'] ?? 0;
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugPrint('get ab settings err: ${err.toString()}');
|
debugPrint('get ab settings err: ${err.toString()}');
|
||||||
|
_setListPullError(err, quiet: quiet, statusCode: statusCode);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _getPersonalAbGuid() async {
|
/// Loads `/api/ab/personal`.
|
||||||
|
/// Returns `true` to continue init, `false` to stop after a real error.
|
||||||
|
Future<bool> _getPersonalAbGuid({required bool quiet}) async {
|
||||||
|
int? statusCode;
|
||||||
try {
|
try {
|
||||||
final api = "${await bind.mainGetApiServer()}/api/ab/personal";
|
final api = "${await bind.mainGetApiServer()}/api/ab/personal";
|
||||||
var headers = getHttpHeaders();
|
var headers = getHttpHeaders();
|
||||||
headers['Content-Type'] = "application/json";
|
headers['Content-Type'] = "application/json";
|
||||||
_setEmptyBody(headers);
|
_setEmptyBody(headers);
|
||||||
final resp = await http.post(Uri.parse(api), headers: headers);
|
final resp = await http.post(Uri.parse(api), headers: headers);
|
||||||
if (resp.statusCode == 404) {
|
statusCode = resp.statusCode;
|
||||||
|
if (statusCode == 404) {
|
||||||
debugPrint("HTTP 404, current api server is legacy mode");
|
debugPrint("HTTP 404, current api server is legacy mode");
|
||||||
return false;
|
// Old server: keep `_personalAbGuid` null and continue in legacy mode.
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
Map<String, dynamic> json =
|
Map<String, dynamic> json =
|
||||||
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
|
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
|
||||||
if (json.containsKey('error')) {
|
if (json.containsKey('error')) {
|
||||||
throw json['error'];
|
throw json['error'];
|
||||||
}
|
}
|
||||||
if (resp.statusCode != 200) {
|
if (statusCode != 200) {
|
||||||
throw 'HTTP ${resp.statusCode}';
|
throw 'HTTP $statusCode';
|
||||||
}
|
}
|
||||||
_personalAbGuid = json['guid'];
|
_personalAbGuid = json['guid'];
|
||||||
|
// New server: guid is available, continue in non-legacy mode.
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugPrint('get personal ab err: ${err.toString()}');
|
debugPrint('get personal ab err: ${err.toString()}');
|
||||||
|
_setListPullError(err, quiet: quiet, statusCode: statusCode);
|
||||||
}
|
}
|
||||||
|
// Real error: stop the current pull.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _getSharedAbProfiles(List<AbProfile> profiles) async {
|
Future<bool> _getSharedAbProfiles(List<AbProfile> profiles,
|
||||||
|
{required bool quiet}) async {
|
||||||
final api = "${await bind.mainGetApiServer()}/api/ab/shared/profiles";
|
final api = "${await bind.mainGetApiServer()}/api/ab/shared/profiles";
|
||||||
|
int? statusCode;
|
||||||
try {
|
try {
|
||||||
var uri0 = Uri.parse(api);
|
var uri0 = Uri.parse(api);
|
||||||
final pageSize = 100;
|
final pageSize = 100;
|
||||||
@@ -273,13 +316,19 @@ class AbModel {
|
|||||||
headers['Content-Type'] = "application/json";
|
headers['Content-Type'] = "application/json";
|
||||||
_setEmptyBody(headers);
|
_setEmptyBody(headers);
|
||||||
final resp = await http.post(uri, headers: headers);
|
final resp = await http.post(uri, headers: headers);
|
||||||
|
statusCode = resp.statusCode;
|
||||||
|
if (statusCode == 404) {
|
||||||
|
debugPrint(
|
||||||
|
"HTTP 404, api server doesn't support shared address book");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
Map<String, dynamic> json =
|
Map<String, dynamic> json =
|
||||||
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
|
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
|
||||||
if (json.containsKey('error')) {
|
if (json.containsKey('error')) {
|
||||||
throw json['error'];
|
throw json['error'];
|
||||||
}
|
}
|
||||||
if (resp.statusCode != 200) {
|
if (statusCode != 200) {
|
||||||
throw 'HTTP ${resp.statusCode}';
|
throw 'HTTP $statusCode';
|
||||||
}
|
}
|
||||||
if (json.containsKey('total')) {
|
if (json.containsKey('total')) {
|
||||||
if (total == 0) total = json['total'];
|
if (total == 0) total = json['total'];
|
||||||
@@ -302,6 +351,7 @@ class AbModel {
|
|||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugPrint('_getSharedAbProfiles err: ${err.toString()}');
|
debugPrint('_getSharedAbProfiles err: ${err.toString()}');
|
||||||
|
_setListPullError(err, quiet: quiet, statusCode: statusCode);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ class GroupModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset() async {
|
reset() async {
|
||||||
|
initialized = false;
|
||||||
groupLoadError.value = '';
|
groupLoadError.value = '';
|
||||||
deviceGroups.clear();
|
deviceGroups.clear();
|
||||||
users.clear();
|
users.clear();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
@@ -15,12 +16,13 @@ import 'package:get/get.dart';
|
|||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
import '../../models/state_model.dart';
|
import '../../models/state_model.dart';
|
||||||
|
import 'input_modifier_utils.dart';
|
||||||
import 'relative_mouse_model.dart';
|
import 'relative_mouse_model.dart';
|
||||||
import '../common.dart';
|
import '../common.dart';
|
||||||
import '../consts.dart';
|
import '../consts.dart';
|
||||||
|
|
||||||
/// Mouse button enum.
|
/// Mouse button enum.
|
||||||
enum MouseButtons { left, right, wheel, back }
|
enum MouseButtons { left, right, wheel, back, forward }
|
||||||
|
|
||||||
const _kMouseEventDown = 'mousedown';
|
const _kMouseEventDown = 'mousedown';
|
||||||
const _kMouseEventUp = 'mouseup';
|
const _kMouseEventUp = 'mouseup';
|
||||||
@@ -157,6 +159,8 @@ extension ToString on MouseButtons {
|
|||||||
return 'wheel';
|
return 'wheel';
|
||||||
case MouseButtons.back:
|
case MouseButtons.back:
|
||||||
return 'back';
|
return 'back';
|
||||||
|
case MouseButtons.forward:
|
||||||
|
return 'forward';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,6 +331,80 @@ class ToReleaseKeys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class InputModel {
|
class InputModel {
|
||||||
|
// Side mouse button support for Linux.
|
||||||
|
// Flutter's Linux embedder drops X11 button 8/9 events, so we capture them
|
||||||
|
// natively via GDK and forward through the platform channel.
|
||||||
|
static InputModel? _activeSideButtonModel;
|
||||||
|
// Tracks per-button which model received a side button down event, so the
|
||||||
|
// matching up event is routed there even if the pointer has left the view
|
||||||
|
// or a different button was pressed in between.
|
||||||
|
static final Map<MouseButtons, InputModel> _sideButtonDownModels = {};
|
||||||
|
static bool _sideButtonChannelInitialized = false;
|
||||||
|
|
||||||
|
/// Each Flutter engine (main window + sub-windows from desktop_multi_window)
|
||||||
|
/// runs its own Dart isolate with its own statics. Called from initEnv()
|
||||||
|
/// which runs per-engine, so each isolate registers its own handler tied
|
||||||
|
/// to its own set of InputModels.
|
||||||
|
static void initSideButtonChannel() {
|
||||||
|
if (!Platform.isLinux) return;
|
||||||
|
if (_sideButtonChannelInitialized) return;
|
||||||
|
_sideButtonChannelInitialized = true;
|
||||||
|
|
||||||
|
const channel = MethodChannel('org.rustdesk.rustdesk/side_buttons');
|
||||||
|
channel.setMethodCallHandler((call) async {
|
||||||
|
if (call.method == 'onSideMouseButton') {
|
||||||
|
final args = call.arguments as Map<dynamic, dynamic>;
|
||||||
|
final button = args['button'] as String;
|
||||||
|
final type = args['type'] as String;
|
||||||
|
final mb = button == 'back' ? MouseButtons.back : MouseButtons.forward;
|
||||||
|
|
||||||
|
if (type == 'down') {
|
||||||
|
final model = _activeSideButtonModel;
|
||||||
|
if (model != null &&
|
||||||
|
!(model.isViewOnly && !model.showMyCursor) &&
|
||||||
|
model.keyboardPerm &&
|
||||||
|
!model.isViewCamera) {
|
||||||
|
_sideButtonDownModels[mb] = model;
|
||||||
|
// Fire-and-forget to avoid blocking the platform channel handler.
|
||||||
|
unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) {
|
||||||
|
debugPrint('[InputModel] failed to send side button $type for $mb: $e');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only route 'up' when we recorded the matching 'down';
|
||||||
|
// dropping avoids sending unpaired 'up' to an unrelated session.
|
||||||
|
// Use _sendMouseUnchecked to bypass permission checks so the
|
||||||
|
// release always goes through even if permissions changed.
|
||||||
|
final model = _sideButtonDownModels.remove(mb);
|
||||||
|
if (model != null) {
|
||||||
|
unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) {
|
||||||
|
debugPrint('[InputModel] failed to send side button $type for $mb: $e');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear any static references to this model (prevents stale routing).
|
||||||
|
/// Releases any held side buttons on the peer so closing a session
|
||||||
|
/// mid-press does not leave a stuck button.
|
||||||
|
void disposeSideButtonTracking() {
|
||||||
|
if (_activeSideButtonModel == this) _activeSideButtonModel = null;
|
||||||
|
final held = _sideButtonDownModels.entries
|
||||||
|
.where((e) => e.value == this)
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
for (final mb in held) {
|
||||||
|
_sideButtonDownModels.remove(mb);
|
||||||
|
// Best-effort release; session may already be tearing down.
|
||||||
|
unawaited(_sendMouseUnchecked('up', mb).catchError((Object e) {
|
||||||
|
debugPrint('[InputModel] failed to release side button $mb: $e');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final WeakReference<FFI> parent;
|
final WeakReference<FFI> parent;
|
||||||
String keyboardMode = '';
|
String keyboardMode = '';
|
||||||
|
|
||||||
@@ -348,6 +426,12 @@ class InputModel {
|
|||||||
final _trackpadAdjustPeerLinux = 0.06;
|
final _trackpadAdjustPeerLinux = 0.06;
|
||||||
// This is an experience value.
|
// This is an experience value.
|
||||||
final _trackpadAdjustMacToWin = 2.50;
|
final _trackpadAdjustMacToWin = 2.50;
|
||||||
|
// Ignore directional locking for very small deltas on both axes (including
|
||||||
|
// tiny single-axis movement) to avoid over-filtering near zero.
|
||||||
|
static const double _trackpadAxisNoiseThreshold = 0.2;
|
||||||
|
// Lock to dominant axis only when one axis is clearly stronger.
|
||||||
|
// 1.6 means the dominant axis must be >= 60% larger than the other.
|
||||||
|
static const double _trackpadAxisLockRatio = 1.6;
|
||||||
int _trackpadSpeed = kDefaultTrackpadSpeed;
|
int _trackpadSpeed = kDefaultTrackpadSpeed;
|
||||||
double _trackpadSpeedInner = kDefaultTrackpadSpeed / 100.0;
|
double _trackpadSpeedInner = kDefaultTrackpadSpeed / 100.0;
|
||||||
var _trackpadScrollUnsent = Offset.zero;
|
var _trackpadScrollUnsent = Offset.zero;
|
||||||
@@ -365,6 +449,16 @@ class InputModel {
|
|||||||
final isPhysicalMouse = false.obs;
|
final isPhysicalMouse = false.obs;
|
||||||
int _lastButtons = 0;
|
int _lastButtons = 0;
|
||||||
Offset lastMousePos = Offset.zero;
|
Offset lastMousePos = Offset.zero;
|
||||||
|
int _lastWheelTsUs = 0;
|
||||||
|
|
||||||
|
// Wheel acceleration thresholds.
|
||||||
|
static const int _wheelAccelFastThresholdUs = 40000; // 40ms
|
||||||
|
static const int _wheelAccelMediumThresholdUs = 80000; // 80ms
|
||||||
|
static const double _wheelBurstVelocityThreshold =
|
||||||
|
0.002; // delta units per microsecond
|
||||||
|
// Wheel burst acceleration (empirical tuning).
|
||||||
|
// Applies only to fast, non-smooth bursts to preserve single-step scrolling.
|
||||||
|
// Flutter uses microseconds for dt, so velocity is in delta/us.
|
||||||
|
|
||||||
// Relative mouse mode (for games/3D apps).
|
// Relative mouse mode (for games/3D apps).
|
||||||
final relativeMouseMode = false.obs;
|
final relativeMouseMode = false.obs;
|
||||||
@@ -396,6 +490,7 @@ class InputModel {
|
|||||||
bool get isRelativeMouseModeSupported => _relativeMouse.isSupported;
|
bool get isRelativeMouseModeSupported => _relativeMouse.isSupported;
|
||||||
|
|
||||||
InputModel(this.parent) {
|
InputModel(this.parent) {
|
||||||
|
initSideButtonChannel();
|
||||||
sessionId = parent.target!.sessionId;
|
sessionId = parent.target!.sessionId;
|
||||||
_relativeMouse = RelativeMouseModel(
|
_relativeMouse = RelativeMouseModel(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
@@ -604,6 +699,38 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Safe: this only re-dispatches synthesized Shift key-up events.
|
||||||
|
// The key-up path clears the tracked Shift state so this does not loop.
|
||||||
|
void _releaseTrackedShiftKeyEventIfNeeded() {
|
||||||
|
final leftShift = toReleaseKeys.lastLShiftKeyEvent;
|
||||||
|
final rightShift = toReleaseKeys.lastRShiftKeyEvent;
|
||||||
|
if (leftShift != null) {
|
||||||
|
handleKeyEvent(leftShift);
|
||||||
|
}
|
||||||
|
if (rightShift != null) {
|
||||||
|
handleKeyEvent(rightShift);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe: this only re-dispatches synthesized Shift key-up events.
|
||||||
|
// The raw key-up path clears the tracked Shift state so this does not loop.
|
||||||
|
void _releaseTrackedRawShiftKeyEventIfNeeded() {
|
||||||
|
final leftShift = toReleaseRawKeys.lastLShiftKeyEvent;
|
||||||
|
final rightShift = toReleaseRawKeys.lastRShiftKeyEvent;
|
||||||
|
if (leftShift != null) {
|
||||||
|
handleRawKeyEvent(RawKeyUpEvent(
|
||||||
|
data: leftShift.data,
|
||||||
|
character: leftShift.character,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (rightShift != null) {
|
||||||
|
handleRawKeyEvent(RawKeyUpEvent(
|
||||||
|
data: rightShift.data,
|
||||||
|
character: rightShift.character,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
|
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
|
||||||
if (isViewOnly) return KeyEventResult.handled;
|
if (isViewOnly) return KeyEventResult.handled;
|
||||||
if (isViewCamera) return KeyEventResult.handled;
|
if (isViewCamera) return KeyEventResult.handled;
|
||||||
@@ -658,6 +785,27 @@ class InputModel {
|
|||||||
toReleaseRawKeys.updateKeyUp(key, e);
|
toReleaseRawKeys.updateKeyUp(key, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On some mobile soft-keyboard paths, Flutter may leave cached Shift state
|
||||||
|
// set even though the current raw key event is not shifted anymore.
|
||||||
|
if (e is RawKeyDownEvent &&
|
||||||
|
shouldReleaseStaleMobileShift(
|
||||||
|
isMobile: isMobile,
|
||||||
|
cachedShiftPressed: shift,
|
||||||
|
actualShiftPressed: e.isShiftPressed,
|
||||||
|
logicalKey: e.logicalKey,
|
||||||
|
hasTrackedShiftKeyDown: toReleaseRawKeys.lastLShiftKeyEvent != null ||
|
||||||
|
toReleaseRawKeys.lastRShiftKeyEvent != null,
|
||||||
|
)) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
'input: releasing stale mobile Shift before replaying tracked raw '
|
||||||
|
'key-up (logicalKey=${e.logicalKey.keyLabel}, '
|
||||||
|
'actualShiftPressed=${e.isShiftPressed}, cachedShiftPressed=$shift)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_releaseTrackedRawShiftKeyEventIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
// * Currently mobile does not enable map mode
|
// * Currently mobile does not enable map mode
|
||||||
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||||
mapKeyboardModeRaw(e, iosCapsLock);
|
mapKeyboardModeRaw(e, iosCapsLock);
|
||||||
@@ -701,6 +849,8 @@ class InputModel {
|
|||||||
iosCapsLock = _getIosCapsFromCharacter(e);
|
iosCapsLock = _getIosCapsFromCharacter(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update cached modifier state before sending the event. The stale mobile
|
||||||
|
// Shift release check below relies on this cached state.
|
||||||
if (e is KeyUpEvent) {
|
if (e is KeyUpEvent) {
|
||||||
handleKeyUpEventModifiers(e);
|
handleKeyUpEventModifiers(e);
|
||||||
} else if (e is KeyDownEvent) {
|
} else if (e is KeyDownEvent) {
|
||||||
@@ -738,6 +888,21 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On some mobile soft-keyboard paths, Flutter may leave cached Shift state
|
||||||
|
// set even though the current key event is not shifted anymore.
|
||||||
|
if (e is KeyDownEvent &&
|
||||||
|
shouldReleaseStaleMobileShift(
|
||||||
|
isMobile: isMobile,
|
||||||
|
cachedShiftPressed: shift,
|
||||||
|
actualShiftPressed: HardwareKeyboard.instance.isShiftPressed,
|
||||||
|
logicalKey: e.logicalKey,
|
||||||
|
hasTrackedShiftKeyDown: toReleaseKeys.lastLShiftKeyEvent != null ||
|
||||||
|
toReleaseKeys.lastRShiftKeyEvent != null,
|
||||||
|
)) {
|
||||||
|
_releaseTrackedShiftKeyEventIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
final isDesktopAndMapMode =
|
final isDesktopAndMapMode =
|
||||||
isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode);
|
isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode);
|
||||||
if (isMobileAndMapMode || isDesktopAndMapMode) {
|
if (isMobileAndMapMode || isDesktopAndMapMode) {
|
||||||
@@ -950,13 +1115,20 @@ class InputModel {
|
|||||||
return evt;
|
return evt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send mouse event unconditionally (no permission checks).
|
||||||
|
/// Used for side button releases that must go through even if permissions
|
||||||
|
/// changed after the matching down was sent.
|
||||||
|
Future<void> _sendMouseUnchecked(String type, MouseButtons button) async {
|
||||||
|
await bind.sessionSendMouse(
|
||||||
|
sessionId: sessionId,
|
||||||
|
msg: json.encode(modify({'type': type, 'buttons': button.value})));
|
||||||
|
}
|
||||||
|
|
||||||
/// Send mouse press event.
|
/// Send mouse press event.
|
||||||
Future<void> sendMouse(String type, MouseButtons button) async {
|
Future<void> sendMouse(String type, MouseButtons button) async {
|
||||||
if (!keyboardPerm) return;
|
if (!keyboardPerm) return;
|
||||||
if (isViewCamera) return;
|
if (isViewCamera) return;
|
||||||
await bind.sessionSendMouse(
|
await _sendMouseUnchecked(type, button);
|
||||||
sessionId: sessionId,
|
|
||||||
msg: json.encode(modify({'type': type, 'buttons': button.value})));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void enterOrLeave(bool enter) {
|
void enterOrLeave(bool enter) {
|
||||||
@@ -964,6 +1136,14 @@ class InputModel {
|
|||||||
toReleaseRawKeys.release(handleRawKeyEvent);
|
toReleaseRawKeys.release(handleRawKeyEvent);
|
||||||
_pointerMovedAfterEnter = false;
|
_pointerMovedAfterEnter = false;
|
||||||
_pointerInsideImage = enter;
|
_pointerInsideImage = enter;
|
||||||
|
_lastWheelTsUs = 0;
|
||||||
|
|
||||||
|
// Track active model for side button events (Linux).
|
||||||
|
if (enter) {
|
||||||
|
_activeSideButtonModel = this;
|
||||||
|
} else if (_activeSideButtonModel == this) {
|
||||||
|
_activeSideButtonModel = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Fix status
|
// Fix status
|
||||||
if (!enter) {
|
if (!enter) {
|
||||||
@@ -1161,6 +1341,7 @@ class InputModel {
|
|||||||
if (isMacOS && peerPlatform == kPeerPlatformWindows) {
|
if (isMacOS && peerPlatform == kPeerPlatformWindows) {
|
||||||
delta *= _trackpadAdjustMacToWin;
|
delta *= _trackpadAdjustMacToWin;
|
||||||
}
|
}
|
||||||
|
delta = _filterTrackpadDeltaAxis(delta);
|
||||||
_trackpadLastDelta = delta;
|
_trackpadLastDelta = delta;
|
||||||
|
|
||||||
var x = delta.dx.toInt();
|
var x = delta.dx.toInt();
|
||||||
@@ -1193,6 +1374,24 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Offset _filterTrackpadDeltaAxis(Offset delta) {
|
||||||
|
final absDx = delta.dx.abs();
|
||||||
|
final absDy = delta.dy.abs();
|
||||||
|
// Keep diagonal intent when movement is tiny on both axes.
|
||||||
|
if (absDx < _trackpadAxisNoiseThreshold &&
|
||||||
|
absDy < _trackpadAxisNoiseThreshold) {
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
// Dominant-axis lock to reduce accidental cross-axis scrolling noise.
|
||||||
|
if (absDy >= absDx * _trackpadAxisLockRatio) {
|
||||||
|
return Offset(0, delta.dy);
|
||||||
|
}
|
||||||
|
if (absDx >= absDy * _trackpadAxisLockRatio) {
|
||||||
|
return Offset(delta.dx, 0);
|
||||||
|
}
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
|
||||||
void _scheduleFling(double x, double y, int delay) {
|
void _scheduleFling(double x, double y, int delay) {
|
||||||
if (isViewCamera) return;
|
if (isViewCamera) return;
|
||||||
if ((x == 0 && y == 0) || _stopFling) {
|
if ((x == 0 && y == 0) || _stopFling) {
|
||||||
@@ -1296,6 +1495,16 @@ class InputModel {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// iOS may emit a synthesized touch event after a real mouse click.
|
||||||
|
/// This helper ignores touch-down events that arrive shortly after a mouse down,
|
||||||
|
/// even when the position is far (e.g., near the top edge).
|
||||||
|
bool _shouldIgnoreTouchAfterMouse(int nowMs) {
|
||||||
|
if (!isIOS) return false;
|
||||||
|
const int kTouchAfterMouseWindowMs = 700;
|
||||||
|
final dt = nowMs - _lastMouseDownTimeMs;
|
||||||
|
return dt >= 0 && dt < kTouchAfterMouseWindowMs;
|
||||||
|
}
|
||||||
|
|
||||||
void onPointDownImage(PointerDownEvent e) {
|
void onPointDownImage(PointerDownEvent e) {
|
||||||
debugPrint("onPointDownImage ${e.kind}");
|
debugPrint("onPointDownImage ${e.kind}");
|
||||||
_stopFling = true;
|
_stopFling = true;
|
||||||
@@ -1308,6 +1517,9 @@ class InputModel {
|
|||||||
// Track mouse down events for duplicate detection on iOS.
|
// Track mouse down events for duplicate detection on iOS.
|
||||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||||
if (e.kind == ui.PointerDeviceKind.mouse) {
|
if (e.kind == ui.PointerDeviceKind.mouse) {
|
||||||
|
if (!isPhysicalMouse.value) {
|
||||||
|
isPhysicalMouse.value = true;
|
||||||
|
}
|
||||||
_lastMouseDownTimeMs = nowMs;
|
_lastMouseDownTimeMs = nowMs;
|
||||||
_lastMouseDownPos = e.position;
|
_lastMouseDownPos = e.position;
|
||||||
}
|
}
|
||||||
@@ -1317,6 +1529,10 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||||
|
// Ignore duplicate touch events that follow a recent mouse click (iOS Magic Mouse issue).
|
||||||
|
if (isPhysicalMouse.value && _shouldIgnoreTouchAfterMouse(nowMs)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
isPhysicalMouse.value = false;
|
isPhysicalMouse.value = false;
|
||||||
}
|
}
|
||||||
@@ -1407,17 +1623,44 @@ class InputModel {
|
|||||||
if (isViewOnly) return;
|
if (isViewOnly) return;
|
||||||
if (isViewCamera) return;
|
if (isViewCamera) return;
|
||||||
if (e is PointerScrollEvent) {
|
if (e is PointerScrollEvent) {
|
||||||
var dx = e.scrollDelta.dx.toInt();
|
final rawDx = e.scrollDelta.dx;
|
||||||
var dy = e.scrollDelta.dy.toInt();
|
final rawDy = e.scrollDelta.dy;
|
||||||
|
final dominantDelta = rawDx.abs() > rawDy.abs() ? rawDx.abs() : rawDy.abs();
|
||||||
|
final isSmooth = dominantDelta < 1;
|
||||||
|
final nowUs = DateTime.now().microsecondsSinceEpoch;
|
||||||
|
final dtUs = _lastWheelTsUs == 0 ? 0 : nowUs - _lastWheelTsUs;
|
||||||
|
_lastWheelTsUs = nowUs;
|
||||||
|
int accel = 1;
|
||||||
|
if (!isSmooth &&
|
||||||
|
dtUs > 0 &&
|
||||||
|
dtUs <= _wheelAccelMediumThresholdUs &&
|
||||||
|
(isWindows || isLinux) &&
|
||||||
|
peerPlatform == kPeerPlatformMacOS) {
|
||||||
|
final velocity = dominantDelta / dtUs;
|
||||||
|
if (velocity >= _wheelBurstVelocityThreshold) {
|
||||||
|
if (dtUs < _wheelAccelFastThresholdUs) {
|
||||||
|
accel = 3;
|
||||||
|
} else {
|
||||||
|
accel = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var dx = rawDx.toInt();
|
||||||
|
var dy = rawDy.toInt();
|
||||||
|
if (rawDx.abs() > rawDy.abs()) {
|
||||||
|
dy = 0;
|
||||||
|
} else {
|
||||||
|
dx = 0;
|
||||||
|
}
|
||||||
if (dx > 0) {
|
if (dx > 0) {
|
||||||
dx = -1;
|
dx = -accel;
|
||||||
} else if (dx < 0) {
|
} else if (dx < 0) {
|
||||||
dx = 1;
|
dx = accel;
|
||||||
}
|
}
|
||||||
if (dy > 0) {
|
if (dy > 0) {
|
||||||
dy = -1;
|
dy = -accel;
|
||||||
} else if (dy < 0) {
|
} else if (dy < 0) {
|
||||||
dy = 1;
|
dy = accel;
|
||||||
}
|
}
|
||||||
bind.sessionSendMouse(
|
bind.sessionSendMouse(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
|
|||||||
38
flutter/lib/models/input_modifier_utils.dart
Normal file
38
flutter/lib/models/input_modifier_utils.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// Returns true when a stale mobile one-shot Shift state should be released
|
||||||
|
/// by replaying a tracked Shift key-down as a synthesized key-up.
|
||||||
|
///
|
||||||
|
/// This is only valid on mobile when Flutter's cached Shift state is still on
|
||||||
|
/// (`cachedShiftPressed == true`) but the current hardware/raw event reports
|
||||||
|
/// Shift as off (`actualShiftPressed == false`).
|
||||||
|
///
|
||||||
|
/// A tracked Shift key-down is required so the caller can safely synthesize the
|
||||||
|
/// matching key-up. Both `shiftLeft` and `shiftRight` are excluded because the
|
||||||
|
/// Shift key event itself must be processed first; otherwise we could release
|
||||||
|
/// the tracked key while still handling the original Shift press/release.
|
||||||
|
/// Callers should evaluate this only after their cached modifier state has been
|
||||||
|
/// updated for the current event.
|
||||||
|
///
|
||||||
|
/// When this returns true, the caller logs a line like:
|
||||||
|
/// `input: releasing stale mobile Shift before replaying tracked raw key-up`
|
||||||
|
/// immediately before calling `_releaseTrackedRawShiftKeyEventIfNeeded()`.
|
||||||
|
bool shouldReleaseStaleMobileShift({
|
||||||
|
required bool isMobile,
|
||||||
|
required bool cachedShiftPressed,
|
||||||
|
required bool actualShiftPressed,
|
||||||
|
required LogicalKeyboardKey logicalKey,
|
||||||
|
required bool hasTrackedShiftKeyDown,
|
||||||
|
}) {
|
||||||
|
if (!isMobile || !cachedShiftPressed || actualShiftPressed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hasTrackedShiftKeyDown) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (logicalKey == LogicalKeyboardKey.shiftLeft ||
|
||||||
|
logicalKey == LogicalKeyboardKey.shiftRight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1016,19 +1016,31 @@ class FfiModel with ChangeNotifier {
|
|||||||
showMsgBox(SessionID sessionId, String type, String title, String text,
|
showMsgBox(SessionID sessionId, String type, String title, String text,
|
||||||
String link, bool hasRetry, OverlayDialogManager dialogManager,
|
String link, bool hasRetry, OverlayDialogManager dialogManager,
|
||||||
{bool? hasCancel}) async {
|
{bool? hasCancel}) async {
|
||||||
final showNoteEdit = parent.target != null &&
|
final noteAllowed = parent.target != null &&
|
||||||
allowAskForNoteAtEndOfConnection(parent.target, false) &&
|
allowAskForNoteAtEndOfConnection(parent.target, false) &&
|
||||||
(title == "Connection Error" || type == "restarting") &&
|
(title == "Connection Error" || type == "restarting");
|
||||||
!hasRetry;
|
final showNoteEdit = noteAllowed && !hasRetry;
|
||||||
if (showNoteEdit) {
|
if (showNoteEdit) {
|
||||||
await showConnEndAuditDialogCloseCanceled(
|
await showConnEndAuditDialogCloseCanceled(
|
||||||
ffi: parent.target!, type: type, title: title, text: text);
|
ffi: parent.target!, type: type, title: title, text: text);
|
||||||
closeConnection();
|
closeConnection();
|
||||||
} else {
|
} else {
|
||||||
|
VoidCallback? onSubmit;
|
||||||
|
if (noteAllowed && hasRetry) {
|
||||||
|
final ffi = parent.target!;
|
||||||
|
onSubmit = () async {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
await showConnEndAuditDialogCloseCanceled(
|
||||||
|
ffi: ffi, type: type, title: title, text: text);
|
||||||
|
closeConnection();
|
||||||
|
};
|
||||||
|
}
|
||||||
msgBox(sessionId, type, title, text, link, dialogManager,
|
msgBox(sessionId, type, title, text, link, dialogManager,
|
||||||
hasCancel: hasCancel,
|
hasCancel: hasCancel,
|
||||||
reconnect: hasRetry ? reconnect : null,
|
reconnect: hasRetry ? reconnect : null,
|
||||||
reconnectTimeout: hasRetry ? _reconnects : null);
|
reconnectTimeout: hasRetry ? _reconnects : null,
|
||||||
|
onSubmit: onSubmit);
|
||||||
}
|
}
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
if (hasRetry) {
|
if (hasRetry) {
|
||||||
@@ -2152,6 +2164,9 @@ class CanvasModel with ChangeNotifier {
|
|||||||
ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle();
|
ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle();
|
||||||
|
|
||||||
Timer? _timerMobileFocusCanvasCursor;
|
Timer? _timerMobileFocusCanvasCursor;
|
||||||
|
Timer? _timerMobileRestoreCanvasOffset;
|
||||||
|
Offset? _offsetBeforeMobileSoftKeyboard;
|
||||||
|
double? _scaleBeforeMobileSoftKeyboard;
|
||||||
|
|
||||||
// `isMobileCanvasChanged` is used to avoid canvas reset when changing the input method
|
// `isMobileCanvasChanged` is used to avoid canvas reset when changing the input method
|
||||||
// after showing the soft keyboard.
|
// after showing the soft keyboard.
|
||||||
@@ -2639,6 +2654,9 @@ class CanvasModel with ChangeNotifier {
|
|||||||
_scale = 1.0;
|
_scale = 1.0;
|
||||||
_lastViewStyle = ViewStyle.defaultViewStyle();
|
_lastViewStyle = ViewStyle.defaultViewStyle();
|
||||||
_timerMobileFocusCanvasCursor?.cancel();
|
_timerMobileFocusCanvasCursor?.cancel();
|
||||||
|
_timerMobileRestoreCanvasOffset?.cancel();
|
||||||
|
_offsetBeforeMobileSoftKeyboard = null;
|
||||||
|
_scaleBeforeMobileSoftKeyboard = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScrollPercent() {
|
updateScrollPercent() {
|
||||||
@@ -2667,6 +2685,31 @@ class CanvasModel with ChangeNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void saveMobileOffsetBeforeSoftKeyboard() {
|
||||||
|
_timerMobileRestoreCanvasOffset?.cancel();
|
||||||
|
_offsetBeforeMobileSoftKeyboard = Offset(_x, _y);
|
||||||
|
_scaleBeforeMobileSoftKeyboard = _scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
void restoreMobileOffsetAfterSoftKeyboard() {
|
||||||
|
_timerMobileRestoreCanvasOffset?.cancel();
|
||||||
|
_timerMobileFocusCanvasCursor?.cancel();
|
||||||
|
final targetOffset = _offsetBeforeMobileSoftKeyboard;
|
||||||
|
final targetScale = _scaleBeforeMobileSoftKeyboard;
|
||||||
|
if (targetOffset == null || targetScale == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_timerMobileRestoreCanvasOffset = Timer(Duration(milliseconds: 100), () {
|
||||||
|
updateSize();
|
||||||
|
_x = targetOffset.dx;
|
||||||
|
_y = targetOffset.dy;
|
||||||
|
_scale = targetScale;
|
||||||
|
_offsetBeforeMobileSoftKeyboard = null;
|
||||||
|
_scaleBeforeMobileSoftKeyboard = null;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// mobile only
|
// mobile only
|
||||||
// Move the canvas to make the cursor visible(center) on the screen.
|
// Move the canvas to make the cursor visible(center) on the screen.
|
||||||
void _moveToCenterCursor() {
|
void _moveToCenterCursor() {
|
||||||
@@ -2919,8 +2962,13 @@ class CursorModel with ChangeNotifier {
|
|||||||
_lastIsBlocked = true;
|
_lastIsBlocked = true;
|
||||||
}
|
}
|
||||||
if (isMobile && _lastKeyboardIsVisible != keyboardIsVisible) {
|
if (isMobile && _lastKeyboardIsVisible != keyboardIsVisible) {
|
||||||
parent.target?.canvasModel.mobileFocusCanvasCursor();
|
if (keyboardIsVisible) {
|
||||||
parent.target?.canvasModel.isMobileCanvasChanged = false;
|
parent.target?.canvasModel.saveMobileOffsetBeforeSoftKeyboard();
|
||||||
|
parent.target?.canvasModel.mobileFocusCanvasCursor();
|
||||||
|
parent.target?.canvasModel.isMobileCanvasChanged = false;
|
||||||
|
} else {
|
||||||
|
parent.target?.canvasModel.restoreMobileOffsetAfterSoftKeyboard();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_lastKeyboardIsVisible = keyboardIsVisible;
|
_lastKeyboardIsVisible = keyboardIsVisible;
|
||||||
}
|
}
|
||||||
@@ -3884,6 +3932,7 @@ class FFI {
|
|||||||
inputModel.resetModifiers();
|
inputModel.resetModifiers();
|
||||||
// Dispose relative mouse mode resources to ensure cursor is restored
|
// Dispose relative mouse mode resources to ensure cursor is restored
|
||||||
inputModel.disposeRelativeMouseMode();
|
inputModel.disposeRelativeMouseMode();
|
||||||
|
inputModel.disposeSideButtonTracking();
|
||||||
if (closeSession) {
|
if (closeSession) {
|
||||||
await bind.sessionClose(sessionId: sessionId);
|
await bind.sessionClose(sessionId: sessionId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleAudio() async {
|
toggleAudio() async {
|
||||||
if (clients.isNotEmpty) {
|
if (clients.any((c) => !c.disconnected)) {
|
||||||
await showClientsMayNotBeChangedAlert(parent.target);
|
await showClientsMayNotBeChangedAlert(parent.target);
|
||||||
}
|
}
|
||||||
if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) {
|
if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) {
|
||||||
@@ -316,7 +316,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleFile() async {
|
toggleFile() async {
|
||||||
if (clients.isNotEmpty) {
|
if (clients.any((c) => !c.disconnected)) {
|
||||||
await showClientsMayNotBeChangedAlert(parent.target);
|
await showClientsMayNotBeChangedAlert(parent.target);
|
||||||
}
|
}
|
||||||
if (!_fileOk &&
|
if (!_fileOk &&
|
||||||
@@ -345,7 +345,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleInput() async {
|
toggleInput() async {
|
||||||
if (clients.isNotEmpty) {
|
if (clients.any((c) => !c.disconnected)) {
|
||||||
await showClientsMayNotBeChangedAlert(parent.target);
|
await showClientsMayNotBeChangedAlert(parent.target);
|
||||||
}
|
}
|
||||||
if (_inputOk) {
|
if (_inputOk) {
|
||||||
@@ -471,17 +471,6 @@ class ServerModel with ChangeNotifier {
|
|||||||
WakelockManager.disable(_wakelockKey);
|
WakelockManager.disable(_wakelockKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> setPermanentPassword(String newPW) async {
|
|
||||||
await bind.mainSetPermanentPassword(password: newPW);
|
|
||||||
await Future.delayed(Duration(milliseconds: 500));
|
|
||||||
final pw = await bind.mainGetPermanentPassword();
|
|
||||||
if (newPW == pw) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchID() async {
|
fetchID() async {
|
||||||
final id = await bind.mainGetMyId();
|
final id = await bind.mainGetMyId();
|
||||||
if (id != _serverId.id) {
|
if (id != _serverId.id) {
|
||||||
@@ -560,10 +549,19 @@ class ServerModel with ChangeNotifier {
|
|||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
_clients.add(client);
|
_clients.add(client);
|
||||||
} else {
|
} else {
|
||||||
|
if (_clients[index].authorized) {
|
||||||
|
_clients[index].privacyMode = client.privacyMode;
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
_clients[index].authorized = true;
|
_clients[index].authorized = true;
|
||||||
|
_clients[index].privacyMode = client.privacyMode;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_clients.any((c) => c.id == client.id)) {
|
final index = _clients.indexWhere((c) => c.id == client.id);
|
||||||
|
if (index >= 0) {
|
||||||
|
_clients[index].privacyMode = client.privacyMode;
|
||||||
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_clients.add(client);
|
_clients.add(client);
|
||||||
@@ -820,6 +818,7 @@ class Client {
|
|||||||
bool isTerminal = false;
|
bool isTerminal = false;
|
||||||
String portForward = "";
|
String portForward = "";
|
||||||
String name = "";
|
String name = "";
|
||||||
|
String avatar = "";
|
||||||
String peerId = ""; // peer user's id,show at app
|
String peerId = ""; // peer user's id,show at app
|
||||||
bool keyboard = false;
|
bool keyboard = false;
|
||||||
bool clipboard = false;
|
bool clipboard = false;
|
||||||
@@ -828,6 +827,7 @@ class Client {
|
|||||||
bool restart = false;
|
bool restart = false;
|
||||||
bool recording = false;
|
bool recording = false;
|
||||||
bool blockInput = false;
|
bool blockInput = false;
|
||||||
|
bool privacyMode = false;
|
||||||
bool disconnected = false;
|
bool disconnected = false;
|
||||||
bool fromSwitch = false;
|
bool fromSwitch = false;
|
||||||
bool inVoiceCall = false;
|
bool inVoiceCall = false;
|
||||||
@@ -847,6 +847,7 @@ class Client {
|
|||||||
isTerminal = json['is_terminal'] ?? false;
|
isTerminal = json['is_terminal'] ?? false;
|
||||||
portForward = json['port_forward'];
|
portForward = json['port_forward'];
|
||||||
name = json['name'];
|
name = json['name'];
|
||||||
|
avatar = json['avatar'] ?? '';
|
||||||
peerId = json['peer_id'];
|
peerId = json['peer_id'];
|
||||||
keyboard = json['keyboard'];
|
keyboard = json['keyboard'];
|
||||||
clipboard = json['clipboard'];
|
clipboard = json['clipboard'];
|
||||||
@@ -855,6 +856,7 @@ class Client {
|
|||||||
restart = json['restart'];
|
restart = json['restart'];
|
||||||
recording = json['recording'];
|
recording = json['recording'];
|
||||||
blockInput = json['block_input'];
|
blockInput = json['block_input'];
|
||||||
|
privacyMode = json['privacy_mode'] ?? privacyMode;
|
||||||
disconnected = json['disconnected'];
|
disconnected = json['disconnected'];
|
||||||
fromSwitch = json['from_switch'];
|
fromSwitch = json['from_switch'];
|
||||||
inVoiceCall = json['in_voice_call'];
|
inVoiceCall = json['in_voice_call'];
|
||||||
@@ -870,6 +872,7 @@ class Client {
|
|||||||
data['is_terminal'] = isTerminal;
|
data['is_terminal'] = isTerminal;
|
||||||
data['port_forward'] = portForward;
|
data['port_forward'] = portForward;
|
||||||
data['name'] = name;
|
data['name'] = name;
|
||||||
|
data['avatar'] = avatar;
|
||||||
data['peer_id'] = peerId;
|
data['peer_id'] = peerId;
|
||||||
data['keyboard'] = keyboard;
|
data['keyboard'] = keyboard;
|
||||||
data['clipboard'] = clipboard;
|
data['clipboard'] = clipboard;
|
||||||
@@ -878,6 +881,7 @@ class Client {
|
|||||||
data['restart'] = restart;
|
data['restart'] = restart;
|
||||||
data['recording'] = recording;
|
data['recording'] = recording;
|
||||||
data['block_input'] = blockInput;
|
data['block_input'] = blockInput;
|
||||||
|
data['privacy_mode'] = privacyMode;
|
||||||
data['disconnected'] = disconnected;
|
data['disconnected'] = disconnected;
|
||||||
data['from_switch'] = fromSwitch;
|
data['from_switch'] = fromSwitch;
|
||||||
data['in_voice_call'] = inVoiceCall;
|
data['in_voice_call'] = inVoiceCall;
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ class TerminalModel with ChangeNotifier {
|
|||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
|
|
||||||
final _inputBuffer = <String>[];
|
final _inputBuffer = <String>[];
|
||||||
|
// Buffer for output data received before terminal view has valid dimensions.
|
||||||
|
// This prevents NaN errors when writing to terminal before layout is complete.
|
||||||
|
final _pendingOutputChunks = <String>[];
|
||||||
|
int _pendingOutputSize = 0;
|
||||||
|
static const int _kMaxOutputBufferChars = 8 * 1024;
|
||||||
|
// View ready state: true when terminal has valid dimensions, safe to write
|
||||||
|
bool _terminalViewReady = false;
|
||||||
|
|
||||||
bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
|
bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
|
||||||
|
|
||||||
@@ -74,6 +81,12 @@ class TerminalModel with ChangeNotifier {
|
|||||||
// This piece of code must be placed before the conditional check in order to initialize properly.
|
// This piece of code must be placed before the conditional check in order to initialize properly.
|
||||||
onResizeExternal?.call(w, h, pw, ph);
|
onResizeExternal?.call(w, h, pw, ph);
|
||||||
|
|
||||||
|
// Mark terminal view as ready and flush any buffered output on first valid resize.
|
||||||
|
// Must be after onResizeExternal so the view layer has valid dimensions before flushing.
|
||||||
|
if (!_terminalViewReady) {
|
||||||
|
_markViewReady();
|
||||||
|
}
|
||||||
|
|
||||||
if (_terminalOpened) {
|
if (_terminalOpened) {
|
||||||
// Notify remote terminal of resize
|
// Notify remote terminal of resize
|
||||||
try {
|
try {
|
||||||
@@ -141,7 +154,7 @@ class TerminalModel with ChangeNotifier {
|
|||||||
debugPrint('[TerminalModel] Error calling sessionOpenTerminal: $e');
|
debugPrint('[TerminalModel] Error calling sessionOpenTerminal: $e');
|
||||||
// Optionally show error to user
|
// Optionally show error to user
|
||||||
if (e is TimeoutException) {
|
if (e is TimeoutException) {
|
||||||
terminal.write('Failed to open terminal: Connection timeout\r\n');
|
_writeToTerminal('Failed to open terminal: Connection timeout\r\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,8 +266,8 @@ class TerminalModel with ChangeNotifier {
|
|||||||
|
|
||||||
void _handleTerminalOpened(Map<String, dynamic> evt) {
|
void _handleTerminalOpened(Map<String, dynamic> evt) {
|
||||||
final bool success = getSuccessFromEvt(evt);
|
final bool success = getSuccessFromEvt(evt);
|
||||||
final String message = evt['message'] ?? '';
|
final String message = evt['message']?.toString() ?? '';
|
||||||
final String? serviceId = evt['service_id'];
|
final String? serviceId = evt['service_id']?.toString();
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[TerminalModel] Terminal opened response: success=$success, message=$message, service_id=$serviceId');
|
'[TerminalModel] Terminal opened response: success=$success, message=$message, service_id=$serviceId');
|
||||||
@@ -262,7 +275,18 @@ class TerminalModel with ChangeNotifier {
|
|||||||
if (success) {
|
if (success) {
|
||||||
_terminalOpened = true;
|
_terminalOpened = true;
|
||||||
|
|
||||||
// Service ID is now saved on the Rust side in handle_terminal_response
|
// On reconnect ("Reconnected to existing terminal"), server may replay recent output.
|
||||||
|
// If this TerminalView instance is reused (not rebuilt), duplicate lines can appear.
|
||||||
|
// We intentionally accept this tradeoff for now to keep logic simple.
|
||||||
|
|
||||||
|
// Fallback: if terminal view is not yet ready but already has valid
|
||||||
|
// dimensions (e.g. layout completed before open response arrived),
|
||||||
|
// mark view ready now to avoid output stuck in buffer indefinitely.
|
||||||
|
if (!_terminalViewReady &&
|
||||||
|
terminal.viewWidth > 0 &&
|
||||||
|
terminal.viewHeight > 0) {
|
||||||
|
_markViewReady();
|
||||||
|
}
|
||||||
|
|
||||||
// Process any buffered input
|
// Process any buffered input
|
||||||
_processBufferedInputAsync().then((_) {
|
_processBufferedInputAsync().then((_) {
|
||||||
@@ -283,7 +307,7 @@ class TerminalModel with ChangeNotifier {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
terminal.write('Failed to open terminal: $message\r\n');
|
_writeToTerminal('Failed to open terminal: $message\r\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,29 +351,82 @@ class TerminalModel with ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
terminal.write(text);
|
_writeToTerminal(text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[TerminalModel] Failed to process terminal data: $e');
|
debugPrint('[TerminalModel] Failed to process terminal data: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write text to terminal, buffering if the view is not yet ready.
|
||||||
|
/// All terminal output should go through this method to avoid NaN errors
|
||||||
|
/// from writing before the terminal view has valid layout dimensions.
|
||||||
|
void _writeToTerminal(String text) {
|
||||||
|
if (!_terminalViewReady) {
|
||||||
|
// If a single chunk exceeds the cap, keep only its tail.
|
||||||
|
// Note: truncation may split a multi-byte ANSI escape sequence,
|
||||||
|
// which can cause a brief visual glitch on flush. This is acceptable
|
||||||
|
// because it only affects the pre-layout buffering window and the
|
||||||
|
// terminal will self-correct on subsequent output.
|
||||||
|
if (text.length >= _kMaxOutputBufferChars) {
|
||||||
|
final truncated = text.substring(text.length - _kMaxOutputBufferChars);
|
||||||
|
_pendingOutputChunks
|
||||||
|
..clear()
|
||||||
|
..add(truncated);
|
||||||
|
_pendingOutputSize = truncated.length;
|
||||||
|
} else {
|
||||||
|
_pendingOutputChunks.add(text);
|
||||||
|
_pendingOutputSize += text.length;
|
||||||
|
// Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences)
|
||||||
|
while (_pendingOutputSize > _kMaxOutputBufferChars &&
|
||||||
|
_pendingOutputChunks.length > 1) {
|
||||||
|
final removed = _pendingOutputChunks.removeAt(0);
|
||||||
|
_pendingOutputSize -= removed.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
terminal.write(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _flushOutputBuffer() {
|
||||||
|
if (_pendingOutputChunks.isEmpty) return;
|
||||||
|
debugPrint(
|
||||||
|
'[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)');
|
||||||
|
for (final chunk in _pendingOutputChunks) {
|
||||||
|
terminal.write(chunk);
|
||||||
|
}
|
||||||
|
_pendingOutputChunks.clear();
|
||||||
|
_pendingOutputSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark terminal view as ready and flush buffered output.
|
||||||
|
void _markViewReady() {
|
||||||
|
if (_terminalViewReady) return;
|
||||||
|
_terminalViewReady = true;
|
||||||
|
_flushOutputBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
void _handleTerminalClosed(Map<String, dynamic> evt) {
|
void _handleTerminalClosed(Map<String, dynamic> evt) {
|
||||||
final int exitCode = evt['exit_code'] ?? 0;
|
final int exitCode = evt['exit_code'] ?? 0;
|
||||||
terminal.write('\r\nTerminal closed with exit code: $exitCode\r\n');
|
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
|
||||||
_terminalOpened = false;
|
_terminalOpened = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleTerminalError(Map<String, dynamic> evt) {
|
void _handleTerminalError(Map<String, dynamic> evt) {
|
||||||
final String message = evt['message'] ?? 'Unknown error';
|
final String message = evt['message'] ?? 'Unknown error';
|
||||||
terminal.write('\r\nTerminal error: $message\r\n');
|
_writeToTerminal('\r\nTerminal error: $message\r\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
// Clear buffers to free memory
|
||||||
|
_inputBuffer.clear();
|
||||||
|
_pendingOutputChunks.clear();
|
||||||
|
_pendingOutputSize = 0;
|
||||||
// Terminal cleanup is handled server-side when service closes
|
// Terminal cleanup is handled server-side when service closes
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,25 @@ bool refreshingUser = false;
|
|||||||
|
|
||||||
class UserModel {
|
class UserModel {
|
||||||
final RxString userName = ''.obs;
|
final RxString userName = ''.obs;
|
||||||
|
final RxString displayName = ''.obs;
|
||||||
|
final RxString avatar = ''.obs;
|
||||||
final RxBool isAdmin = false.obs;
|
final RxBool isAdmin = false.obs;
|
||||||
final RxString networkError = ''.obs;
|
final RxString networkError = ''.obs;
|
||||||
bool get isLogin => userName.isNotEmpty;
|
bool get isLogin => userName.isNotEmpty;
|
||||||
|
String get displayNameOrUserName =>
|
||||||
|
displayName.value.trim().isEmpty ? userName.value : displayName.value;
|
||||||
|
String get accountLabelWithHandle {
|
||||||
|
final username = userName.value.trim();
|
||||||
|
if (username.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
final preferred = displayName.value.trim();
|
||||||
|
if (preferred.isEmpty || preferred == username) {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
return '$preferred (@$username)';
|
||||||
|
}
|
||||||
|
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
UserModel(this.parent) {
|
UserModel(this.parent) {
|
||||||
@@ -98,7 +114,9 @@ class UserModel {
|
|||||||
_updateLocalUserInfo() {
|
_updateLocalUserInfo() {
|
||||||
final userInfo = getLocalUserInfo();
|
final userInfo = getLocalUserInfo();
|
||||||
if (userInfo != null) {
|
if (userInfo != null) {
|
||||||
userName.value = userInfo['name'];
|
userName.value = (userInfo['name'] ?? '').toString();
|
||||||
|
displayName.value = (userInfo['display_name'] ?? '').toString();
|
||||||
|
avatar.value = (userInfo['avatar'] ?? '').toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,10 +128,14 @@ class UserModel {
|
|||||||
await gFFI.groupModel.reset();
|
await gFFI.groupModel.reset();
|
||||||
}
|
}
|
||||||
userName.value = '';
|
userName.value = '';
|
||||||
|
displayName.value = '';
|
||||||
|
avatar.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
_parseAndUpdateUser(UserPayload user) {
|
_parseAndUpdateUser(UserPayload user) {
|
||||||
userName.value = user.name;
|
userName.value = user.name;
|
||||||
|
displayName.value = user.displayName;
|
||||||
|
avatar.value = user.avatar;
|
||||||
isAdmin.value = user.isAdmin;
|
isAdmin.value = user.isAdmin;
|
||||||
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user));
|
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user));
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
|
|||||||
@@ -1159,10 +1159,6 @@ class RustdeskImpl {
|
|||||||
return Future.value('');
|
return Future.value('');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> mainGetPermanentPassword({dynamic hint}) {
|
|
||||||
return Future.value('');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> mainGetFingerprint({dynamic hint}) {
|
Future<String> mainGetFingerprint({dynamic hint}) {
|
||||||
return Future.value('');
|
return Future.value('');
|
||||||
}
|
}
|
||||||
@@ -1346,9 +1342,9 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError("mainUpdateTemporaryPassword");
|
throw UnimplementedError("mainUpdateTemporaryPassword");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> mainSetPermanentPassword(
|
Future<bool> mainSetPermanentPasswordWithResult(
|
||||||
{required String password, dynamic hint}) {
|
{required String password, dynamic hint}) {
|
||||||
throw UnimplementedError("mainSetPermanentPassword");
|
throw UnimplementedError("mainSetPermanentPasswordWithResult");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> mainCheckSuperUserPermission({dynamic hint}) {
|
Future<bool> mainCheckSuperUserPermission({dynamic hint}) {
|
||||||
@@ -1542,7 +1538,10 @@ class RustdeskImpl {
|
|||||||
|
|
||||||
Future<void> mainAccountAuth(
|
Future<void> mainAccountAuth(
|
||||||
{required String op, required bool rememberMe, dynamic hint}) {
|
{required String op, required bool rememberMe, dynamic hint}) {
|
||||||
return Future(() => js.context.callMethod('setByName', [
|
// Safari only allows auth popups while handling the original user gesture.
|
||||||
|
// Use Future.sync so the JS call runs synchronously (pre-opening the OIDC
|
||||||
|
// window) while any interop error still surfaces as a Future error.
|
||||||
|
return Future.sync(() => js.context.callMethod('setByName', [
|
||||||
'account_auth',
|
'account_auth',
|
||||||
jsonEncode({'op': op, 'remember': rememberMe})
|
jsonEncode({'op': op, 'remember': rememberMe})
|
||||||
]));
|
]));
|
||||||
@@ -1730,7 +1729,7 @@ class RustdeskImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String mainSupportedPrivacyModeImpls({dynamic hint}) {
|
String mainSupportedPrivacyModeImpls({dynamic hint}) {
|
||||||
throw UnimplementedError("mainSupportedPrivacyModeImpls");
|
return '[]';
|
||||||
}
|
}
|
||||||
|
|
||||||
String mainSupportedInputSource({dynamic hint}) {
|
String mainSupportedInputSource({dynamic hint}) {
|
||||||
@@ -2034,5 +2033,9 @@ class RustdeskImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String mainResolveAvatarUrl({required String avatar, dynamic hint}) {
|
||||||
|
return js.context.callMethod('getByName', ['resolve_avatar_url', avatar])?.toString() ?? avatar;
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,80 @@ void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view);
|
|||||||
|
|
||||||
extern bool gIsConnectionManager;
|
extern bool gIsConnectionManager;
|
||||||
|
|
||||||
|
// --- Side mouse button support (back/forward) ---
|
||||||
|
// Flutter's Linux embedder doesn't deliver X11 button 8/9 events to Dart.
|
||||||
|
// We intercept them via GDK and forward through a dedicated platform channel.
|
||||||
|
|
||||||
|
static const char* kSideButtonChannelName = "org.rustdesk.rustdesk/side_buttons";
|
||||||
|
|
||||||
|
static gboolean on_side_button_event(GtkWidget* widget, GdkEventButton* event, gpointer user_data) {
|
||||||
|
if (event->button != 8 && event->button != 9) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
// Ignore GDK_2BUTTON_PRESS / GDK_3BUTTON_PRESS (double/triple-click synthetic
|
||||||
|
// events) - only handle real press and release.
|
||||||
|
if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlMethodChannel* channel = FL_METHOD_CHANNEL(user_data);
|
||||||
|
if (channel == NULL) return FALSE;
|
||||||
|
|
||||||
|
g_autoptr(FlValue) args = fl_value_new_map();
|
||||||
|
fl_value_set_string_take(args, "button",
|
||||||
|
fl_value_new_string(event->button == 8 ? "back" : "forward"));
|
||||||
|
fl_value_set_string_take(args, "type",
|
||||||
|
fl_value_new_string(event->type == GDK_BUTTON_PRESS ? "down" : "up"));
|
||||||
|
|
||||||
|
fl_method_channel_invoke_method(channel, "onSideMouseButton", args,
|
||||||
|
NULL, NULL, NULL);
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FlMethodChannel* side_buttons_create_channel(FlEngine* engine) {
|
||||||
|
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
|
||||||
|
return fl_method_channel_new(
|
||||||
|
fl_engine_get_binary_messenger(engine),
|
||||||
|
kSideButtonChannelName,
|
||||||
|
FL_METHOD_CODEC(codec));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void side_buttons_channel_destroy(gpointer data) {
|
||||||
|
g_object_unref(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void side_buttons_init_for_window(GtkWindow* window, FlMethodChannel* channel) {
|
||||||
|
// Guard against double-initialization (would leave dangling signal user_data).
|
||||||
|
if (g_object_get_data(G_OBJECT(window), "side-buttons-channel") != NULL) return;
|
||||||
|
|
||||||
|
gtk_widget_add_events(GTK_WIDGET(window),
|
||||||
|
GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
|
||||||
|
// Store channel on the window so it stays alive and is freed with the window.
|
||||||
|
g_object_set_data_full(G_OBJECT(window), "side-buttons-channel",
|
||||||
|
g_object_ref(channel), side_buttons_channel_destroy);
|
||||||
|
g_signal_connect(window, "button-press-event",
|
||||||
|
G_CALLBACK(on_side_button_event), channel);
|
||||||
|
g_signal_connect(window, "button-release-event",
|
||||||
|
G_CALLBACK(on_side_button_event), channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_subwindow_created(FlPluginRegistry* registry) {
|
||||||
|
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
|
||||||
|
wayland_shortcuts_inhibit_init_for_subwindow(registry);
|
||||||
|
#endif
|
||||||
|
// Set up side button forwarding for sub-windows.
|
||||||
|
if (registry == NULL || !FL_IS_VIEW(registry)) return;
|
||||||
|
FlView* view = FL_VIEW(registry);
|
||||||
|
GtkWidget* toplevel = gtk_widget_get_toplevel(GTK_WIDGET(view));
|
||||||
|
if (toplevel != NULL && GTK_IS_WINDOW(toplevel)) {
|
||||||
|
FlMethodChannel* channel = side_buttons_create_channel(fl_view_get_engine(view));
|
||||||
|
if (channel == NULL) return;
|
||||||
|
side_buttons_init_for_window(GTK_WINDOW(toplevel), channel);
|
||||||
|
g_object_unref(channel); // window now owns a ref via g_object_set_data_full
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
GtkWidget *find_gl_area(GtkWidget *widget);
|
GtkWidget *find_gl_area(GtkWidget *widget);
|
||||||
|
|
||||||
// Implements GApplication::activate.
|
// Implements GApplication::activate.
|
||||||
@@ -96,12 +170,12 @@ static void my_application_activate(GApplication* application) {
|
|||||||
gtk_widget_show(GTK_WIDGET(window));
|
gtk_widget_show(GTK_WIDGET(window));
|
||||||
gtk_widget_show(GTK_WIDGET(view));
|
gtk_widget_show(GTK_WIDGET(view));
|
||||||
|
|
||||||
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
|
// Register callback for sub-windows created by desktop_multi_window plugin.
|
||||||
// Register callback for sub-windows created by desktop_multi_window plugin
|
// Handles both Wayland shortcuts inhibition (guarded inside) and side button
|
||||||
// Only sub-windows (remote windows) need keyboard shortcuts inhibition
|
// forwarding. Safe to call on X11-only builds - the plugin just stores the
|
||||||
|
// callback pointer regardless of windowing system.
|
||||||
desktop_multi_window_plugin_set_window_created_callback(
|
desktop_multi_window_plugin_set_window_created_callback(
|
||||||
(WindowCreatedCallback)wayland_shortcuts_inhibit_init_for_subwindow);
|
(WindowCreatedCallback)on_subwindow_created);
|
||||||
#endif
|
|
||||||
|
|
||||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||||
|
|
||||||
@@ -116,6 +190,11 @@ static void my_application_activate(GApplication* application) {
|
|||||||
self,
|
self,
|
||||||
nullptr);
|
nullptr);
|
||||||
|
|
||||||
|
// Forward side mouse button events (back/forward) to Dart on the main window.
|
||||||
|
FlMethodChannel* side_channel = side_buttons_create_channel(fl_view_get_engine(view));
|
||||||
|
side_buttons_init_for_window(window, side_channel);
|
||||||
|
g_object_unref(side_channel);
|
||||||
|
|
||||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||||
version: 1.4.5+63
|
version: 1.4.6+64
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
sdk: '^3.1.0'
|
||||||
@@ -113,8 +113,8 @@ dependencies:
|
|||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
icons_launcher: ^2.0.4
|
icons_launcher: ^2.0.4
|
||||||
#flutter_test:
|
flutter_test:
|
||||||
#sdk: flutter
|
sdk: flutter
|
||||||
build_runner: ^2.4.6
|
build_runner: ^2.4.6
|
||||||
freezed: ^2.4.2
|
freezed: ^2.4.2
|
||||||
flutter_lints: ^2.0.2
|
flutter_lints: ^2.0.2
|
||||||
|
|||||||
125
flutter/test/input_modifier_utils_test.dart
Normal file
125
flutter/test/input_modifier_utils_test.dart
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_hbb/models/input_modifier_utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('shouldReleaseStaleMobileShift', () {
|
||||||
|
test('does not release when cached shift is already false', () {
|
||||||
|
expect(
|
||||||
|
shouldReleaseStaleMobileShift(
|
||||||
|
isMobile: true,
|
||||||
|
cachedShiftPressed: false,
|
||||||
|
actualShiftPressed: false,
|
||||||
|
logicalKey: LogicalKeyboardKey.keyD,
|
||||||
|
hasTrackedShiftKeyDown: true,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('releases one-shot mobile shift after a text key', () {
|
||||||
|
expect(
|
||||||
|
shouldReleaseStaleMobileShift(
|
||||||
|
isMobile: true,
|
||||||
|
cachedShiftPressed: true,
|
||||||
|
actualShiftPressed: false,
|
||||||
|
logicalKey: LogicalKeyboardKey.keyD,
|
||||||
|
hasTrackedShiftKeyDown: true,
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not release manually toggled shift without tracked key down',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
shouldReleaseStaleMobileShift(
|
||||||
|
isMobile: true,
|
||||||
|
cachedShiftPressed: true,
|
||||||
|
actualShiftPressed: false,
|
||||||
|
logicalKey: LogicalKeyboardKey.keyD,
|
||||||
|
hasTrackedShiftKeyDown: false,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not release when shift is still physically pressed', () {
|
||||||
|
expect(
|
||||||
|
shouldReleaseStaleMobileShift(
|
||||||
|
isMobile: true,
|
||||||
|
cachedShiftPressed: true,
|
||||||
|
actualShiftPressed: true,
|
||||||
|
logicalKey: LogicalKeyboardKey.keyD,
|
||||||
|
hasTrackedShiftKeyDown: true,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not release on non-mobile platforms', () {
|
||||||
|
expect(
|
||||||
|
shouldReleaseStaleMobileShift(
|
||||||
|
isMobile: false,
|
||||||
|
cachedShiftPressed: true,
|
||||||
|
actualShiftPressed: false,
|
||||||
|
logicalKey: LogicalKeyboardKey.keyD,
|
||||||
|
hasTrackedShiftKeyDown: true,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('releases on enter key', () {
|
||||||
|
expect(
|
||||||
|
shouldReleaseStaleMobileShift(
|
||||||
|
isMobile: true,
|
||||||
|
cachedShiftPressed: true,
|
||||||
|
actualShiftPressed: false,
|
||||||
|
logicalKey: LogicalKeyboardKey.enter,
|
||||||
|
hasTrackedShiftKeyDown: true,
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('releases on arrow key', () {
|
||||||
|
expect(
|
||||||
|
shouldReleaseStaleMobileShift(
|
||||||
|
isMobile: true,
|
||||||
|
cachedShiftPressed: true,
|
||||||
|
actualShiftPressed: false,
|
||||||
|
logicalKey: LogicalKeyboardKey.arrowLeft,
|
||||||
|
hasTrackedShiftKeyDown: true,
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not release on modifier events', () {
|
||||||
|
expect(
|
||||||
|
shouldReleaseStaleMobileShift(
|
||||||
|
isMobile: true,
|
||||||
|
cachedShiftPressed: true,
|
||||||
|
actualShiftPressed: false,
|
||||||
|
logicalKey: LogicalKeyboardKey.shiftLeft,
|
||||||
|
hasTrackedShiftKeyDown: true,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not release on shiftRight modifier events', () {
|
||||||
|
expect(
|
||||||
|
shouldReleaseStaleMobileShift(
|
||||||
|
isMobile: true,
|
||||||
|
cachedShiftPressed: true,
|
||||||
|
actualShiftPressed: false,
|
||||||
|
logicalKey: LogicalKeyboardKey.shiftRight,
|
||||||
|
hasTrackedShiftKeyDown: true,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <cstdlib> // for getenv and _putenv
|
#include <cstdlib> // for getenv and _putenv
|
||||||
#include <cstring> // for strcmp
|
#include <cstring> // for strcmp
|
||||||
|
#include <string> // for std::wstring
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -15,6 +16,43 @@ constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
|||||||
// The number of Win32Window objects that currently exist.
|
// The number of Win32Window objects that currently exist.
|
||||||
static int g_active_window_count = 0;
|
static int g_active_window_count = 0;
|
||||||
|
|
||||||
|
// Static variable to hold the custom icon (needs cleanup on exit)
|
||||||
|
static HICON g_custom_icon_ = nullptr;
|
||||||
|
|
||||||
|
// Try to load icon from data\flutter_assets\assets\icon.ico if it exists.
|
||||||
|
// Returns nullptr if the file doesn't exist or can't be loaded.
|
||||||
|
HICON LoadCustomIcon() {
|
||||||
|
if (g_custom_icon_ != nullptr) {
|
||||||
|
return g_custom_icon_;
|
||||||
|
}
|
||||||
|
wchar_t exe_path[MAX_PATH];
|
||||||
|
if (!GetModuleFileNameW(nullptr, exe_path, MAX_PATH)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring icon_path = exe_path;
|
||||||
|
size_t last_slash = icon_path.find_last_of(L"\\/");
|
||||||
|
if (last_slash == std::wstring::npos) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
icon_path = icon_path.substr(0, last_slash + 1);
|
||||||
|
icon_path += L"data\\flutter_assets\\assets\\icon.ico";
|
||||||
|
|
||||||
|
// Check file attributes - reject if missing, directory, or reparse point (symlink/junction)
|
||||||
|
DWORD file_attr = GetFileAttributesW(icon_path.c_str());
|
||||||
|
if (file_attr == INVALID_FILE_ATTRIBUTES ||
|
||||||
|
(file_attr & FILE_ATTRIBUTE_DIRECTORY) ||
|
||||||
|
(file_attr & FILE_ATTRIBUTE_REPARSE_POINT)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_custom_icon_ = (HICON)LoadImageW(
|
||||||
|
nullptr, icon_path.c_str(), IMAGE_ICON, 0, 0,
|
||||||
|
LR_LOADFROMFILE | LR_DEFAULTSIZE);
|
||||||
|
return g_custom_icon_;
|
||||||
|
}
|
||||||
|
|
||||||
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||||
|
|
||||||
// Scale helper to convert logical scaler values to physical using passed in
|
// Scale helper to convert logical scaler values to physical using passed in
|
||||||
@@ -81,8 +119,16 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
|||||||
window_class.cbClsExtra = 0;
|
window_class.cbClsExtra = 0;
|
||||||
window_class.cbWndExtra = 0;
|
window_class.cbWndExtra = 0;
|
||||||
window_class.hInstance = GetModuleHandle(nullptr);
|
window_class.hInstance = GetModuleHandle(nullptr);
|
||||||
window_class.hIcon =
|
|
||||||
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
// Try to load icon from data\flutter_assets\assets\icon.ico if it exists
|
||||||
|
HICON custom_icon = LoadCustomIcon();
|
||||||
|
if (custom_icon != nullptr) {
|
||||||
|
window_class.hIcon = custom_icon;
|
||||||
|
} else {
|
||||||
|
window_class.hIcon =
|
||||||
|
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||||
|
}
|
||||||
|
|
||||||
window_class.hbrBackground = 0;
|
window_class.hbrBackground = 0;
|
||||||
window_class.lpszMenuName = nullptr;
|
window_class.lpszMenuName = nullptr;
|
||||||
window_class.lpfnWndProc = Win32Window::WndProc;
|
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||||
@@ -95,6 +141,12 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
|||||||
void WindowClassRegistrar::UnregisterWindowClass() {
|
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||||
UnregisterClass(kWindowClassName, nullptr);
|
UnregisterClass(kWindowClassName, nullptr);
|
||||||
class_registered_ = false;
|
class_registered_ = false;
|
||||||
|
|
||||||
|
// Clean up the custom icon if it was loaded
|
||||||
|
if (g_custom_icon_ != nullptr) {
|
||||||
|
DestroyIcon(g_custom_icon_);
|
||||||
|
g_custom_icon_ = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Win32Window::Win32Window() {
|
Win32Window::Win32Window() {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
//!
|
//!
|
||||||
//! For now, we transfer all file names with windows separators, UTF-16 encoded.
|
//! For now, we transfer all file names with windows separators, UTF-16 encoded.
|
||||||
//! *Need a way to transfer file names with '\' safely*.
|
//! *Need a way to transfer file names with '\' safely*.
|
||||||
//! Maybe we can use URL encoded file names and '/' seperators as a new standard, while keep the support to old schemes.
|
//! Maybe we can use URL encoded file names and '/' separators as a new standard, while keep the support to old schemes.
|
||||||
//!
|
//!
|
||||||
//! # Note
|
//! # Note
|
||||||
//! - all files on FS should be read only, and mark the owner to be the current user
|
//! - all files on FS should be read only, and mark the owner to be the current user
|
||||||
|
|||||||
@@ -624,6 +624,7 @@ void CliprdrStream_Delete(CliprdrStream *instance)
|
|||||||
if (instance)
|
if (instance)
|
||||||
{
|
{
|
||||||
free(instance->iStream.lpVtbl);
|
free(instance->iStream.lpVtbl);
|
||||||
|
instance->iStream.lpVtbl = NULL;
|
||||||
free(instance);
|
free(instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2160,7 +2161,7 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi
|
|||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
/* add to name array */
|
/* add to name array */
|
||||||
clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc(MAX_PATH * 2);
|
clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc((size_t)MAX_PATH * sizeof(WCHAR));
|
||||||
|
|
||||||
if (!clipboard->file_names[clipboard->nFiles])
|
if (!clipboard->file_names[clipboard->nFiles])
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
|
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
|
||||||
|
|
||||||
use hbb_common::libc::c_int;
|
use hbb_common::libc::c_int;
|
||||||
|
use hbb_common::x11::xlib::{Display, XCloseDisplay, XGetPointerMapping, XOpenDisplay};
|
||||||
use libxdo_sys::{self, xdo_t, CURRENTWINDOW};
|
use libxdo_sys::{self, xdo_t, CURRENTWINDOW};
|
||||||
use std::{borrow::Cow, ffi::CString};
|
use std::{borrow::Cow, ffi::CString};
|
||||||
|
|
||||||
@@ -32,6 +33,51 @@ fn mousebutton(button: MouseButton) -> c_int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Minimum number of buttons the X11 core pointer must support.
|
||||||
|
/// Buttons 8 (Back) and 9 (Forward) are needed for mouse side buttons.
|
||||||
|
const MIN_POINTER_BUTTONS: usize = 9;
|
||||||
|
|
||||||
|
/// Check that the X11 core pointer's button map includes at least 9 buttons
|
||||||
|
/// so that `XTestFakeButtonEvent` can simulate Back (8) and Forward (9).
|
||||||
|
///
|
||||||
|
/// RustDesk's uinput "Mouse passthrough" device normally provides enough
|
||||||
|
/// buttons, but we log a warning if the map is too small so the issue is
|
||||||
|
/// diagnosable. `XSetPointerMapping` cannot extend the button count (its
|
||||||
|
/// length must match `XGetPointerMapping`), so we only diagnose here.
|
||||||
|
fn check_x11_button_map() {
|
||||||
|
// Skip on non-X11 sessions to avoid noisy "XOpenDisplay failed" warnings
|
||||||
|
// on pure Wayland or headless environments without $DISPLAY.
|
||||||
|
if std::env::var_os("DISPLAY").is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let display: *mut Display = unsafe { XOpenDisplay(std::ptr::null()) };
|
||||||
|
if display.is_null() {
|
||||||
|
log::warn!("XOpenDisplay failed, cannot check button map");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut current_map = [0u8; 32];
|
||||||
|
let nbuttons =
|
||||||
|
unsafe { XGetPointerMapping(display, current_map.as_mut_ptr(), current_map.len() as i32) };
|
||||||
|
unsafe { XCloseDisplay(display) };
|
||||||
|
|
||||||
|
if nbuttons < 0 {
|
||||||
|
log::warn!("XGetPointerMapping failed (returned {nbuttons})");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nbuttons = nbuttons as usize;
|
||||||
|
if nbuttons >= MIN_POINTER_BUTTONS {
|
||||||
|
log::info!("X11 pointer has {nbuttons} buttons, side buttons supported");
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"X11 pointer has only {nbuttons} buttons (need {MIN_POINTER_BUTTONS}); \
|
||||||
|
back/forward side buttons may not work until a device with more buttons is added"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The main struct for handling the event emitting
|
/// The main struct for handling the event emitting
|
||||||
pub(super) struct EnigoXdo {
|
pub(super) struct EnigoXdo {
|
||||||
xdo: *mut xdo_t,
|
xdo: *mut xdo_t,
|
||||||
@@ -52,6 +98,7 @@ impl Default for EnigoXdo {
|
|||||||
log::warn!("Failed to create xdo context, xdo functions will be disabled");
|
log::warn!("Failed to create xdo context, xdo functions will be disabled");
|
||||||
} else {
|
} else {
|
||||||
log::info!("xdo context created successfully");
|
log::info!("xdo context created successfully");
|
||||||
|
check_x11_button_map();
|
||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
xdo,
|
xdo,
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ impl KeyboardControllable for Enigo {
|
|||||||
for pos in 0..mod_len {
|
for pos in 0..mod_len {
|
||||||
let rpos = mod_len - 1 - pos;
|
let rpos = mod_len - 1 - pos;
|
||||||
if flag & (0x0001 << rpos) != 0 {
|
if flag & (0x0001 << rpos) != 0 {
|
||||||
self.key_up(modifiers[pos]);
|
self.key_up(modifiers[rpos]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +298,18 @@ impl KeyboardControllable for Enigo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn key_up(&mut self, key: Key) {
|
fn key_up(&mut self, key: Key) {
|
||||||
keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0);
|
match key {
|
||||||
|
Key::Layout(c) => {
|
||||||
|
let code = self.get_layoutdependent_keycode(c);
|
||||||
|
if code as u16 != 0xFFFF {
|
||||||
|
let vk = code & 0x00FF;
|
||||||
|
keybd_event(KEYEVENTF_KEYUP, vk, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_key_state(&mut self, key: Key) -> bool {
|
fn get_key_state(&mut self, key: Key) -> bool {
|
||||||
|
|||||||
Submodule libs/hbb_common updated: da339dca64...6490a8655c
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.4.5"
|
version = "1.4.6"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "RustDesk Remote Desktop"
|
description = "RustDesk Remote Desktop"
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ fn create_media_codec(name: &str, direction: MediaCodecDirection) -> Option<Medi
|
|||||||
log::error!("Failed to start decoder: {:?}", e);
|
log::error!("Failed to start decoder: {:?}", e);
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
log::debug!("Init decoder successed!: {:?}", name);
|
log::debug!("Init decoder succeeded!: {:?}", name);
|
||||||
return Some(MediaCodecDecoder {
|
return Some(MediaCodecDecoder {
|
||||||
decoder: codec,
|
decoder: codec,
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ impl<'a> PixelProvider<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait Recorder {
|
pub trait Recorder {
|
||||||
fn capture(&mut self, timeout_ms: u64) -> Result<PixelProvider, Box<dyn Error>>;
|
fn capture(&mut self, timeout_ms: u64) -> Result<PixelProvider<'_>, Box<dyn Error>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait BoxCloneCapturable {
|
pub trait BoxCloneCapturable {
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ impl PipeWireRecorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Recorder for PipeWireRecorder {
|
impl Recorder for PipeWireRecorder {
|
||||||
fn capture(&mut self, timeout_ms: u64) -> Result<PixelProvider, Box<dyn Error>> {
|
fn capture(&mut self, timeout_ms: u64) -> Result<PixelProvider<'_>, Box<dyn Error>> {
|
||||||
if let Some(sample) = self
|
if let Some(sample) = self
|
||||||
.appsink
|
.appsink
|
||||||
.try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms))
|
.try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms))
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ unsafe fn check_x11_shm_available(c: *mut xcb_connection_t) -> Result<(), Error>
|
|||||||
let mut e: *mut xcb_generic_error_t = std::ptr::null_mut();
|
let mut e: *mut xcb_generic_error_t = std::ptr::null_mut();
|
||||||
let reply = xcb_shm_query_version_reply(c, cookie, &mut e as _);
|
let reply = xcb_shm_query_version_reply(c, cookie, &mut e as _);
|
||||||
if reply.is_null() {
|
if reply.is_null() {
|
||||||
// TODO: Should seperate SHM disabled from SHM not supported?
|
// TODO: Should separate SHM disabled from SHM not supported?
|
||||||
return Err(Error::UnsupportedExtension);
|
return Err(Error::UnsupportedExtension);
|
||||||
} else {
|
} else {
|
||||||
// https://github.com/FFmpeg/FFmpeg/blob/6229e4ac425b4566446edefb67d5c225eb397b58/libavdevice/xcbgrab.c#L229
|
// https://github.com/FFmpeg/FFmpeg/blob/6229e4ac425b4566446edefb67d5c225eb397b58/libavdevice/xcbgrab.c#L229
|
||||||
|
|||||||
@@ -29,4 +29,4 @@ TODO
|
|||||||
|
|
||||||
## X11
|
## X11
|
||||||
|
|
||||||
## OSX
|
## macOS
|
||||||
|
|||||||
@@ -6,15 +6,13 @@ if [ "$1" = configure ]; then
|
|||||||
|
|
||||||
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
|
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
|
||||||
ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk
|
ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk
|
||||||
|
|
||||||
if [ "systemd" == "$INITSYS" ]; then
|
if [ "systemd" == "$INITSYS" ]; then
|
||||||
|
|
||||||
if [ -e /etc/systemd/system/rustdesk.service ]; then
|
if [ -e /etc/systemd/system/rustdesk.service ]; then
|
||||||
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
|
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)')
|
mkdir -p /usr/lib/systemd/system/
|
||||||
parsedVersion=$(echo "${version//./}")
|
|
||||||
mkdir -p /usr/lib/systemd/system/
|
|
||||||
cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service
|
cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service
|
||||||
# try fix error in Ubuntu 18.04
|
# try fix error in Ubuntu 18.04
|
||||||
# Failed to reload rustdesk.service: Unit rustdesk.service is not loaded properly: Exec format error.
|
# Failed to reload rustdesk.service: Unit rustdesk.service is not loaded properly: Exec format error.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=rustdesk
|
pkgname=rustdesk
|
||||||
pkgver=1.4.5
|
pkgver=1.4.6
|
||||||
pkgrel=0
|
pkgrel=0
|
||||||
epoch=
|
epoch=
|
||||||
pkgdesc=""
|
pkgdesc=""
|
||||||
|
|||||||
82
res/audits.py
Normal file → Executable file
82
res/audits.py
Normal file → Executable file
@@ -43,7 +43,7 @@ def get_connection_type_name(conn_type):
|
|||||||
"""Convert connection type number to readable name"""
|
"""Convert connection type number to readable name"""
|
||||||
type_map = {
|
type_map = {
|
||||||
0: "Remote Desktop",
|
0: "Remote Desktop",
|
||||||
1: "File Transfer",
|
1: "File Transfer",
|
||||||
2: "Port Transfer",
|
2: "Port Transfer",
|
||||||
3: "View Camera",
|
3: "View Camera",
|
||||||
4: "Terminal"
|
4: "Terminal"
|
||||||
@@ -55,7 +55,7 @@ def get_console_type_name(console_type):
|
|||||||
"""Convert console audit type number to readable name"""
|
"""Convert console audit type number to readable name"""
|
||||||
type_map = {
|
type_map = {
|
||||||
0: "Group Management",
|
0: "Group Management",
|
||||||
1: "User Management",
|
1: "User Management",
|
||||||
2: "Device Management",
|
2: "Device Management",
|
||||||
3: "Address Book Management"
|
3: "Address Book Management"
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ def get_console_operation_name(operation_code):
|
|||||||
operation_map = {
|
operation_map = {
|
||||||
0: "User Login",
|
0: "User Login",
|
||||||
1: "Add Group",
|
1: "Add Group",
|
||||||
2: "Add User",
|
2: "Add User",
|
||||||
3: "Add Device",
|
3: "Add Device",
|
||||||
4: "Delete Groups",
|
4: "Delete Groups",
|
||||||
5: "Disconnect Device",
|
5: "Disconnect Device",
|
||||||
@@ -95,7 +95,7 @@ def get_console_operation_name(operation_code):
|
|||||||
def get_alarm_type_name(alarm_type):
|
def get_alarm_type_name(alarm_type):
|
||||||
"""Convert alarm type number to readable name"""
|
"""Convert alarm type number to readable name"""
|
||||||
type_map = {
|
type_map = {
|
||||||
0: "Access attempt outside the IP whiltelist",
|
0: "Access attempt outside the IP whitelist",
|
||||||
1: "Over 30 consecutive access attempts",
|
1: "Over 30 consecutive access attempts",
|
||||||
2: "Multiple access attempts within one minute",
|
2: "Multiple access attempts within one minute",
|
||||||
3: "Over 30 consecutive login attempts",
|
3: "Over 30 consecutive login attempts",
|
||||||
@@ -109,24 +109,24 @@ def enhance_audit_data(data, audit_type):
|
|||||||
"""Enhance audit data with readable formats"""
|
"""Enhance audit data with readable formats"""
|
||||||
if not data:
|
if not data:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
enhanced_data = []
|
enhanced_data = []
|
||||||
for item in data:
|
for item in data:
|
||||||
enhanced_item = item.copy()
|
enhanced_item = item.copy()
|
||||||
|
|
||||||
# Convert timestamps - replace original values
|
# Convert timestamps - replace original values
|
||||||
if 'created_at' in enhanced_item:
|
if 'created_at' in enhanced_item:
|
||||||
enhanced_item['created_at'] = format_timestamp(enhanced_item['created_at'])
|
enhanced_item['created_at'] = format_timestamp(enhanced_item['created_at'])
|
||||||
if 'end_time' in enhanced_item:
|
if 'end_time' in enhanced_item:
|
||||||
enhanced_item['end_time'] = format_timestamp(enhanced_item['end_time'])
|
enhanced_item['end_time'] = format_timestamp(enhanced_item['end_time'])
|
||||||
|
|
||||||
# Add type-specific enhancements - replace original values
|
# Add type-specific enhancements - replace original values
|
||||||
if audit_type == 'conn':
|
if audit_type == 'conn':
|
||||||
if 'conn_type' in enhanced_item:
|
if 'conn_type' in enhanced_item:
|
||||||
enhanced_item['conn_type'] = get_connection_type_name(enhanced_item['conn_type'])
|
enhanced_item['conn_type'] = get_connection_type_name(enhanced_item['conn_type'])
|
||||||
else:
|
else:
|
||||||
enhanced_item['conn_type'] = "Not Logged In"
|
enhanced_item['conn_type'] = "Not Logged In"
|
||||||
|
|
||||||
elif audit_type == 'console':
|
elif audit_type == 'console':
|
||||||
if 'typ' in enhanced_item:
|
if 'typ' in enhanced_item:
|
||||||
# Replace typ field with type and convert to readable name
|
# Replace typ field with type and convert to readable name
|
||||||
@@ -136,14 +136,14 @@ def enhance_audit_data(data, audit_type):
|
|||||||
# Replace iop field with operation and convert to readable name
|
# Replace iop field with operation and convert to readable name
|
||||||
enhanced_item['operation'] = get_console_operation_name(enhanced_item['iop'])
|
enhanced_item['operation'] = get_console_operation_name(enhanced_item['iop'])
|
||||||
del enhanced_item['iop']
|
del enhanced_item['iop']
|
||||||
|
|
||||||
elif audit_type == 'alarm' and 'typ' in enhanced_item:
|
elif audit_type == 'alarm' and 'typ' in enhanced_item:
|
||||||
# Replace typ field with type and convert to readable name
|
# Replace typ field with type and convert to readable name
|
||||||
enhanced_item['type'] = get_alarm_type_name(enhanced_item['typ'])
|
enhanced_item['type'] = get_alarm_type_name(enhanced_item['typ'])
|
||||||
del enhanced_item['typ']
|
del enhanced_item['typ']
|
||||||
|
|
||||||
enhanced_data.append(enhanced_item)
|
enhanced_data.append(enhanced_item)
|
||||||
|
|
||||||
return enhanced_data
|
return enhanced_data
|
||||||
|
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ def check_response(response):
|
|||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print(f"Error: HTTP {response.status_code} - {response.text}")
|
print(f"Error: HTTP {response.status_code} - {response.text}")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response_json = response.json()
|
response_json = response.json()
|
||||||
if "error" in response_json:
|
if "error" in response_json:
|
||||||
@@ -163,28 +163,28 @@ def check_response(response):
|
|||||||
return response.text or "Success"
|
return response.text or "Success"
|
||||||
|
|
||||||
|
|
||||||
def view_audits_common(url, token, endpoint, filters=None, page_size=None, current=None,
|
def view_audits_common(url, token, endpoint, filters=None, page_size=None, current=None,
|
||||||
created_at=None, days_ago=None, non_wildcard_fields=None):
|
created_at=None, days_ago=None, non_wildcard_fields=None):
|
||||||
"""Common function for viewing audits"""
|
"""Common function for viewing audits"""
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
# Set default page size and current page
|
# Set default page size and current page
|
||||||
if page_size is None:
|
if page_size is None:
|
||||||
page_size = 10
|
page_size = 10
|
||||||
if current is None:
|
if current is None:
|
||||||
current = 1
|
current = 1
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"pageSize": page_size,
|
"pageSize": page_size,
|
||||||
"current": current
|
"current": current
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add filter parameters if provided
|
# Add filter parameters if provided
|
||||||
if filters:
|
if filters:
|
||||||
for key, value in filters.items():
|
for key, value in filters.items():
|
||||||
if value is not None:
|
if value is not None:
|
||||||
params[key] = value
|
params[key] = value
|
||||||
|
|
||||||
# Handle time filters
|
# Handle time filters
|
||||||
if days_ago is not None:
|
if days_ago is not None:
|
||||||
# Calculate datetime from days ago
|
# Calculate datetime from days ago
|
||||||
@@ -205,10 +205,10 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre
|
|||||||
# Apply wildcard patterns for string fields (excluding specific fields)
|
# Apply wildcard patterns for string fields (excluding specific fields)
|
||||||
if non_wildcard_fields is None:
|
if non_wildcard_fields is None:
|
||||||
non_wildcard_fields = set()
|
non_wildcard_fields = set()
|
||||||
|
|
||||||
# Always exclude these fields from wildcard treatment
|
# Always exclude these fields from wildcard treatment
|
||||||
non_wildcard_fields.update(["created_at", "pageSize", "current"])
|
non_wildcard_fields.update(["created_at", "pageSize", "current"])
|
||||||
|
|
||||||
string_params = {}
|
string_params = {}
|
||||||
for k, v in params.items():
|
for k, v in params.items():
|
||||||
if isinstance(v, str) and k not in non_wildcard_fields:
|
if isinstance(v, str) and k not in non_wildcard_fields:
|
||||||
@@ -221,10 +221,10 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre
|
|||||||
|
|
||||||
response = requests.get(f"{url}/api/audits/{endpoint}", headers=headers, params=string_params)
|
response = requests.get(f"{url}/api/audits/{endpoint}", headers=headers, params=string_params)
|
||||||
response_json = check_response(response)
|
response_json = check_response(response)
|
||||||
|
|
||||||
# Enhance the data with readable formats
|
# Enhance the data with readable formats
|
||||||
data = enhance_audit_data(response_json.get("data", []), endpoint)
|
data = enhance_audit_data(response_json.get("data", []), endpoint)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"data": data,
|
"data": data,
|
||||||
"total": response_json.get("total", 0),
|
"total": response_json.get("total", 0),
|
||||||
@@ -233,7 +233,7 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def view_conn_audits(url, token, remote=None, conn_type=None,
|
def view_conn_audits(url, token, remote=None, conn_type=None,
|
||||||
page_size=None, current=None, created_at=None, days_ago=None):
|
page_size=None, current=None, created_at=None, days_ago=None):
|
||||||
"""View connection audits"""
|
"""View connection audits"""
|
||||||
filters = {
|
filters = {
|
||||||
@@ -241,7 +241,7 @@ def view_conn_audits(url, token, remote=None, conn_type=None,
|
|||||||
"conn_type": conn_type
|
"conn_type": conn_type
|
||||||
}
|
}
|
||||||
non_wildcard_fields = {"conn_type"}
|
non_wildcard_fields = {"conn_type"}
|
||||||
|
|
||||||
return view_audits_common(
|
return view_audits_common(
|
||||||
url, token, "conn", filters, page_size, current, created_at, days_ago, non_wildcard_fields
|
url, token, "conn", filters, page_size, current, created_at, days_ago, non_wildcard_fields
|
||||||
)
|
)
|
||||||
@@ -254,7 +254,7 @@ def view_file_audits(url, token, remote=None,
|
|||||||
"remote": remote
|
"remote": remote
|
||||||
}
|
}
|
||||||
non_wildcard_fields = set()
|
non_wildcard_fields = set()
|
||||||
|
|
||||||
return view_audits_common(
|
return view_audits_common(
|
||||||
url, token, "file", filters, page_size, current, created_at, days_ago, non_wildcard_fields
|
url, token, "file", filters, page_size, current, created_at, days_ago, non_wildcard_fields
|
||||||
)
|
)
|
||||||
@@ -267,7 +267,7 @@ def view_alarm_audits(url, token, device=None,
|
|||||||
"device": device
|
"device": device
|
||||||
}
|
}
|
||||||
non_wildcard_fields = set()
|
non_wildcard_fields = set()
|
||||||
|
|
||||||
return view_audits_common(
|
return view_audits_common(
|
||||||
url, token, "alarm", filters, page_size, current, created_at, days_ago, non_wildcard_fields
|
url, token, "alarm", filters, page_size, current, created_at, days_ago, non_wildcard_fields
|
||||||
)
|
)
|
||||||
@@ -280,7 +280,7 @@ def view_console_audits(url, token, operator=None,
|
|||||||
"operator": operator
|
"operator": operator
|
||||||
}
|
}
|
||||||
non_wildcard_fields = set()
|
non_wildcard_fields = set()
|
||||||
|
|
||||||
return view_audits_common(
|
return view_audits_common(
|
||||||
url, token, "console", filters, page_size, current, created_at, days_ago, non_wildcard_fields
|
url, token, "console", filters, page_size, current, created_at, days_ago, non_wildcard_fields
|
||||||
)
|
)
|
||||||
@@ -295,15 +295,15 @@ def main():
|
|||||||
)
|
)
|
||||||
parser.add_argument("--url", required=True, help="URL of the API")
|
parser.add_argument("--url", required=True, help="URL of the API")
|
||||||
parser.add_argument("--token", required=True, help="Bearer token for authentication")
|
parser.add_argument("--token", required=True, help="Bearer token for authentication")
|
||||||
|
|
||||||
# Pagination parameters
|
# Pagination parameters
|
||||||
parser.add_argument("--page-size", type=int, default=10, help="Number of records per page (default: 10)")
|
parser.add_argument("--page-size", type=int, default=10, help="Number of records per page (default: 10)")
|
||||||
parser.add_argument("--current", type=int, default=1, help="Current page number (default: 1)")
|
parser.add_argument("--current", type=int, default=1, help="Current page number (default: 1)")
|
||||||
|
|
||||||
# Time filtering parameters
|
# Time filtering parameters
|
||||||
parser.add_argument("--created-at", help="Filter by creation time in local time (format: 2025-09-16 14:15:57 or 2025-09-16 14:15:57.000)")
|
parser.add_argument("--created-at", help="Filter by creation time in local time (format: 2025-09-16 14:15:57 or 2025-09-16 14:15:57.000)")
|
||||||
parser.add_argument("--days-ago", type=int, help="Filter by days ago (e.g., 7 for last 7 days)")
|
parser.add_argument("--days-ago", type=int, help="Filter by days ago (e.g., 7 for last 7 days)")
|
||||||
|
|
||||||
# Audit filters (simplified)
|
# Audit filters (simplified)
|
||||||
parser.add_argument("--remote", help="Remote peer ID filter (for conn/file audits)")
|
parser.add_argument("--remote", help="Remote peer ID filter (for conn/file audits)")
|
||||||
parser.add_argument("--device", help="Device ID filter (for alarm audits)")
|
parser.add_argument("--device", help="Device ID filter (for alarm audits)")
|
||||||
@@ -319,9 +319,9 @@ def main():
|
|||||||
if args.command == "view-conn":
|
if args.command == "view-conn":
|
||||||
# View connection audits
|
# View connection audits
|
||||||
result = view_conn_audits(
|
result = view_conn_audits(
|
||||||
args.url,
|
args.url,
|
||||||
args.token,
|
args.token,
|
||||||
args.remote,
|
args.remote,
|
||||||
args.conn_type,
|
args.conn_type,
|
||||||
args.page_size,
|
args.page_size,
|
||||||
args.current,
|
args.current,
|
||||||
@@ -329,12 +329,12 @@ def main():
|
|||||||
args.days_ago
|
args.days_ago
|
||||||
)
|
)
|
||||||
print(json.dumps(result, indent=2))
|
print(json.dumps(result, indent=2))
|
||||||
|
|
||||||
elif args.command == "view-file":
|
elif args.command == "view-file":
|
||||||
# View file audits
|
# View file audits
|
||||||
result = view_file_audits(
|
result = view_file_audits(
|
||||||
args.url,
|
args.url,
|
||||||
args.token,
|
args.token,
|
||||||
args.remote,
|
args.remote,
|
||||||
args.page_size,
|
args.page_size,
|
||||||
args.current,
|
args.current,
|
||||||
@@ -342,12 +342,12 @@ def main():
|
|||||||
args.days_ago
|
args.days_ago
|
||||||
)
|
)
|
||||||
print(json.dumps(result, indent=2))
|
print(json.dumps(result, indent=2))
|
||||||
|
|
||||||
elif args.command == "view-alarm":
|
elif args.command == "view-alarm":
|
||||||
# View alarm audits
|
# View alarm audits
|
||||||
result = view_alarm_audits(
|
result = view_alarm_audits(
|
||||||
args.url,
|
args.url,
|
||||||
args.token,
|
args.token,
|
||||||
args.device,
|
args.device,
|
||||||
args.page_size,
|
args.page_size,
|
||||||
args.current,
|
args.current,
|
||||||
@@ -355,12 +355,12 @@ def main():
|
|||||||
args.days_ago
|
args.days_ago
|
||||||
)
|
)
|
||||||
print(json.dumps(result, indent=2))
|
print(json.dumps(result, indent=2))
|
||||||
|
|
||||||
elif args.command == "view-console":
|
elif args.command == "view-console":
|
||||||
# View console audits
|
# View console audits
|
||||||
result = view_console_audits(
|
result = view_console_audits(
|
||||||
args.url,
|
args.url,
|
||||||
args.token,
|
args.token,
|
||||||
args.operator,
|
args.operator,
|
||||||
args.page_size,
|
args.page_size,
|
||||||
args.current,
|
args.current,
|
||||||
|
|||||||
@@ -31,22 +31,168 @@ LExit:
|
|||||||
return WcaFinalize(er);
|
return WcaFinalize(er);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CAUTION: We can't simply remove the install folder here, because silent repair/upgrade will fail.
|
// Helper function to safely delete a file or directory using handle-based deletion.
|
||||||
// `RemoveInstallFolder()` is a deferred custom action, it will be executed after the files are copied.
|
// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions.
|
||||||
// `msiexec /i package.msi /qn`
|
BOOL SafeDeleteItem(LPCWSTR fullPath)
|
||||||
|
{
|
||||||
|
// Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT
|
||||||
|
// to prevent following symlinks.
|
||||||
|
// Use shared access to allow deletion even when other processes have the file open.
|
||||||
|
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT;
|
||||||
|
HANDLE hFile = CreateFileW(
|
||||||
|
fullPath,
|
||||||
|
DELETE,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access
|
||||||
|
NULL,
|
||||||
|
OPEN_EXISTING,
|
||||||
|
flags,
|
||||||
|
NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hFile == INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to open '%ls'. Error: %lu", fullPath, GetLastError());
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use SetFileInformationByHandle to mark for deletion.
|
||||||
|
// The file will be deleted when the handle is closed.
|
||||||
|
FILE_DISPOSITION_INFO dispInfo;
|
||||||
|
dispInfo.DeleteFile = TRUE;
|
||||||
|
|
||||||
|
BOOL result = SetFileInformationByHandle(
|
||||||
|
hFile,
|
||||||
|
FileDispositionInfo,
|
||||||
|
&dispInfo,
|
||||||
|
sizeof(dispInfo)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to mark '%ls' for deletion. Error: %lu", fullPath, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseHandle(hFile);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to recursively delete a directory's contents with detailed logging.
|
||||||
|
void RecursiveDelete(LPCWSTR path)
|
||||||
|
{
|
||||||
|
// Ensure the path is not empty or null.
|
||||||
|
if (path == NULL || path[0] == L'\0')
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extra safety: never operate directly on a root path.
|
||||||
|
if (PathIsRootW(path))
|
||||||
|
{
|
||||||
|
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MAX_PATH is enough here since the installer should not be using longer paths.
|
||||||
|
// No need to handle extended-length paths (\\?\) in this context.
|
||||||
|
WCHAR searchPath[MAX_PATH];
|
||||||
|
HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WIN32_FIND_DATAW findData;
|
||||||
|
HANDLE hFind = FindFirstFileW(searchPath, &findData);
|
||||||
|
|
||||||
|
if (hFind == INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
// This can happen if the directory is empty or doesn't exist, which is not an error in our case.
|
||||||
|
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Skip '.' and '..' directories.
|
||||||
|
if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MAX_PATH is enough here since the installer should not be using longer paths.
|
||||||
|
// No need to handle extended-length paths (\\?\) in this context.
|
||||||
|
WCHAR fullPath[MAX_PATH];
|
||||||
|
hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before acting, ensure the read-only attribute is not set.
|
||||||
|
if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
|
||||||
|
{
|
||||||
|
if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY))
|
||||||
|
{
|
||||||
|
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||||
|
{
|
||||||
|
// Check for reparse points (symlinks/junctions) to prevent directory traversal attacks.
|
||||||
|
// Do not follow reparse points, only remove the link itself.
|
||||||
|
if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
|
||||||
|
{
|
||||||
|
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath);
|
||||||
|
SafeDeleteItem(fullPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Recursively delete directory contents first
|
||||||
|
RecursiveDelete(fullPath);
|
||||||
|
// Then delete the directory itself
|
||||||
|
SafeDeleteItem(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Delete file using safe handle-based deletion
|
||||||
|
SafeDeleteItem(fullPath);
|
||||||
|
}
|
||||||
|
} while (FindNextFileW(hFind, &findData) != 0);
|
||||||
|
|
||||||
|
DWORD lastError = GetLastError();
|
||||||
|
if (lastError != ERROR_NO_MORE_FILES)
|
||||||
|
{
|
||||||
|
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError);
|
||||||
|
}
|
||||||
|
|
||||||
|
FindClose(hFind);
|
||||||
|
}
|
||||||
|
|
||||||
|
// See `Package.wxs` for the sequence of this custom action.
|
||||||
//
|
//
|
||||||
// So we need to delete the files separately in install folder.
|
// Upgrade/uninstall sequence:
|
||||||
|
// 1. InstallInitialize
|
||||||
|
// 2. RemoveExistingProducts
|
||||||
|
// ├─ TerminateProcesses
|
||||||
|
// ├─ TryStopDeleteService
|
||||||
|
// ├─ RemoveInstallFolder - <-- Here
|
||||||
|
// └─ RemoveFiles
|
||||||
|
// 3. InstallValidate
|
||||||
|
// 4. InstallFiles
|
||||||
|
// 5. InstallExecute
|
||||||
|
// 6. InstallFinalize
|
||||||
UINT __stdcall RemoveInstallFolder(
|
UINT __stdcall RemoveInstallFolder(
|
||||||
__in MSIHANDLE hInstall)
|
__in MSIHANDLE hInstall)
|
||||||
{
|
{
|
||||||
HRESULT hr = S_OK;
|
HRESULT hr = S_OK;
|
||||||
DWORD er = ERROR_SUCCESS;
|
DWORD er = ERROR_SUCCESS;
|
||||||
|
|
||||||
int nResult = 0;
|
|
||||||
LPWSTR installFolder = NULL;
|
LPWSTR installFolder = NULL;
|
||||||
LPWSTR pwz = NULL;
|
LPWSTR pwz = NULL;
|
||||||
LPWSTR pwzData = NULL;
|
LPWSTR pwzData = NULL;
|
||||||
WCHAR runtimeBroker[1024] = { 0, };
|
|
||||||
|
|
||||||
hr = WcaInitialize(hInstall, "RemoveInstallFolder");
|
hr = WcaInitialize(hInstall, "RemoveInstallFolder");
|
||||||
ExitOnFailure(hr, "Failed to initialize");
|
ExitOnFailure(hr, "Failed to initialize");
|
||||||
@@ -58,24 +204,23 @@ UINT __stdcall RemoveInstallFolder(
|
|||||||
hr = WcaReadStringFromCaData(&pwz, &installFolder);
|
hr = WcaReadStringFromCaData(&pwz, &installFolder);
|
||||||
ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
|
ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
|
||||||
|
|
||||||
StringCchPrintfW(runtimeBroker, sizeof(runtimeBroker) / sizeof(runtimeBroker[0]), L"%ls\\RuntimeBroker_rustdesk.exe", installFolder);
|
if (installFolder == NULL || installFolder[0] == L'\0') {
|
||||||
|
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete.");
|
||||||
SHFILEOPSTRUCTW fileOp;
|
goto LExit;
|
||||||
ZeroMemory(&fileOp, sizeof(SHFILEOPSTRUCT));
|
|
||||||
fileOp.wFunc = FO_DELETE;
|
|
||||||
fileOp.pFrom = runtimeBroker;
|
|
||||||
fileOp.fFlags = FOF_NOCONFIRMATION | FOF_SILENT;
|
|
||||||
|
|
||||||
nResult = SHFileOperationW(&fileOp);
|
|
||||||
if (nResult == 0)
|
|
||||||
{
|
|
||||||
WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has been deleted.", runtimeBroker);
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
if (PathIsRootW(installFolder)) {
|
||||||
WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has not been deleted, error code: 0x%02X. Please refer to https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationa for the error codes.", runtimeBroker, nResult);
|
WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder);
|
||||||
|
goto LExit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder);
|
||||||
|
|
||||||
|
RecursiveDelete(installFolder);
|
||||||
|
|
||||||
|
// The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories.
|
||||||
|
// We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer.
|
||||||
|
|
||||||
LExit:
|
LExit:
|
||||||
ReleaseStr(pwzData);
|
ReleaseStr(pwzData);
|
||||||
|
|
||||||
@@ -109,9 +254,12 @@ bool TerminateProcessIfNotContainsParam(pfnNtQueryInformationProcess NtQueryInfo
|
|||||||
{
|
{
|
||||||
if (pebUpp.CommandLine.Length > 0)
|
if (pebUpp.CommandLine.Length > 0)
|
||||||
{
|
{
|
||||||
WCHAR *commandLine = (WCHAR *)malloc(pebUpp.CommandLine.Length);
|
// Allocate extra space for null terminator
|
||||||
|
WCHAR *commandLine = (WCHAR *)malloc(pebUpp.CommandLine.Length + sizeof(WCHAR));
|
||||||
if (commandLine != NULL)
|
if (commandLine != NULL)
|
||||||
{
|
{
|
||||||
|
// Initialize all bytes to zero for safety
|
||||||
|
memset(commandLine, 0, pebUpp.CommandLine.Length + sizeof(WCHAR));
|
||||||
if (ReadProcessMemory(process, pebUpp.CommandLine.Buffer,
|
if (ReadProcessMemory(process, pebUpp.CommandLine.Buffer,
|
||||||
commandLine, pebUpp.CommandLine.Length, &dwBytesRead))
|
commandLine, pebUpp.CommandLine.Length, &dwBytesRead))
|
||||||
{
|
{
|
||||||
@@ -468,10 +616,10 @@ UINT __stdcall TryStopDeleteService(__in MSIHANDLE hInstall)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (IsServiceRunningW(svcName)) {
|
if (IsServiceRunningW(svcName)) {
|
||||||
WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is not stoped after 1000 ms.", svcName);
|
WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is not stopped after 1000 ms.", svcName);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is stoped.", svcName);
|
WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is stopped.", svcName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MyDeleteServiceW(svcName)) {
|
if (MyDeleteServiceW(svcName)) {
|
||||||
@@ -497,7 +645,7 @@ UINT __stdcall TryStopDeleteService(__in MSIHANDLE hInstall)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// It's really strange that we need sleep here.
|
// It's really strange that we need sleep here.
|
||||||
// But the upgrading may be stucked at "copying new files" because the file is in using.
|
// But the upgrading may be stuck at "copying new files" because the file is in using.
|
||||||
// Steps to reproduce: Install -> stop service in tray --> start service -> upgrade
|
// Steps to reproduce: Install -> stop service in tray --> start service -> upgrade
|
||||||
// Sleep(300);
|
// Sleep(300);
|
||||||
|
|
||||||
@@ -610,7 +758,7 @@ UINT __stdcall AddRegSoftwareSASGeneration(__in MSIHANDLE hInstall)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Why RegSetValueExW always return 998?
|
// Why RegSetValueExW always return 998?
|
||||||
//
|
//
|
||||||
result = RegCreateKeyExW(HKEY_LOCAL_MACHINE, subKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL);
|
result = RegCreateKeyExW(HKEY_LOCAL_MACHINE, subKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL);
|
||||||
if (result != ERROR_SUCCESS) {
|
if (result != ERROR_SUCCESS) {
|
||||||
WcaLog(LOGMSG_STANDARD, "Failed to create or open registry key: %d", result);
|
WcaLog(LOGMSG_STANDARD, "Failed to create or open registry key: %d", result);
|
||||||
@@ -726,7 +874,7 @@ void TryCreateStartServiceByShell(LPWSTR svcName, LPWSTR svcBinary, LPWSTR szSvc
|
|||||||
i = 0;
|
i = 0;
|
||||||
j = 0;
|
j = 0;
|
||||||
// svcBinary is a string with double quotes, we need to escape it for shell arguments.
|
// svcBinary is a string with double quotes, we need to escape it for shell arguments.
|
||||||
// It is orignal used for `CreateServiceW`.
|
// It is original used for `CreateServiceW`.
|
||||||
// eg. "C:\Program Files\MyApp\MyApp.exe" --service -> \"C:\Program Files\MyApp\MyApp.exe\" --service
|
// eg. "C:\Program Files\MyApp\MyApp.exe" --service -> \"C:\Program Files\MyApp\MyApp.exe\" --service
|
||||||
while (true) {
|
while (true) {
|
||||||
if (svcBinary[j] == L'"') {
|
if (svcBinary[j] == L'"') {
|
||||||
|
|||||||
@@ -336,7 +336,9 @@ def gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir):
|
|||||||
f'{indent}<RegistryValue Type="integer" Name="Language" Value="[ProductLanguage]" />\n'
|
f'{indent}<RegistryValue Type="integer" Name="Language" Value="[ProductLanguage]" />\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
estimated_size = get_folder_size(dist_dir)
|
# EstimatedSize in uninstall registry must be in KB.
|
||||||
|
estimated_size_bytes = get_folder_size(dist_dir)
|
||||||
|
estimated_size = max(1, (estimated_size_bytes + 1023) // 1024)
|
||||||
lines_new.append(
|
lines_new.append(
|
||||||
f'{indent}<RegistryValue Type="integer" Name="EstimatedSize" Value="{estimated_size}" />\n'
|
f'{indent}<RegistryValue Type="integer" Name="EstimatedSize" Value="{estimated_size}" />\n'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.5
|
Version: 1.4.6
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.5
|
Version: 1.4.6
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.5
|
Version: 1.4.6
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -25,7 +25,13 @@ impl Session {
|
|||||||
pub fn new(id: &str, sender: mpsc::UnboundedSender<Data>) -> Self {
|
pub fn new(id: &str, sender: mpsc::UnboundedSender<Data>) -> Self {
|
||||||
let mut password = "".to_owned();
|
let mut password = "".to_owned();
|
||||||
if PeerConfig::load(id).password.is_empty() {
|
if PeerConfig::load(id).password.is_empty() {
|
||||||
password = rpassword::prompt_password("Enter password: ").unwrap();
|
match rpassword::prompt_password("Enter password: ") {
|
||||||
|
Ok(p) => password = p,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to read password: {:?}", e);
|
||||||
|
password = "".to_owned();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let session = Self {
|
let session = Self {
|
||||||
id: id.to_owned(),
|
id: id.to_owned(),
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ use crate::{
|
|||||||
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported,
|
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported,
|
||||||
kcp_stream::KcpStream,
|
kcp_stream::KcpStream,
|
||||||
secure_tcp,
|
secure_tcp,
|
||||||
ui_interface::{get_builtin_option, use_texture_render},
|
ui_interface::{get_builtin_option, resolve_avatar_url, use_texture_render},
|
||||||
ui_session_interface::{InvokeUiSession, Session},
|
ui_session_interface::{InvokeUiSession, Session},
|
||||||
};
|
};
|
||||||
#[cfg(feature = "unix-file-copy-paste")]
|
#[cfg(feature = "unix-file-copy-paste")]
|
||||||
@@ -119,10 +119,13 @@ pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access";
|
|||||||
pub const LOGIN_MSG_OFFLINE: &str = "Offline";
|
pub const LOGIN_MSG_OFFLINE: &str = "Offline";
|
||||||
pub const LOGIN_SCREEN_WAYLAND: &str = "Wayland login screen is not supported";
|
pub const LOGIN_SCREEN_WAYLAND: &str = "Wayland login screen is not supported";
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version.";
|
pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "ubuntu-21-04-required";
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str =
|
pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str =
|
||||||
"Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.";
|
"wayland-requires-higher-linux-version";
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub const SCRAP_XDP_PORTAL_UNAVAILABLE: &str =
|
||||||
|
"xdp-portal-unavailable";
|
||||||
pub const SCRAP_X11_REQUIRED: &str = "x11 expected";
|
pub const SCRAP_X11_REQUIRED: &str = "x11 expected";
|
||||||
pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required";
|
pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required";
|
||||||
|
|
||||||
@@ -2625,15 +2628,32 @@ impl LoginConfigHandler {
|
|||||||
} else {
|
} else {
|
||||||
(my_id, self.id.clone())
|
(my_id, self.id.clone())
|
||||||
};
|
};
|
||||||
|
let mut avatar = get_builtin_option(keys::OPTION_AVATAR);
|
||||||
|
if avatar.is_empty() {
|
||||||
|
avatar = serde_json::from_str::<serde_json::Value>(&LocalConfig::get_option(
|
||||||
|
"user_info",
|
||||||
|
))
|
||||||
|
.ok()
|
||||||
|
.and_then(|x| {
|
||||||
|
x.get("avatar")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.map(|x| x.trim().to_owned())
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
}
|
||||||
|
avatar = resolve_avatar_url(avatar);
|
||||||
let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME);
|
let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME);
|
||||||
if display_name.is_empty() {
|
if display_name.is_empty() {
|
||||||
display_name =
|
display_name =
|
||||||
serde_json::from_str::<serde_json::Value>(&LocalConfig::get_option("user_info"))
|
serde_json::from_str::<serde_json::Value>(&LocalConfig::get_option("user_info"))
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
x.get("name")
|
x.get("display_name")
|
||||||
.map(|x| x.as_str().unwrap_or_default())
|
.and_then(|x| x.as_str())
|
||||||
|
.map(|x| x.trim())
|
||||||
|
.filter(|x| !x.is_empty())
|
||||||
|
.or_else(|| x.get("name").and_then(|x| x.as_str()))
|
||||||
|
.map(|x| x.to_owned())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_owned()
|
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
}
|
}
|
||||||
@@ -2681,6 +2701,7 @@ impl LoginConfigHandler {
|
|||||||
})
|
})
|
||||||
.into(),
|
.into(),
|
||||||
hwid,
|
hwid,
|
||||||
|
avatar,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
match self.conn_type {
|
match self.conn_type {
|
||||||
@@ -3849,6 +3870,7 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b
|
|||||||
&& !text.to_lowercase().contains("resolve")
|
&& !text.to_lowercase().contains("resolve")
|
||||||
&& !text.to_lowercase().contains("mismatch")
|
&& !text.to_lowercase().contains("mismatch")
|
||||||
&& !text.to_lowercase().contains("manually")
|
&& !text.to_lowercase().contains("manually")
|
||||||
|
&& !text.to_lowercase().contains("restricted")
|
||||||
&& !text.to_lowercase().contains("not allowed")))
|
&& !text.to_lowercase().contains("not allowed")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -586,7 +586,6 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
file_num,
|
file_num,
|
||||||
include_hidden,
|
include_hidden,
|
||||||
is_remote,
|
is_remote,
|
||||||
Vec::new(),
|
|
||||||
od,
|
od,
|
||||||
));
|
));
|
||||||
allow_err!(
|
allow_err!(
|
||||||
@@ -659,7 +658,6 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
file_num,
|
file_num,
|
||||||
include_hidden,
|
include_hidden,
|
||||||
is_remote,
|
is_remote,
|
||||||
Vec::new(),
|
|
||||||
od,
|
od,
|
||||||
);
|
);
|
||||||
job.is_last_job = true;
|
job.is_last_job = true;
|
||||||
@@ -845,19 +843,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Data::CancelJob(id) => {
|
Data::CancelJob(id) => {
|
||||||
let mut msg_out = Message::new();
|
self.cancel_transfer_job(id, peer).await;
|
||||||
let mut file_action = FileAction::new();
|
|
||||||
file_action.set_cancel(FileTransferCancel {
|
|
||||||
id: id,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
msg_out.set_file_action(file_action);
|
|
||||||
allow_err!(peer.send(&msg_out).await);
|
|
||||||
if let Some(job) = fs::remove_job(id, &mut self.write_jobs) {
|
|
||||||
job.remove_download_file();
|
|
||||||
}
|
|
||||||
let _ = fs::remove_job(id, &mut self.read_jobs);
|
|
||||||
self.remove_jobs.remove(&id);
|
|
||||||
}
|
}
|
||||||
Data::RemoveDir((id, path)) => {
|
Data::RemoveDir((id, path)) => {
|
||||||
let mut msg_out = Message::new();
|
let mut msg_out = Message::new();
|
||||||
@@ -1053,6 +1039,22 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn cancel_transfer_job(&mut self, id: i32, peer: &mut Stream) {
|
||||||
|
let mut msg_out = Message::new();
|
||||||
|
let mut file_action = FileAction::new();
|
||||||
|
file_action.set_cancel(FileTransferCancel {
|
||||||
|
id,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
msg_out.set_file_action(file_action);
|
||||||
|
allow_err!(peer.send(&msg_out).await);
|
||||||
|
if let Some(job) = fs::remove_job(id, &mut self.write_jobs) {
|
||||||
|
job.remove_download_file();
|
||||||
|
}
|
||||||
|
let _ = fs::remove_job(id, &mut self.read_jobs);
|
||||||
|
self.remove_jobs.remove(&id);
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn sync_jobs_status_to_local(&mut self) -> bool {
|
pub async fn sync_jobs_status_to_local(&mut self) -> bool {
|
||||||
if !self.is_connected {
|
if !self.is_connected {
|
||||||
return false;
|
return false;
|
||||||
@@ -1446,6 +1448,23 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
if !self.handler.lc.read().unwrap().disable_clipboard.v {
|
if !self.handler.lc.read().unwrap().disable_clipboard.v {
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
update_clipboard(_mcb.clipboards, ClipboardSide::Client);
|
update_clipboard(_mcb.clipboards, ClipboardSide::Client);
|
||||||
|
#[cfg(target_os = "ios")]
|
||||||
|
{
|
||||||
|
if let Some(cb) = _mcb
|
||||||
|
.clipboards
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.format.enum_value() == Ok(ClipboardFormat::Text))
|
||||||
|
{
|
||||||
|
let content = if cb.compress {
|
||||||
|
hbb_common::compress::decompress(&cb.content)
|
||||||
|
} else {
|
||||||
|
cb.content.to_vec()
|
||||||
|
};
|
||||||
|
if let Ok(content) = String::from_utf8(content) {
|
||||||
|
self.handler.clipboard(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
crate::clipboard::handle_msg_multi_clipboards(_mcb);
|
crate::clipboard::handle_msg_multi_clipboards(_mcb);
|
||||||
}
|
}
|
||||||
@@ -1470,14 +1489,43 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
fs::transform_windows_path(&mut entries);
|
fs::transform_windows_path(&mut entries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.handler
|
// We cannot call cancel_transfer_job/handle_job_status while holding
|
||||||
.update_folder_files(fd.id, &entries, fd.path, false, false);
|
// a mutable borrow from fs::get_job(&mut self.write_jobs), so defer
|
||||||
|
// the error handling until after the borrow scope ends.
|
||||||
|
let mut set_files_err = None;
|
||||||
if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) {
|
if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) {
|
||||||
log::info!("job set_files: {:?}", entries);
|
log::info!("job set_files: {:?}", entries);
|
||||||
job.set_files(entries);
|
if let Err(err) = job.set_files(entries) {
|
||||||
job.set_finished_size_on_resume();
|
set_files_err = Some(err.to_string());
|
||||||
|
} else {
|
||||||
|
job.set_finished_size_on_resume();
|
||||||
|
self.handler.update_folder_files(
|
||||||
|
fd.id,
|
||||||
|
job.files(),
|
||||||
|
fd.path,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if let Some(job) = self.remove_jobs.get_mut(&fd.id) {
|
} else if let Some(job) = self.remove_jobs.get_mut(&fd.id) {
|
||||||
|
// Intentionally keep raw entries here:
|
||||||
|
// - remote remove flow executes deletions on peer side;
|
||||||
|
// - local remove flow is populated from local get_recursive_files().
|
||||||
job.files = entries;
|
job.files = entries;
|
||||||
|
self.handler
|
||||||
|
.update_folder_files(fd.id, &job.files, fd.path, false, false);
|
||||||
|
} else {
|
||||||
|
self.handler
|
||||||
|
.update_folder_files(fd.id, &entries, fd.path, false, false);
|
||||||
|
}
|
||||||
|
if let Some(err) = set_files_err {
|
||||||
|
log::warn!(
|
||||||
|
"Rejected unsafe file list from remote peer for job {}: {}",
|
||||||
|
fd.id,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
self.cancel_transfer_job(fd.id, peer).await;
|
||||||
|
self.handle_job_status(fd.id, -1, Some(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(file_response::Union::Digest(digest)) => {
|
Some(file_response::Union::Digest(digest)) => {
|
||||||
@@ -1749,6 +1797,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
Ok(Permission::BlockInput) => {
|
Ok(Permission::BlockInput) => {
|
||||||
self.handler.set_permission("block_input", p.enabled);
|
self.handler.set_permission("block_input", p.enabled);
|
||||||
}
|
}
|
||||||
|
Ok(Permission::PrivacyMode) => {
|
||||||
|
self.handler.set_permission("privacy_mode", p.enabled);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ pub fn check_clipboard_cm() -> ResultType<MultiClipboards> {
|
|||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
fn update_clipboard_(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
fn update_clipboard_(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
||||||
let to_update_data = proto::from_multi_clipbards(multi_clipboards);
|
let to_update_data = proto::from_multi_clipboards(multi_clipboards);
|
||||||
if to_update_data.is_empty() {
|
if to_update_data.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -432,7 +432,7 @@ impl ClipboardContext {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let is_kde_x11 = false;
|
let is_kde_x11 = false;
|
||||||
let clear_holder_text = if is_kde_x11 {
|
let clear_holder_text = if is_kde_x11 {
|
||||||
"RustDesk placeholder to clear the file clipbard"
|
"RustDesk placeholder to clear the file clipboard"
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
@@ -672,7 +672,7 @@ mod proto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn from_multi_clipbards(multi_clipboards: Vec<Clipboard>) -> Vec<ClipboardData> {
|
pub fn from_multi_clipboards(multi_clipboards: Vec<Clipboard>) -> Vec<ClipboardData> {
|
||||||
multi_clipboards
|
multi_clipboards
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(from_clipboard)
|
.filter_map(from_clipboard)
|
||||||
@@ -814,7 +814,7 @@ pub mod clipboard_listener {
|
|||||||
subscribers: listener_lock.subscribers.clone(),
|
subscribers: listener_lock.subscribers.clone(),
|
||||||
};
|
};
|
||||||
let (tx_start_res, rx_start_res) = channel();
|
let (tx_start_res, rx_start_res) = channel();
|
||||||
let h = start_clipbard_master_thread(handler, tx_start_res);
|
let h = start_clipboard_master_thread(handler, tx_start_res);
|
||||||
let shutdown = match rx_start_res.recv() {
|
let shutdown = match rx_start_res.recv() {
|
||||||
Ok((Some(s), _)) => s,
|
Ok((Some(s), _)) => s,
|
||||||
Ok((None, err)) => {
|
Ok((None, err)) => {
|
||||||
@@ -854,7 +854,7 @@ pub mod clipboard_listener {
|
|||||||
log::info!("Clipboard listener unsubscribed: {}", name);
|
log::info!("Clipboard listener unsubscribed: {}", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_clipbard_master_thread(
|
fn start_clipboard_master_thread(
|
||||||
handler: impl ClipboardHandler + Send + 'static,
|
handler: impl ClipboardHandler + Send + 'static,
|
||||||
tx_start_res: Sender<(Option<Shutdown>, String)>,
|
tx_start_res: Sender<(Option<Shutdown>, String)>,
|
||||||
) -> JoinHandle<()> {
|
) -> JoinHandle<()> {
|
||||||
|
|||||||
588
src/common.rs
588
src/common.rs
@@ -39,7 +39,7 @@ use hbb_common::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
hbbs_http::{create_http_client_async, get_url_for_tls},
|
hbbs_http::{create_http_client_async, get_url_for_tls},
|
||||||
ui_interface::{get_option, set_option},
|
ui_interface::{get_api_server as ui_get_api_server, get_option, is_installed, set_option},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
@@ -1086,6 +1086,7 @@ fn get_api_server_(api: String, custom: String) -> String {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_public(url: &str) -> bool {
|
pub fn is_public(url: &str) -> bool {
|
||||||
|
let url = url.to_ascii_lowercase();
|
||||||
url.contains("rustdesk.com/") || url.ends_with("rustdesk.com")
|
url.contains("rustdesk.com/") || url.ends_with("rustdesk.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1123,22 +1124,286 @@ pub fn get_audit_server(api: String, custom: String, typ: String) -> String {
|
|||||||
format!("{}/api/audit/{}", url, typ)
|
format!("{}/api/audit/{}", url, typ)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
|
/// Check if we should use raw TCP proxy for API calls.
|
||||||
|
/// Returns true if USE_RAW_TCP_FOR_API builtin option is "Y", WebSocket is off,
|
||||||
|
/// and the target URL belongs to the configured non-public API host.
|
||||||
|
#[inline]
|
||||||
|
fn should_use_raw_tcp_for_api(url: &str) -> bool {
|
||||||
|
get_builtin_option(keys::OPTION_USE_RAW_TCP_FOR_API) == "Y"
|
||||||
|
&& !use_ws()
|
||||||
|
&& is_tcp_proxy_api_target(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if we can attempt raw TCP proxy fallback for this target URL.
|
||||||
|
#[inline]
|
||||||
|
fn can_fallback_to_raw_tcp(url: &str) -> bool {
|
||||||
|
!use_ws() && is_tcp_proxy_api_target(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn should_use_tcp_proxy_for_api_url(url: &str, api_url: &str) -> bool {
|
||||||
|
if api_url.is_empty() || is_public(api_url) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let target_host = url::Url::parse(url)
|
||||||
|
.ok()
|
||||||
|
.and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase()));
|
||||||
|
let api_host = url::Url::parse(api_url)
|
||||||
|
.ok()
|
||||||
|
.and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase()));
|
||||||
|
|
||||||
|
matches!((target_host, api_host), (Some(target), Some(api)) if target == api)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn is_tcp_proxy_api_target(url: &str) -> bool {
|
||||||
|
should_use_tcp_proxy_for_api_url(url, &ui_get_api_server())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tcp_proxy_log_target(url: &str) -> String {
|
||||||
|
url::Url::parse(url)
|
||||||
|
.ok()
|
||||||
|
.map(|parsed| {
|
||||||
|
let mut redacted = format!("{}://", parsed.scheme());
|
||||||
|
let Some(host) = parsed.host() else {
|
||||||
|
return "<invalid-url>".to_owned();
|
||||||
|
};
|
||||||
|
redacted.push_str(&host.to_string());
|
||||||
|
if let Some(port) = parsed.port() {
|
||||||
|
redacted.push(':');
|
||||||
|
redacted.push_str(&port.to_string());
|
||||||
|
}
|
||||||
|
redacted.push_str(parsed.path());
|
||||||
|
redacted
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "<invalid-url>".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get_tcp_proxy_addr() -> String {
|
||||||
|
check_port(Config::get_rendezvous_server(), RENDEZVOUS_PORT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send an HTTP request via the rendezvous server's TCP proxy using protobuf.
|
||||||
|
/// Connects with `connect_tcp` + `secure_tcp`, sends `HttpProxyRequest`,
|
||||||
|
/// receives `HttpProxyResponse`.
|
||||||
|
///
|
||||||
|
/// The entire operation (connect + handshake + send + receive) is wrapped in
|
||||||
|
/// an overall timeout of `CONNECT_TIMEOUT + READ_TIMEOUT` so that a stall at
|
||||||
|
/// any stage cannot block the caller indefinitely.
|
||||||
|
async fn tcp_proxy_request(
|
||||||
|
method: &str,
|
||||||
|
url: &str,
|
||||||
|
body: &[u8],
|
||||||
|
headers: Vec<HeaderEntry>,
|
||||||
|
) -> ResultType<HttpProxyResponse> {
|
||||||
|
let tcp_addr = get_tcp_proxy_addr();
|
||||||
|
if tcp_addr.is_empty() {
|
||||||
|
bail!("No rendezvous server configured for TCP proxy");
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed = url::Url::parse(url)?;
|
||||||
|
let path = if let Some(query) = parsed.query() {
|
||||||
|
format!("{}?{}", parsed.path(), query)
|
||||||
|
} else {
|
||||||
|
parsed.path().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!(
|
||||||
|
"Sending {} {} via TCP proxy to {}",
|
||||||
|
method,
|
||||||
|
parsed.path(),
|
||||||
|
tcp_addr
|
||||||
|
);
|
||||||
|
|
||||||
|
let overall_timeout = CONNECT_TIMEOUT + READ_TIMEOUT;
|
||||||
|
timeout(overall_timeout, async {
|
||||||
|
let mut conn = socket_client::connect_tcp(&*tcp_addr, CONNECT_TIMEOUT).await?;
|
||||||
|
let key = crate::get_key(true).await;
|
||||||
|
secure_tcp_silent(&mut conn, &key).await?;
|
||||||
|
|
||||||
|
let mut req = HttpProxyRequest::new();
|
||||||
|
req.method = method.to_uppercase();
|
||||||
|
req.path = path;
|
||||||
|
req.headers = headers.into();
|
||||||
|
req.body = Bytes::from(body.to_vec());
|
||||||
|
|
||||||
|
let mut msg_out = RendezvousMessage::new();
|
||||||
|
msg_out.set_http_proxy_request(req);
|
||||||
|
conn.send(&msg_out).await?;
|
||||||
|
|
||||||
|
match conn.next().await {
|
||||||
|
Some(Ok(bytes)) => {
|
||||||
|
let msg_in = RendezvousMessage::parse_from_bytes(&bytes)?;
|
||||||
|
match msg_in.union {
|
||||||
|
Some(rendezvous_message::Union::HttpProxyResponse(resp)) => Ok(resp),
|
||||||
|
_ => bail!("Unexpected response from TCP proxy"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Err(e)) => bail!("TCP proxy read error: {}", e),
|
||||||
|
None => bail!("TCP proxy connection closed without response"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build HeaderEntry list from "Key: Value" style header string (used by post_request).
|
||||||
|
/// If the caller supplies a Content-Type header it overrides the default `application/json`.
|
||||||
|
fn parse_simple_header(header: &str) -> Vec<HeaderEntry> {
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
let mut has_content_type = false;
|
||||||
|
if !header.is_empty() {
|
||||||
|
let tmp: Vec<&str> = header.splitn(2, ": ").collect();
|
||||||
|
if tmp.len() == 2 {
|
||||||
|
if tmp[0].eq_ignore_ascii_case("Content-Type") {
|
||||||
|
has_content_type = true;
|
||||||
|
}
|
||||||
|
entries.push(HeaderEntry {
|
||||||
|
name: tmp[0].into(),
|
||||||
|
value: tmp[1].into(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !has_content_type {
|
||||||
|
entries.insert(
|
||||||
|
0,
|
||||||
|
HeaderEntry {
|
||||||
|
name: "Content-Type".into(),
|
||||||
|
value: "application/json".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST request via TCP proxy.
|
||||||
|
async fn post_request_via_tcp_proxy(url: &str, body: &str, header: &str) -> ResultType<String> {
|
||||||
|
let headers = parse_simple_header(header);
|
||||||
|
let resp = tcp_proxy_request("POST", url, body.as_bytes(), headers).await?;
|
||||||
|
if !resp.error.is_empty() {
|
||||||
|
bail!("TCP proxy error: {}", resp.error);
|
||||||
|
}
|
||||||
|
Ok(String::from_utf8_lossy(&resp.body).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn http_proxy_response_to_json(resp: HttpProxyResponse) -> ResultType<String> {
|
||||||
|
if !resp.error.is_empty() {
|
||||||
|
bail!("TCP proxy error: {}", resp.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut response_headers = Map::new();
|
||||||
|
for entry in resp.headers.iter() {
|
||||||
|
response_headers.insert(entry.name.to_lowercase(), json!(entry.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = Map::new();
|
||||||
|
result.insert("status_code".to_string(), json!(resp.status));
|
||||||
|
result.insert("headers".to_string(), Value::Object(response_headers));
|
||||||
|
result.insert(
|
||||||
|
"body".to_string(),
|
||||||
|
json!(String::from_utf8_lossy(&resp.body)),
|
||||||
|
);
|
||||||
|
|
||||||
|
serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_json_header_entries(header: &str) -> ResultType<Vec<HeaderEntry>> {
|
||||||
|
let v: Value = serde_json::from_str(header)?;
|
||||||
|
if let Value::Object(obj) = v {
|
||||||
|
Ok(obj
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| HeaderEntry {
|
||||||
|
name: key.clone(),
|
||||||
|
value: value.as_str().unwrap_or_default().into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("HTTP header information parsing failed!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns (status_code, body_text). Separating status so the wrapper can decide on fallback.
|
||||||
|
async fn post_request_http(url: &str, body: &str, header: &str) -> ResultType<(u16, String)> {
|
||||||
let proxy_conf = Config::get_socks();
|
let proxy_conf = Config::get_socks();
|
||||||
let tls_url = get_url_for_tls(&url, &proxy_conf);
|
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||||
let tls_type = get_cached_tls_type(tls_url);
|
let tls_type = get_cached_tls_type(tls_url);
|
||||||
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||||
let response = post_request_(
|
let response = post_request_(
|
||||||
&url,
|
url,
|
||||||
tls_url,
|
tls_url,
|
||||||
body.clone(),
|
body.to_owned(),
|
||||||
header,
|
header,
|
||||||
tls_type,
|
tls_type,
|
||||||
danger_accept_invalid_cert,
|
danger_accept_invalid_cert,
|
||||||
danger_accept_invalid_cert,
|
danger_accept_invalid_cert,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(response.text().await?)
|
let status = response.status().as_u16();
|
||||||
|
let text = response.text().await?;
|
||||||
|
Ok((status, text))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try `http_fn` first; on connection failure or 5xx, fall back to `tcp_fn`
|
||||||
|
/// if the URL is eligible. 4xx responses are returned as-is.
|
||||||
|
async fn with_tcp_proxy_fallback<HttpFut, TcpFut>(
|
||||||
|
url: &str,
|
||||||
|
method: &str,
|
||||||
|
http_fn: HttpFut,
|
||||||
|
tcp_fn: TcpFut,
|
||||||
|
) -> ResultType<String>
|
||||||
|
where
|
||||||
|
HttpFut: Future<Output = ResultType<(u16, String)>>,
|
||||||
|
TcpFut: Future<Output = ResultType<String>>,
|
||||||
|
{
|
||||||
|
if should_use_raw_tcp_for_api(url) {
|
||||||
|
return tcp_fn.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let http_result = http_fn.await;
|
||||||
|
let should_fallback = match &http_result {
|
||||||
|
Err(_) => true,
|
||||||
|
Ok((status, _)) => *status >= 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_fallback && can_fallback_to_raw_tcp(url) {
|
||||||
|
log::warn!(
|
||||||
|
"HTTP {} to {} failed or 5xx (result: {:?}), trying TCP proxy fallback",
|
||||||
|
method,
|
||||||
|
tcp_proxy_log_target(url),
|
||||||
|
http_result
|
||||||
|
.as_ref()
|
||||||
|
.map(|(s, _)| *s)
|
||||||
|
.map_err(|e| e.to_string()),
|
||||||
|
);
|
||||||
|
match tcp_fn.await {
|
||||||
|
Ok(resp) => return Ok(resp),
|
||||||
|
Err(tcp_err) => {
|
||||||
|
log::warn!("TCP proxy fallback also failed: {:?}", tcp_err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http_result.map(|(_status, text)| text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST request with raw TCP proxy support.
|
||||||
|
/// - If `USE_RAW_TCP_FOR_API` is "Y" and WS is off, goes directly through TCP proxy.
|
||||||
|
/// - Otherwise tries HTTP first; on connection failure or 5xx status,
|
||||||
|
/// falls back to TCP proxy if WS is off.
|
||||||
|
/// - 4xx responses are returned as-is (server is reachable, business logic error).
|
||||||
|
/// - If fallback also fails, returns the original HTTP result (text or error).
|
||||||
|
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
|
||||||
|
with_tcp_proxy_fallback(
|
||||||
|
&url,
|
||||||
|
"POST",
|
||||||
|
post_request_http(&url, &body, header),
|
||||||
|
post_request_via_tcp_proxy(&url, &body, header),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
@@ -1246,21 +1511,16 @@ async fn get_http_response_async(
|
|||||||
tls_type.unwrap_or(TlsType::Rustls),
|
tls_type.unwrap_or(TlsType::Rustls),
|
||||||
danger_accept_invalid_cert.unwrap_or(false),
|
danger_accept_invalid_cert.unwrap_or(false),
|
||||||
);
|
);
|
||||||
let mut http_client = match method {
|
let normalized_method = method.to_ascii_lowercase();
|
||||||
|
let mut http_client = match normalized_method.as_str() {
|
||||||
"get" => http_client.get(url),
|
"get" => http_client.get(url),
|
||||||
"post" => http_client.post(url),
|
"post" => http_client.post(url),
|
||||||
"put" => http_client.put(url),
|
"put" => http_client.put(url),
|
||||||
"delete" => http_client.delete(url),
|
"delete" => http_client.delete(url),
|
||||||
_ => return Err(anyhow!("The HTTP request method is not supported!")),
|
_ => return Err(anyhow!("The HTTP request method is not supported!")),
|
||||||
};
|
};
|
||||||
let v = serde_json::from_str(header)?;
|
for entry in parse_json_header_entries(header)? {
|
||||||
|
http_client = http_client.header(entry.name, entry.value);
|
||||||
if let Value::Object(obj) = v {
|
|
||||||
for (key, value) in obj.iter() {
|
|
||||||
http_client = http_client.header(key, value.as_str().unwrap_or_default());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(anyhow!("HTTP header information parsing failed!"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if tls_type.is_some() && danger_accept_invalid_cert.is_some() {
|
if tls_type.is_some() && danger_accept_invalid_cert.is_some() {
|
||||||
@@ -1340,6 +1600,51 @@ async fn get_http_response_async(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns (status_code, json_string) so the caller can inspect the status
|
||||||
|
/// without re-parsing the serialized JSON.
|
||||||
|
async fn http_request_http(
|
||||||
|
url: &str,
|
||||||
|
method: &str,
|
||||||
|
body: Option<String>,
|
||||||
|
header: &str,
|
||||||
|
) -> ResultType<(u16, String)> {
|
||||||
|
let proxy_conf = Config::get_socks();
|
||||||
|
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||||
|
let tls_type = get_cached_tls_type(tls_url);
|
||||||
|
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||||
|
let response = get_http_response_async(
|
||||||
|
url,
|
||||||
|
tls_url,
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
header,
|
||||||
|
tls_type,
|
||||||
|
danger_accept_invalid_cert,
|
||||||
|
danger_accept_invalid_cert,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
// Serialize response headers
|
||||||
|
let mut response_headers = Map::new();
|
||||||
|
for (key, value) in response.headers() {
|
||||||
|
response_headers.insert(key.to_string(), json!(value.to_str().unwrap_or("")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let status_code = response.status().as_u16();
|
||||||
|
let response_body = response.text().await?;
|
||||||
|
|
||||||
|
// Construct the JSON object
|
||||||
|
let mut result = Map::new();
|
||||||
|
result.insert("status_code".to_string(), json!(status_code));
|
||||||
|
result.insert("headers".to_string(), Value::Object(response_headers));
|
||||||
|
result.insert("body".to_string(), json!(response_body));
|
||||||
|
|
||||||
|
// Convert map to JSON string
|
||||||
|
let json_str = serde_json::to_string(&result)
|
||||||
|
.map_err(|e| anyhow!("Failed to serialize response: {}", e))?;
|
||||||
|
Ok((status_code, json_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HTTP request with raw TCP proxy support.
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
pub async fn http_request_sync(
|
pub async fn http_request_sync(
|
||||||
url: String,
|
url: String,
|
||||||
@@ -1347,44 +1652,28 @@ pub async fn http_request_sync(
|
|||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
header: String,
|
header: String,
|
||||||
) -> ResultType<String> {
|
) -> ResultType<String> {
|
||||||
let proxy_conf = Config::get_socks();
|
with_tcp_proxy_fallback(
|
||||||
let tls_url = get_url_for_tls(&url, &proxy_conf);
|
|
||||||
let tls_type = get_cached_tls_type(tls_url);
|
|
||||||
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
|
||||||
let response = get_http_response_async(
|
|
||||||
&url,
|
&url,
|
||||||
tls_url,
|
|
||||||
&method,
|
&method,
|
||||||
body.clone(),
|
http_request_http(&url, &method, body.clone(), &header),
|
||||||
&header,
|
http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header),
|
||||||
tls_type,
|
|
||||||
danger_accept_invalid_cert,
|
|
||||||
danger_accept_invalid_cert,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
// Serialize response headers
|
}
|
||||||
let mut response_headers = serde_json::map::Map::new();
|
|
||||||
for (key, value) in response.headers() {
|
|
||||||
response_headers.insert(
|
|
||||||
key.to_string(),
|
|
||||||
serde_json::json!(value.to_str().unwrap_or("")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let status_code = response.status().as_u16();
|
/// General HTTP request via TCP proxy. Header is a JSON string (used by http_request_sync).
|
||||||
let response_body = response.text().await?;
|
/// Returns a JSON string with status_code, headers, body (same format as http_request_sync).
|
||||||
|
async fn http_request_via_tcp_proxy(
|
||||||
|
url: &str,
|
||||||
|
method: &str,
|
||||||
|
body: Option<&str>,
|
||||||
|
header: &str,
|
||||||
|
) -> ResultType<String> {
|
||||||
|
let headers = parse_json_header_entries(header)?;
|
||||||
|
let body_bytes = body.unwrap_or("").as_bytes();
|
||||||
|
|
||||||
// Construct the JSON object
|
let resp = tcp_proxy_request(method, url, body_bytes, headers).await?;
|
||||||
let mut result = serde_json::map::Map::new();
|
http_proxy_response_to_json(resp)
|
||||||
result.insert("status_code".to_string(), serde_json::json!(status_code));
|
|
||||||
result.insert(
|
|
||||||
"headers".to_string(),
|
|
||||||
serde_json::Value::Object(response_headers),
|
|
||||||
);
|
|
||||||
result.insert("body".to_string(), serde_json::json!(response_body));
|
|
||||||
|
|
||||||
// Convert map to JSON string
|
|
||||||
serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -1647,7 +1936,7 @@ pub fn check_process(arg: &str, mut same_uid: bool) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) -> ResultType<()> {
|
||||||
// Skip additional encryption when using WebSocket connections (wss://)
|
// Skip additional encryption when using WebSocket connections (wss://)
|
||||||
// as WebSocket Secure (wss://) already provides transport layer encryption.
|
// as WebSocket Secure (wss://) already provides transport layer encryption.
|
||||||
// This doesn't affect the end-to-end encryption between clients,
|
// This doesn't affect the end-to-end encryption between clients,
|
||||||
@@ -1680,7 +1969,9 @@ pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
|||||||
});
|
});
|
||||||
timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??;
|
timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??;
|
||||||
conn.set_key(key);
|
conn.set_key(key);
|
||||||
log::info!("Connection secured");
|
if log_on_success {
|
||||||
|
log::info!("Connection secured");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -1691,6 +1982,14 @@ pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||||
|
secure_tcp_impl(conn, key, true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn secure_tcp_silent(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||||
|
secure_tcp_impl(conn, key, false).await
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn get_pk(pk: &[u8]) -> Option<[u8; 32]> {
|
fn get_pk(pk: &[u8]) -> Option<[u8; 32]> {
|
||||||
if pk.len() == 32 {
|
if pk.len() == 32 {
|
||||||
@@ -2468,11 +2767,13 @@ mod tests {
|
|||||||
assert!(is_public("https://rustdesk.com/"));
|
assert!(is_public("https://rustdesk.com/"));
|
||||||
assert!(is_public("https://www.rustdesk.com/"));
|
assert!(is_public("https://www.rustdesk.com/"));
|
||||||
assert!(is_public("https://api.rustdesk.com/v1"));
|
assert!(is_public("https://api.rustdesk.com/v1"));
|
||||||
|
assert!(is_public("https://API.RUSTDESK.COM/v1"));
|
||||||
assert!(is_public("https://rustdesk.com/path"));
|
assert!(is_public("https://rustdesk.com/path"));
|
||||||
|
|
||||||
// Test URLs ending with "rustdesk.com"
|
// Test URLs ending with "rustdesk.com"
|
||||||
assert!(is_public("rustdesk.com"));
|
assert!(is_public("rustdesk.com"));
|
||||||
assert!(is_public("https://rustdesk.com"));
|
assert!(is_public("https://rustdesk.com"));
|
||||||
|
assert!(is_public("https://RustDesk.com"));
|
||||||
assert!(is_public("http://www.rustdesk.com"));
|
assert!(is_public("http://www.rustdesk.com"));
|
||||||
assert!(is_public("https://api.rustdesk.com"));
|
assert!(is_public("https://api.rustdesk.com"));
|
||||||
|
|
||||||
@@ -2485,6 +2786,193 @@ mod tests {
|
|||||||
assert!(!is_public("rustdesk.comhello.com"));
|
assert!(!is_public("rustdesk.comhello.com"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_use_tcp_proxy_for_api_url() {
|
||||||
|
assert!(should_use_tcp_proxy_for_api_url(
|
||||||
|
"https://admin.example.com/api/login",
|
||||||
|
"https://admin.example.com"
|
||||||
|
));
|
||||||
|
assert!(should_use_tcp_proxy_for_api_url(
|
||||||
|
"https://admin.example.com:21114/api/login",
|
||||||
|
"https://admin.example.com"
|
||||||
|
));
|
||||||
|
assert!(!should_use_tcp_proxy_for_api_url(
|
||||||
|
"https://api.telegram.org/bot123/sendMessage",
|
||||||
|
"https://admin.example.com"
|
||||||
|
));
|
||||||
|
assert!(!should_use_tcp_proxy_for_api_url(
|
||||||
|
"https://admin.rustdesk.com/api/login",
|
||||||
|
"https://admin.rustdesk.com"
|
||||||
|
));
|
||||||
|
assert!(!should_use_tcp_proxy_for_api_url(
|
||||||
|
"https://admin.example.com/api/login",
|
||||||
|
"not a url"
|
||||||
|
));
|
||||||
|
assert!(!should_use_tcp_proxy_for_api_url(
|
||||||
|
"not a url",
|
||||||
|
"https://admin.example.com"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_tcp_proxy_addr_normalizes_bare_ipv6_host() {
|
||||||
|
struct RestoreCustomRendezvousServer(String);
|
||||||
|
|
||||||
|
impl Drop for RestoreCustomRendezvousServer {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
Config::set_option(
|
||||||
|
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(),
|
||||||
|
self.0.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _restore = RestoreCustomRendezvousServer(Config::get_option(
|
||||||
|
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER,
|
||||||
|
));
|
||||||
|
Config::set_option(
|
||||||
|
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(),
|
||||||
|
"1:2".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(get_tcp_proxy_addr(), format!("[1:2]:{RENDEZVOUS_PORT}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_http_request_via_tcp_proxy_rejects_invalid_header_json() {
|
||||||
|
let result = http_request_via_tcp_proxy("not a url", "get", None, "{").await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_http_request_via_tcp_proxy_rejects_non_object_header_json() {
|
||||||
|
let err = http_request_via_tcp_proxy("not a url", "get", None, "[]")
|
||||||
|
.await
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string();
|
||||||
|
assert!(err.contains("HTTP header information parsing failed!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_json_header_entries_preserves_single_content_type() {
|
||||||
|
let headers = parse_json_header_entries(
|
||||||
|
r#"{"Content-Type":"text/plain","Authorization":"Bearer token"}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
headers
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||||
|
.count(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
headers
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||||
|
.map(|entry| entry.value.as_str()),
|
||||||
|
Some("text/plain")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_json_header_entries_does_not_add_default_content_type() {
|
||||||
|
let headers = parse_json_header_entries(r#"{"Authorization":"Bearer token"}"#).unwrap();
|
||||||
|
|
||||||
|
assert!(!headers
|
||||||
|
.iter()
|
||||||
|
.any(|entry| entry.name.eq_ignore_ascii_case("Content-Type")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_simple_header_respects_custom_content_type() {
|
||||||
|
let headers = parse_simple_header("Content-Type: text/plain");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
headers
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||||
|
.count(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
headers
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||||
|
.map(|entry| entry.value.as_str()),
|
||||||
|
Some("text/plain")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_simple_header_preserves_non_content_type_header() {
|
||||||
|
let headers = parse_simple_header("Authorization: Bearer token");
|
||||||
|
|
||||||
|
assert!(headers.iter().any(|entry| {
|
||||||
|
entry.name.eq_ignore_ascii_case("Authorization")
|
||||||
|
&& entry.value.as_str() == "Bearer token"
|
||||||
|
}));
|
||||||
|
assert_eq!(
|
||||||
|
headers
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||||
|
.count(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
headers
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||||
|
.map(|entry| entry.value.as_str()),
|
||||||
|
Some("application/json")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tcp_proxy_log_target_redacts_query_only() {
|
||||||
|
assert_eq!(
|
||||||
|
tcp_proxy_log_target("https://example.com/api/heartbeat?token=secret"),
|
||||||
|
"https://example.com/api/heartbeat"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tcp_proxy_log_target_brackets_ipv6_host_with_port() {
|
||||||
|
assert_eq!(
|
||||||
|
tcp_proxy_log_target("https://[2001:db8::1]:21114/api/heartbeat?token=secret"),
|
||||||
|
"https://[2001:db8::1]:21114/api/heartbeat"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_http_proxy_response_to_json() {
|
||||||
|
let mut resp = HttpProxyResponse {
|
||||||
|
status: 200,
|
||||||
|
body: br#"{"ok":true}"#.to_vec().into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
resp.headers.push(HeaderEntry {
|
||||||
|
name: "Content-Type".into(),
|
||||||
|
value: "application/json".into(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let json = http_proxy_response_to_json(resp).unwrap();
|
||||||
|
let value: Value = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(value["status_code"], 200);
|
||||||
|
assert_eq!(value["headers"]["content-type"], "application/json");
|
||||||
|
assert_eq!(value["body"], r#"{"ok":true}"#);
|
||||||
|
|
||||||
|
let err = http_proxy_response_to_json(HttpProxyResponse {
|
||||||
|
error: "dial failed".into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string();
|
||||||
|
assert!(err.contains("TCP proxy error: dial failed"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mouse_event_constants_and_mask_layout() {
|
fn test_mouse_event_constants_and_mask_layout() {
|
||||||
use super::input::*;
|
use super::input::*;
|
||||||
|
|||||||
@@ -187,7 +187,10 @@ pub fn core_main() -> Option<Vec<String>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
hbb_common::config::PeerConfig::preload_peers();
|
{
|
||||||
|
crate::platform::try_remove_temp_update_files();
|
||||||
|
hbb_common::config::PeerConfig::preload_peers();
|
||||||
|
}
|
||||||
std::thread::spawn(move || crate::start_server(false, no_server));
|
std::thread::spawn(move || crate::start_server(false, no_server));
|
||||||
} else {
|
} else {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -202,17 +205,24 @@ pub fn core_main() -> Option<Vec<String>> {
|
|||||||
if config::is_disable_installation() {
|
if config::is_disable_installation() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let res = platform::update_me(false);
|
|
||||||
let text = match res {
|
let text = match crate::platform::prepare_custom_client_update() {
|
||||||
Ok(_) => translate("Update successfully!".to_string()),
|
Err(e) => {
|
||||||
Err(err) => {
|
log::error!("Error preparing custom client update: {}", e);
|
||||||
log::error!("Failed with error: {err}");
|
"Update failed!".to_string()
|
||||||
translate("Update failed!".to_string())
|
|
||||||
}
|
}
|
||||||
|
Ok(false) => "Update failed!".to_string(),
|
||||||
|
Ok(true) => match platform::update_me(false) {
|
||||||
|
Ok(_) => "Updated successfully!".to_string(),
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Failed with error: {err}");
|
||||||
|
"Update failed!".to_string()
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
Toast::new(Toast::POWERSHELL_APP_ID)
|
Toast::new(Toast::POWERSHELL_APP_ID)
|
||||||
.title(&config::APP_NAME.read().unwrap())
|
.title(&config::APP_NAME.read().unwrap())
|
||||||
.text1(&text)
|
.text1(&translate(text))
|
||||||
.sound(Some(Sound::Default))
|
.sound(Some(Sound::Default))
|
||||||
.duration(Duration::Short)
|
.duration(Duration::Short)
|
||||||
.show()
|
.show()
|
||||||
@@ -325,8 +335,8 @@ pub fn core_main() -> Option<Vec<String>> {
|
|||||||
log::info!("Starting update process...");
|
log::info!("Starting update process...");
|
||||||
let _text = match platform::update_me() {
|
let _text = match platform::update_me() {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
println!("{}", translate("Update successfully!".to_string()));
|
println!("{}", translate("Updated successfully!".to_string()));
|
||||||
log::info!("Update successfully!");
|
log::info!("Updated successfully!");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Update failed with error: {}", err);
|
eprintln!("Update failed with error: {}", err);
|
||||||
|
|||||||
@@ -605,21 +605,30 @@ pub fn session_handle_flutter_raw_key_event(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncReturn<()> is used to make sure enter() and leave() are executed in the sequence this function is called.
|
|
||||||
//
|
|
||||||
// If the cursor jumps between remote page of two connections, leave view and enter view will be called.
|
// If the cursor jumps between remote page of two connections, leave view and enter view will be called.
|
||||||
// session_enter_or_leave() will be called then.
|
// session_enter_or_leave() will be called then.
|
||||||
// As rust is multi-thread, it is possible that enter() is called before leave().
|
// As Rust is multi-threaded, enter() can be called before leave().
|
||||||
// This will cause the keyboard input to take no effect.
|
// The Rust-side grab ownership state filters stale transitions.
|
||||||
pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> {
|
pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> {
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
if let Some(session) = sessions::get_session_by_session_id(&_session_id) {
|
if let Some(session) = sessions::get_session_by_session_id(&_session_id) {
|
||||||
let keyboard_mode = session.get_keyboard_mode();
|
let keyboard_mode = session.get_keyboard_mode();
|
||||||
|
// Use the full per-window UUID (not lc.session_id which is per-connection)
|
||||||
|
// so that two windows viewing the same peer get distinct grab owners.
|
||||||
|
let window_id = _session_id.as_u128();
|
||||||
if _enter {
|
if _enter {
|
||||||
set_cur_session_id_(_session_id, &keyboard_mode);
|
set_cur_session_id_(_session_id, &keyboard_mode);
|
||||||
session.enter(keyboard_mode);
|
crate::keyboard::client::change_grab_status(
|
||||||
|
crate::common::GrabState::Run,
|
||||||
|
&keyboard_mode,
|
||||||
|
window_id,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
session.leave(keyboard_mode);
|
crate::keyboard::client::change_grab_status(
|
||||||
|
crate::common::GrabState::Wait,
|
||||||
|
&keyboard_mode,
|
||||||
|
window_id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SyncReturn(())
|
SyncReturn(())
|
||||||
@@ -963,6 +972,27 @@ pub fn main_show_option(_key: String) -> SyncReturn<bool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_set_option(key: String, value: String) {
|
pub fn main_set_option(key: String, value: String) {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
let is_permission_option = key.eq(config::keys::OPTION_ENABLE_CLIPBOARD)
|
||||||
|
|| key.eq(config::keys::OPTION_ENABLE_FILE_TRANSFER)
|
||||||
|
|| key.eq(config::keys::OPTION_ENABLE_AUDIO);
|
||||||
|
let allow_perm_change_in_accept_window = config::option2bool(
|
||||||
|
config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW,
|
||||||
|
&crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW),
|
||||||
|
);
|
||||||
|
if is_permission_option
|
||||||
|
&& !allow_perm_change_in_accept_window
|
||||||
|
&& crate::ui_cm_interface::has_active_clients()
|
||||||
|
{
|
||||||
|
log::info!(
|
||||||
|
"blocked main_set_option by policy, key={}, value={}",
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) {
|
if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) {
|
||||||
crate::ui_cm_interface::switch_permission_all(
|
crate::ui_cm_interface::switch_permission_all(
|
||||||
@@ -1010,7 +1040,29 @@ pub fn main_get_options_sync() -> SyncReturn<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_set_options(json: String) {
|
pub fn main_set_options(json: String) {
|
||||||
let map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or(HashMap::new());
|
let mut map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or(HashMap::new());
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
let allow_perm_change_in_accept_window = config::option2bool(
|
||||||
|
config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW,
|
||||||
|
&crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW),
|
||||||
|
);
|
||||||
|
if !allow_perm_change_in_accept_window && crate::ui_cm_interface::has_active_clients() {
|
||||||
|
for key in [
|
||||||
|
config::keys::OPTION_ENABLE_CLIPBOARD,
|
||||||
|
config::keys::OPTION_ENABLE_FILE_TRANSFER,
|
||||||
|
config::keys::OPTION_ENABLE_AUDIO,
|
||||||
|
] {
|
||||||
|
if let Some(value) = map.remove(key) {
|
||||||
|
log::info!(
|
||||||
|
"blocked main_set_options item by policy, key={}, value={}",
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if !map.is_empty() {
|
if !map.is_empty() {
|
||||||
set_options(map)
|
set_options(map)
|
||||||
}
|
}
|
||||||
@@ -1101,6 +1153,10 @@ pub fn main_get_api_server() -> String {
|
|||||||
get_api_server()
|
get_api_server()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn<String> {
|
||||||
|
SyncReturn(resolve_avatar_url(avatar))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn main_http_request(url: String, method: String, body: Option<String>, header: String) {
|
pub fn main_http_request(url: String, method: String, body: Option<String>, header: String) {
|
||||||
http_request(url, method, body, header)
|
http_request(url, method, body, header)
|
||||||
}
|
}
|
||||||
@@ -1689,8 +1745,8 @@ pub fn main_get_temporary_password() -> String {
|
|||||||
ui_interface::temporary_password()
|
ui_interface::temporary_password()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_get_permanent_password() -> String {
|
pub fn main_set_permanent_password_with_result(password: String) -> bool {
|
||||||
ui_interface::permanent_password()
|
ui_interface::set_permanent_password_with_result(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_get_fingerprint() -> String {
|
pub fn main_get_fingerprint() -> String {
|
||||||
@@ -2068,10 +2124,6 @@ pub fn main_update_temporary_password() {
|
|||||||
update_temporary_password();
|
update_temporary_password();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_set_permanent_password(password: String) {
|
|
||||||
set_permanent_password(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main_check_super_user_permission() -> bool {
|
pub fn main_check_super_user_permission() -> bool {
|
||||||
check_super_user_permission()
|
check_super_user_permission()
|
||||||
}
|
}
|
||||||
@@ -2419,16 +2471,23 @@ pub fn is_disable_installation() -> SyncReturn<bool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_preset_password() -> bool {
|
pub fn is_preset_password() -> bool {
|
||||||
config::HARD_SETTINGS
|
let hard = config::HARD_SETTINGS
|
||||||
.read()
|
.read()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get("password")
|
.get("password")
|
||||||
.map_or(false, |p| {
|
.cloned()
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
.unwrap_or_default();
|
||||||
return p == &crate::ipc::get_permanent_password();
|
if hard.is_empty() {
|
||||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
return false;
|
||||||
return p == &config::Config::get_permanent_password();
|
}
|
||||||
})
|
|
||||||
|
// On desktop, service owns the authoritative config; query it via IPC and return only a boolean.
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
return crate::ipc::is_permanent_password_preset();
|
||||||
|
|
||||||
|
// On mobile, we have no service IPC; verify against local storage.
|
||||||
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
|
return config::Config::matches_permanent_password_plain(&hard);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't call this function for desktop version.
|
// Don't call this function for desktop version.
|
||||||
@@ -2764,6 +2823,10 @@ pub fn main_get_common(key: String) -> String {
|
|||||||
return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string();
|
return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string();
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
return false.to_string();
|
return false.to_string();
|
||||||
|
} else if key == "permanent-password-set" {
|
||||||
|
return ui_interface::is_permanent_password_set().to_string();
|
||||||
|
} else if key == "local-permanent-password-set" {
|
||||||
|
return ui_interface::is_local_permanent_password_set().to_string();
|
||||||
} else {
|
} else {
|
||||||
if key.starts_with("download-data-") {
|
if key.starts_with("download-data-") {
|
||||||
let id = key.replace("download-data-", "");
|
let id = key.replace("download-data-", "");
|
||||||
@@ -2776,10 +2839,13 @@ pub fn main_get_common(key: String) -> String {
|
|||||||
} else if key.starts_with("download-file-") {
|
} else if key.starts_with("download-file-") {
|
||||||
let _version = key.replace("download-file-", "");
|
let _version = key.replace("download-file-", "");
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
return match crate::platform::windows::is_msi_installed() {
|
return match (
|
||||||
Ok(true) => format!("rustdesk-{_version}-x86_64.msi"),
|
crate::platform::windows::is_msi_installed(),
|
||||||
Ok(false) => format!("rustdesk-{_version}-x86_64.exe"),
|
crate::common::is_custom_client(),
|
||||||
Err(e) => {
|
) {
|
||||||
|
(Ok(true), false) => format!("rustdesk-{_version}-x86_64.msi"),
|
||||||
|
(Ok(true), true) | (Ok(false), _) => format!("rustdesk-{_version}-x86_64.exe"),
|
||||||
|
(Err(e), _) => {
|
||||||
log::error!("Failed to check if is msi: {}", e);
|
log::error!("Failed to check if is msi: {}", e);
|
||||||
format!("error:update-failed-check-msi-tip")
|
format!("error:update-failed-check-msi-tip")
|
||||||
}
|
}
|
||||||
@@ -2870,36 +2936,23 @@ pub fn main_set_common(_key: String, _value: String) {
|
|||||||
} else if _key == "update-me" {
|
} else if _key == "update-me" {
|
||||||
if let Some(new_version_file) = get_download_file_from_url(&_value) {
|
if let Some(new_version_file) = get_download_file_from_url(&_value) {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"New version file is downloaed, update begin, {:?}",
|
"New version file is downloaded, update begin, {:?}",
|
||||||
new_version_file.to_str()
|
new_version_file.to_str()
|
||||||
);
|
);
|
||||||
if let Some(f) = new_version_file.to_str() {
|
if let Some(f) = new_version_file.to_str() {
|
||||||
// 1.4.0 does not support "--update"
|
// 1.4.0 does not support "--update"
|
||||||
// But we can assume that the new version supports it.
|
// But we can assume that the new version supports it.
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
if f.ends_with(".exe") {
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||||
if let Err(e) =
|
|
||||||
crate::platform::run_exe_in_cur_session(f, vec!["--update"], false)
|
|
||||||
{
|
|
||||||
log::error!("Failed to run the update exe: {}", e);
|
|
||||||
}
|
|
||||||
} else if f.ends_with(".msi") {
|
|
||||||
if let Err(e) = crate::platform::update_me_msi(f, false) {
|
|
||||||
log::error!("Failed to run the update msi: {}", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// unreachable!()
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
match crate::platform::update_to(f) {
|
match crate::platform::update_to(f) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
log::info!("Update successfully!");
|
log::info!("Update process is launched successfully!");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to update to new version, {}", e);
|
log::error!("Failed to update to new version, {}", e);
|
||||||
|
fs::remove_file(f).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fs::remove_file(f).ok();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if _key == "extract-update-dmg" {
|
} else if _key == "extract-update-dmg" {
|
||||||
@@ -3055,6 +3108,22 @@ pub mod server_side {
|
|||||||
return env.new_string(res).unwrap_or_default().into_raw();
|
return env.new_string(res).unwrap_or_default().into_raw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "system" fn Java_ffi_FFI_getBuildinOption(
|
||||||
|
env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
key: JString,
|
||||||
|
) -> jstring {
|
||||||
|
let mut env = env;
|
||||||
|
let res = if let Ok(key) = env.get_string(&key) {
|
||||||
|
let key: String = key.into();
|
||||||
|
super::get_builtin_option(&key)
|
||||||
|
} else {
|
||||||
|
"".into()
|
||||||
|
};
|
||||||
|
return env.new_string(res).unwrap_or_default().into_raw();
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "system" fn Java_ffi_FFI_isServiceClipboardEnabled(
|
pub unsafe extern "system" fn Java_ffi_FFI_isServiceClipboardEnabled(
|
||||||
env: JNIEnv,
|
env: JNIEnv,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use reqwest::blocking::Response;
|
use hbb_common::ResultType;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
|
|
||||||
@@ -21,11 +21,9 @@ pub enum HbbHttpResponse<T> {
|
|||||||
Data(T),
|
Data(T),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: DeserializeOwned> TryFrom<Response> for HbbHttpResponse<T> {
|
impl<T: DeserializeOwned> HbbHttpResponse<T> {
|
||||||
type Error = reqwest::Error;
|
pub fn parse(body: &str) -> ResultType<Self> {
|
||||||
|
let map = serde_json::from_str::<Map<String, Value>>(body)?;
|
||||||
fn try_from(resp: Response) -> Result<Self, <Self as TryFrom<Response>>::Error> {
|
|
||||||
let map = resp.json::<Map<String, Value>>()?;
|
|
||||||
if let Some(error) = map.get("error") {
|
if let Some(error) = map.get("error") {
|
||||||
if let Some(err) = error.as_str() {
|
if let Some(err) = error.as_str() {
|
||||||
Ok(Self::Error(err.to_owned()))
|
Ok(Self::Error(err.to_owned()))
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use super::HbbHttpResponse;
|
use super::HbbHttpResponse;
|
||||||
use crate::hbbs_http::create_http_client_with_url;
|
use crate::hbbs_http::create_http_client_with_url;
|
||||||
use hbb_common::{config::LocalConfig, log, ResultType};
|
use hbb_common::{config::LocalConfig, log, ResultType};
|
||||||
use reqwest::blocking::Client;
|
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -17,6 +16,7 @@ lazy_static::lazy_static! {
|
|||||||
|
|
||||||
const QUERY_INTERVAL_SECS: f32 = 1.0;
|
const QUERY_INTERVAL_SECS: f32 = 1.0;
|
||||||
const QUERY_TIMEOUT_SECS: u64 = 60 * 3;
|
const QUERY_TIMEOUT_SECS: u64 = 60 * 3;
|
||||||
|
|
||||||
const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth";
|
const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth";
|
||||||
const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth";
|
const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth";
|
||||||
const LOGIN_ACCOUNT_AUTH: &str = "Login account auth";
|
const LOGIN_ACCOUNT_AUTH: &str = "Login account auth";
|
||||||
@@ -80,6 +80,10 @@ pub enum UserStatus {
|
|||||||
pub struct UserPayload {
|
pub struct UserPayload {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub note: Option<String>,
|
pub note: Option<String>,
|
||||||
@@ -104,7 +108,7 @@ pub struct AuthBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct OidcSession {
|
pub struct OidcSession {
|
||||||
client: Option<Client>,
|
warmed_api_server: Option<String>,
|
||||||
state_msg: &'static str,
|
state_msg: &'static str,
|
||||||
failed_msg: String,
|
failed_msg: String,
|
||||||
code_url: Option<OidcAuthUrl>,
|
code_url: Option<OidcAuthUrl>,
|
||||||
@@ -131,7 +135,7 @@ impl Default for UserStatus {
|
|||||||
impl OidcSession {
|
impl OidcSession {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
client: None,
|
warmed_api_server: None,
|
||||||
state_msg: REQUESTING_ACCOUNT_AUTH,
|
state_msg: REQUESTING_ACCOUNT_AUTH,
|
||||||
failed_msg: "".to_owned(),
|
failed_msg: "".to_owned(),
|
||||||
code_url: None,
|
code_url: None,
|
||||||
@@ -144,12 +148,13 @@ impl OidcSession {
|
|||||||
|
|
||||||
fn ensure_client(api_server: &str) {
|
fn ensure_client(api_server: &str) {
|
||||||
let mut write_guard = OIDC_SESSION.write().unwrap();
|
let mut write_guard = OIDC_SESSION.write().unwrap();
|
||||||
if write_guard.client.is_none() {
|
if write_guard.warmed_api_server.as_deref() == Some(api_server) {
|
||||||
// This URL is used to detect the appropriate TLS implementation for the server.
|
return;
|
||||||
let login_option_url = format!("{}/api/login-options", &api_server);
|
|
||||||
let client = create_http_client_with_url(&login_option_url);
|
|
||||||
write_guard.client = Some(client);
|
|
||||||
}
|
}
|
||||||
|
// This URL is used to detect the appropriate TLS implementation for the server.
|
||||||
|
let login_option_url = format!("{}/api/login-options", api_server);
|
||||||
|
let _ = create_http_client_with_url(&login_option_url);
|
||||||
|
write_guard.warmed_api_server = Some(api_server.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auth(
|
fn auth(
|
||||||
@@ -159,26 +164,15 @@ impl OidcSession {
|
|||||||
uuid: &str,
|
uuid: &str,
|
||||||
) -> ResultType<HbbHttpResponse<OidcAuthUrl>> {
|
) -> ResultType<HbbHttpResponse<OidcAuthUrl>> {
|
||||||
Self::ensure_client(api_server);
|
Self::ensure_client(api_server);
|
||||||
let resp = if let Some(client) = &OIDC_SESSION.read().unwrap().client {
|
let body = serde_json::json!({
|
||||||
client
|
"op": op,
|
||||||
.post(format!("{}/api/oidc/auth", api_server))
|
"id": id,
|
||||||
.json(&serde_json::json!({
|
"uuid": uuid,
|
||||||
"op": op,
|
"deviceInfo": crate::ui_interface::get_login_device_info(),
|
||||||
"id": id,
|
})
|
||||||
"uuid": uuid,
|
.to_string();
|
||||||
"deviceInfo": crate::ui_interface::get_login_device_info(),
|
let resp = crate::post_request_sync(format!("{}/api/oidc/auth", api_server), body, "")?;
|
||||||
}))
|
HbbHttpResponse::parse(&resp)
|
||||||
.send()?
|
|
||||||
} else {
|
|
||||||
hbb_common::bail!("http client not initialized");
|
|
||||||
};
|
|
||||||
let status = resp.status();
|
|
||||||
match resp.try_into() {
|
|
||||||
Ok(v) => Ok(v),
|
|
||||||
Err(err) => {
|
|
||||||
hbb_common::bail!("Http status: {}, err: {}", status, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query(
|
fn query(
|
||||||
@@ -192,11 +186,19 @@ impl OidcSession {
|
|||||||
&[("code", code), ("id", id), ("uuid", uuid)],
|
&[("code", code), ("id", id), ("uuid", uuid)],
|
||||||
)?;
|
)?;
|
||||||
Self::ensure_client(api_server);
|
Self::ensure_client(api_server);
|
||||||
if let Some(client) = &OIDC_SESSION.read().unwrap().client {
|
#[derive(Deserialize)]
|
||||||
Ok(client.get(url).send()?.try_into()?)
|
struct HttpResponseBody {
|
||||||
} else {
|
body: String,
|
||||||
hbb_common::bail!("http client not initialized")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let resp = crate::http_request_sync(
|
||||||
|
url.to_string(),
|
||||||
|
"GET".to_owned(),
|
||||||
|
None,
|
||||||
|
"{}".to_owned(),
|
||||||
|
)?;
|
||||||
|
let resp = serde_json::from_str::<HttpResponseBody>(&resp)?;
|
||||||
|
HbbHttpResponse::parse(&resp.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(&mut self) {
|
fn reset(&mut self) {
|
||||||
@@ -268,7 +270,13 @@ impl OidcSession {
|
|||||||
);
|
);
|
||||||
LocalConfig::set_option(
|
LocalConfig::set_option(
|
||||||
"user_info".to_owned(),
|
"user_info".to_owned(),
|
||||||
serde_json::json!({ "name": auth_body.user.name, "status": auth_body.user.status }).to_string(),
|
serde_json::json!({
|
||||||
|
"name": auth_body.user.name,
|
||||||
|
"display_name": auth_body.user.display_name,
|
||||||
|
"avatar": auth_body.user.avatar,
|
||||||
|
"status": auth_body.user.status
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,8 +53,25 @@ pub fn download_file(
|
|||||||
auto_del_dur: Option<Duration>,
|
auto_del_dur: Option<Duration>,
|
||||||
) -> ResultType<String> {
|
) -> ResultType<String> {
|
||||||
let id = url.clone();
|
let id = url.clone();
|
||||||
if DOWNLOADERS.lock().unwrap().contains_key(&id) {
|
// First pass: if a non-error downloader exists for this URL, reuse it.
|
||||||
return Ok(id);
|
// If an errored downloader exists, remove it so this call can retry.
|
||||||
|
let mut stale_path = None;
|
||||||
|
{
|
||||||
|
let mut downloaders = DOWNLOADERS.lock().unwrap();
|
||||||
|
if let Some(downloader) = downloaders.get(&id) {
|
||||||
|
if downloader.error.is_none() {
|
||||||
|
return Ok(id);
|
||||||
|
}
|
||||||
|
stale_path = downloader.path.clone();
|
||||||
|
downloaders.remove(&id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(p) = stale_path {
|
||||||
|
if p.exists() {
|
||||||
|
if let Err(e) = std::fs::remove_file(&p) {
|
||||||
|
log::warn!("Failed to remove stale download file {}: {}", p.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(path) = path.as_ref() {
|
if let Some(path) = path.as_ref() {
|
||||||
@@ -75,8 +92,26 @@ pub fn download_file(
|
|||||||
tx_cancel: tx,
|
tx_cancel: tx,
|
||||||
finished: false,
|
finished: false,
|
||||||
};
|
};
|
||||||
let mut downloaders = DOWNLOADERS.lock().unwrap();
|
// Second pass (atomic with insert) to avoid race with another concurrent caller.
|
||||||
downloaders.insert(id.clone(), downloader);
|
let mut stale_path_after_check = None;
|
||||||
|
{
|
||||||
|
let mut downloaders = DOWNLOADERS.lock().unwrap();
|
||||||
|
if let Some(existing) = downloaders.get(&id) {
|
||||||
|
if existing.error.is_none() {
|
||||||
|
return Ok(id);
|
||||||
|
}
|
||||||
|
stale_path_after_check = existing.path.clone();
|
||||||
|
downloaders.remove(&id);
|
||||||
|
}
|
||||||
|
downloaders.insert(id.clone(), downloader);
|
||||||
|
}
|
||||||
|
if let Some(p) = stale_path_after_check {
|
||||||
|
if p.exists() {
|
||||||
|
if let Err(e) = std::fs::remove_file(&p) {
|
||||||
|
log::warn!("Failed to remove stale download file {}: {}", p.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let id2 = id.clone();
|
let id2 = id.clone();
|
||||||
std::thread::spawn(
|
std::thread::spawn(
|
||||||
|
|||||||
@@ -286,10 +286,14 @@ fn heartbeat_url() -> String {
|
|||||||
|
|
||||||
fn handle_config_options(config_options: HashMap<String, String>) {
|
fn handle_config_options(config_options: HashMap<String, String>) {
|
||||||
let mut options = Config::get_options();
|
let mut options = Config::get_options();
|
||||||
|
let default_settings = config::DEFAULT_SETTINGS.read().unwrap().clone();
|
||||||
config_options
|
config_options
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| {
|
.map(|(k, v)| {
|
||||||
if v.is_empty() {
|
// Priority: user config > default advanced options.
|
||||||
|
// Only when default advanced options are also empty, remove user option (fallback to built-in default);
|
||||||
|
// otherwise insert an empty value so user config remains present.
|
||||||
|
if v.is_empty() && default_settings.get(k).map_or("", |v| v).is_empty() {
|
||||||
options.remove(k);
|
options.remove(k);
|
||||||
} else {
|
} else {
|
||||||
options.insert(k.to_string(), v.to_string());
|
options.insert(k.to_string(), v.to_string());
|
||||||
|
|||||||
139
src/ipc.rs
139
src/ipc.rs
@@ -226,6 +226,7 @@ pub enum Data {
|
|||||||
is_terminal: bool,
|
is_terminal: bool,
|
||||||
peer_id: String,
|
peer_id: String,
|
||||||
name: String,
|
name: String,
|
||||||
|
avatar: String,
|
||||||
authorized: bool,
|
authorized: bool,
|
||||||
port_forward: String,
|
port_forward: String,
|
||||||
keyboard: bool,
|
keyboard: bool,
|
||||||
@@ -236,6 +237,7 @@ pub enum Data {
|
|||||||
restart: bool,
|
restart: bool,
|
||||||
recording: bool,
|
recording: bool,
|
||||||
block_input: bool,
|
block_input: bool,
|
||||||
|
privacy_mode: bool,
|
||||||
from_switch: bool,
|
from_switch: bool,
|
||||||
},
|
},
|
||||||
ChatMessage {
|
ChatMessage {
|
||||||
@@ -631,8 +633,29 @@ async fn handle(data: Data, stream: &mut Connection) {
|
|||||||
value = Some(Config::get_id());
|
value = Some(Config::get_id());
|
||||||
} else if name == "temporary-password" {
|
} else if name == "temporary-password" {
|
||||||
value = Some(password::temporary_password());
|
value = Some(password::temporary_password());
|
||||||
} else if name == "permanent-password" {
|
} else if name == "permanent-password-storage-and-salt" {
|
||||||
value = Some(Config::get_permanent_password());
|
let (storage, salt) = Config::get_local_permanent_password_storage_and_salt();
|
||||||
|
value = Some(storage + "\n" + &salt);
|
||||||
|
} else if name == "permanent-password-set" {
|
||||||
|
value = Some(if Config::has_permanent_password() {
|
||||||
|
"Y".to_owned()
|
||||||
|
} else {
|
||||||
|
"N".to_owned()
|
||||||
|
});
|
||||||
|
} else if name == "permanent-password-is-preset" {
|
||||||
|
let hard = config::HARD_SETTINGS
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get("password")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let is_preset =
|
||||||
|
!hard.is_empty() && Config::matches_permanent_password_plain(&hard);
|
||||||
|
value = Some(if is_preset {
|
||||||
|
"Y".to_owned()
|
||||||
|
} else {
|
||||||
|
"N".to_owned()
|
||||||
|
});
|
||||||
} else if name == "salt" {
|
} else if name == "salt" {
|
||||||
value = Some(Config::get_salt());
|
value = Some(Config::get_salt());
|
||||||
} else if name == "rendezvous_server" {
|
} else if name == "rendezvous_server" {
|
||||||
@@ -668,13 +691,24 @@ async fn handle(data: Data, stream: &mut Connection) {
|
|||||||
allow_err!(stream.send(&Data::Config((name, value))).await);
|
allow_err!(stream.send(&Data::Config((name, value))).await);
|
||||||
}
|
}
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
|
let mut updated = true;
|
||||||
if name == "id" {
|
if name == "id" {
|
||||||
Config::set_key_confirmed(false);
|
Config::set_key_confirmed(false);
|
||||||
Config::set_id(&value);
|
Config::set_id(&value);
|
||||||
} else if name == "temporary-password" {
|
} else if name == "temporary-password" {
|
||||||
password::update_temporary_password();
|
password::update_temporary_password();
|
||||||
} else if name == "permanent-password" {
|
} else if name == "permanent-password" {
|
||||||
Config::set_permanent_password(&value);
|
if Config::is_disable_change_permanent_password() {
|
||||||
|
log::warn!("Changing permanent password is disabled");
|
||||||
|
updated = false;
|
||||||
|
} else {
|
||||||
|
Config::set_permanent_password(&value);
|
||||||
|
}
|
||||||
|
// Explicitly ACK/NACK permanent-password writes. This allows UIs/FFI to
|
||||||
|
// distinguish "accepted by daemon" vs "IPC send succeeded" without
|
||||||
|
// reading back any secret.
|
||||||
|
let ack = if updated { "Y" } else { "N" }.to_owned();
|
||||||
|
allow_err!(stream.send(&Data::Config((name.clone(), Some(ack)))).await);
|
||||||
} else if name == "salt" {
|
} else if name == "salt" {
|
||||||
Config::set_salt(&value);
|
Config::set_salt(&value);
|
||||||
} else if name == "voice-call-input" {
|
} else if name == "voice-call-input" {
|
||||||
@@ -684,7 +718,9 @@ async fn handle(data: Data, stream: &mut Connection) {
|
|||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log::info!("{} updated", name);
|
if updated {
|
||||||
|
log::info!("{} updated", name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Data::Options(value) => match value {
|
Data::Options(value) => match value {
|
||||||
@@ -1142,13 +1178,57 @@ pub fn update_temporary_password() -> ResultType<()> {
|
|||||||
set_config("temporary-password", "".to_owned())
|
set_config("temporary-password", "".to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_permanent_password() -> String {
|
fn apply_permanent_password_storage_and_salt_payload(payload: Option<&str>) -> ResultType<()> {
|
||||||
if let Ok(Some(v)) = get_config("permanent-password") {
|
let Some(payload) = payload else {
|
||||||
Config::set_permanent_password(&v);
|
return Ok(());
|
||||||
v
|
};
|
||||||
} else {
|
let Some((storage, salt)) = payload.split_once('\n') else {
|
||||||
Config::get_permanent_password()
|
bail!("Invalid permanent-password-storage-and-salt payload");
|
||||||
|
};
|
||||||
|
|
||||||
|
if storage.is_empty() {
|
||||||
|
Config::set_permanent_password_storage_for_sync("", "")?;
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Config::set_permanent_password_storage_for_sync(storage, salt)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync_permanent_password_storage_from_daemon() -> ResultType<()> {
|
||||||
|
let v = get_config("permanent-password-storage-and-salt")?;
|
||||||
|
apply_permanent_password_storage_and_salt_payload(v.as_deref())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync_permanent_password_storage_from_daemon_async() -> ResultType<()> {
|
||||||
|
let ms_timeout = 1_000;
|
||||||
|
let v = get_config_async("permanent-password-storage-and-salt", ms_timeout).await?;
|
||||||
|
apply_permanent_password_storage_and_salt_payload(v.as_deref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_permanent_password_set() -> bool {
|
||||||
|
match get_config("permanent-password-set") {
|
||||||
|
Ok(Some(v)) => {
|
||||||
|
let v = v.trim();
|
||||||
|
return v == "Y";
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// No response/value (timeout).
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Connection error.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::warn!("Failed to query permanent password state from daemon");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_permanent_password_preset() -> bool {
|
||||||
|
if let Ok(Some(v)) = get_config("permanent-password-is-preset") {
|
||||||
|
let v = v.trim();
|
||||||
|
return v == "Y";
|
||||||
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_fingerprint() -> String {
|
pub fn get_fingerprint() -> String {
|
||||||
@@ -1158,8 +1238,41 @@ pub fn get_fingerprint() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_permanent_password(v: String) -> ResultType<()> {
|
pub fn set_permanent_password(v: String) -> ResultType<()> {
|
||||||
Config::set_permanent_password(&v);
|
if Config::is_disable_change_permanent_password() {
|
||||||
set_config("permanent-password", v)
|
bail!("Changing permanent password is disabled");
|
||||||
|
}
|
||||||
|
if set_permanent_password_with_ack(v)? {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
bail!("Changing permanent password was rejected by daemon");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
pub async fn set_permanent_password_with_ack(v: String) -> ResultType<bool> {
|
||||||
|
set_permanent_password_with_ack_async(v).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_permanent_password_with_ack_async(v: String) -> ResultType<bool> {
|
||||||
|
// The daemon ACK/NACK is expected quickly since it applies the config in-process.
|
||||||
|
let ms_timeout = 1_000;
|
||||||
|
let mut c = connect(ms_timeout, "").await?;
|
||||||
|
c.send_config("permanent-password", v).await?;
|
||||||
|
if let Some(Data::Config((name2, Some(v)))) = c.next_timeout(ms_timeout).await? {
|
||||||
|
if name2 == "permanent-password" {
|
||||||
|
let v = v.trim();
|
||||||
|
let ok = v == "Y";
|
||||||
|
if ok {
|
||||||
|
// Ensure the hashed permanent password storage is written to the user config file.
|
||||||
|
// This sync must not affect the daemon ACK outcome.
|
||||||
|
if let Err(err) = sync_permanent_password_storage_from_daemon_async().await {
|
||||||
|
log::warn!("Failed to sync permanent password storage from daemon: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(ok);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
@@ -1583,6 +1696,6 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn verify_ffi_enum_data_size() {
|
fn verify_ffi_enum_data_size() {
|
||||||
println!("{}", std::mem::size_of::<Data>());
|
println!("{}", std::mem::size_of::<Data>());
|
||||||
assert!(std::mem::size_of::<Data>() <= 96);
|
assert!(std::mem::size_of::<Data>() <= 120);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
223
src/keyboard.rs
223
src/keyboard.rs
@@ -82,8 +82,67 @@ lazy_static::lazy_static! {
|
|||||||
pub mod client {
|
pub mod client {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// Tracks grab ownership and serializes transitions across threads.
|
||||||
|
///
|
||||||
|
/// Multiple Flutter isolates (one per session window) call
|
||||||
|
/// `change_grab_status(Run/Wait)` concurrently. Without serialization a
|
||||||
|
/// stale `Wait` from session A can clobber session B's freshly acquired
|
||||||
|
/// grab on any desktop OS.
|
||||||
|
///
|
||||||
|
/// Windows and macOS are less susceptible in practice because the Flutter
|
||||||
|
/// side triggers `enterView` only after a mouse click inside the window,
|
||||||
|
/// but we cannot rely on that. On Linux/X11, `XGrabKeyboard` can also
|
||||||
|
/// cause a focus-change feedback loop (~10 Hz), so `last_grab` debounces
|
||||||
|
/// spurious `Wait` events that arrive shortly after a `Run`.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct GrabOwnerState {
|
||||||
|
owner: Option<u128>,
|
||||||
|
last_grab: Option<std::time::Instant>,
|
||||||
|
/// True while a deferred-release thread is in flight. Prevents
|
||||||
|
/// spawning redundant threads during the X11 feedback loop.
|
||||||
|
deferred_pending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How long after a grab acquisition we suppress Wait from the same session.
|
||||||
|
/// Must exceed one full X11 feedback cycle (~100 ms: 50 ms enable + 50 ms disable).
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
const GRAB_DEBOUNCE_MS: u128 = 300;
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref IS_GRAB_STARTED: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
|
static ref IS_GRAB_STARTED: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
|
||||||
|
static ref GRAB_STATE: Arc<Mutex<GrabOwnerState>> = Arc::new(Mutex::new(GrabOwnerState::default()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref GRAB_OP_LOCK: Mutex<()> = Mutex::new(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn apply_run_grab_if_owner(session_id: u128, disable_first: bool) {
|
||||||
|
let _lock = GRAB_OP_LOCK.lock().unwrap();
|
||||||
|
let gs = GRAB_STATE.lock().unwrap();
|
||||||
|
if gs.owner != Some(session_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(gs);
|
||||||
|
if disable_first {
|
||||||
|
log::debug!("[grab] handoff: disable_grab before re-grab");
|
||||||
|
rdev::disable_grab();
|
||||||
|
}
|
||||||
|
rdev::enable_grab();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn disable_grab_if_released() {
|
||||||
|
let _lock = GRAB_OP_LOCK.lock().unwrap();
|
||||||
|
let should_disable = {
|
||||||
|
let gs = GRAB_STATE.lock().unwrap();
|
||||||
|
gs.owner.is_none() && gs.last_grab.is_none()
|
||||||
|
};
|
||||||
|
if should_disable {
|
||||||
|
rdev::disable_grab();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_grab_loop() {
|
pub fn start_grab_loop() {
|
||||||
@@ -96,36 +155,167 @@ pub mod client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn change_grab_status(state: GrabState, keyboard_mode: &str) {
|
pub fn change_grab_status(state: GrabState, keyboard_mode: &str, session_id: u128) {
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
if !IS_RDEV_ENABLED.load(Ordering::SeqCst) {
|
if !IS_RDEV_ENABLED.load(Ordering::SeqCst) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Serialize transitions so a stale `Wait` from a previous owner cannot
|
||||||
|
// clobber a fresh `Run` from a different session window.
|
||||||
|
let mut release_after_unlock = None;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let mut run_grab_after_unlock = None;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let mut disable_after_unlock = false;
|
||||||
|
let mut gs = GRAB_STATE.lock().unwrap();
|
||||||
match state {
|
match state {
|
||||||
GrabState::Ready => {}
|
GrabState::Ready => {}
|
||||||
GrabState::Run => {
|
GrabState::Run => {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
update_grab_get_key_name(keyboard_mode);
|
update_grab_get_key_name(keyboard_mode);
|
||||||
|
|
||||||
|
// Idempotent: if this session already owns the grab, just
|
||||||
|
// refresh the debounce timer (proves the session is still
|
||||||
|
// actively focused) and skip the actual grab call.
|
||||||
|
if gs.owner == Some(session_id) {
|
||||||
|
gs.last_grab = Some(std::time::Instant::now());
|
||||||
|
// Reset so the next Wait can spawn a fresh deferred-release
|
||||||
|
// timer with an up-to-date snapshot of last_grab.
|
||||||
|
gs.deferred_pending = false;
|
||||||
|
log::debug!(
|
||||||
|
"[grab] Run(0x{:x}): already owner, refresh debounce",
|
||||||
|
session_id
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!(
|
||||||
|
"[grab] Run(0x{:x}): prev_owner={}, mode={}",
|
||||||
|
session_id,
|
||||||
|
gs.owner
|
||||||
|
.map_or("none".to_string(), |id| format!("0x{:x}", id)),
|
||||||
|
keyboard_mode,
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||||
KEYBOARD_HOOKED.swap(true, Ordering::SeqCst);
|
KEYBOARD_HOOKED.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
rdev::enable_grab();
|
let had_owner = gs.owner.is_some();
|
||||||
|
gs.owner = Some(session_id);
|
||||||
|
gs.last_grab = Some(std::time::Instant::now());
|
||||||
|
// Invalidate any in-flight deferred release from the previous
|
||||||
|
// owner so it cannot suppress a fresh timer for the new owner.
|
||||||
|
gs.deferred_pending = false;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
run_grab_after_unlock = Some(had_owner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
GrabState::Wait => {
|
GrabState::Wait => {
|
||||||
|
// Drop stale `Wait` events that do not correspond to the
|
||||||
|
// current grab owner. This prevents a late PointerExit from
|
||||||
|
// session A from releasing session B's freshly acquired grab.
|
||||||
|
if gs.owner != Some(session_id) {
|
||||||
|
log::debug!(
|
||||||
|
"[grab] Wait(0x{:x}): ignored, owner={}",
|
||||||
|
session_id,
|
||||||
|
gs.owner
|
||||||
|
.map_or("none".to_string(), |id| format!("0x{:x}", id)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce: on Linux/X11, XGrabKeyboard causes a focus-change
|
||||||
|
// feedback loop (grab -> PointerExit -> ungrab -> PointerEnter ->
|
||||||
|
// grab -> ...). Suppress Wait if the grab was acquired recently
|
||||||
|
// by this same session -- it is X11 feedback, not a real leave.
|
||||||
|
// A deferred release is scheduled so that a genuine leave within
|
||||||
|
// the debounce window is not permanently lost.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if let Some(t) = gs.last_grab {
|
||||||
|
let elapsed = t.elapsed().as_millis();
|
||||||
|
if elapsed < GRAB_DEBOUNCE_MS {
|
||||||
|
if !gs.deferred_pending {
|
||||||
|
log::debug!(
|
||||||
|
"[grab] Wait(0x{:x}): debounced ({}ms < {}ms), scheduling deferred release",
|
||||||
|
session_id, elapsed, GRAB_DEBOUNCE_MS,
|
||||||
|
);
|
||||||
|
gs.deferred_pending = true;
|
||||||
|
let remaining = (GRAB_DEBOUNCE_MS - elapsed) as u64 + 50;
|
||||||
|
let snapshot = gs.last_grab;
|
||||||
|
let mode = keyboard_mode.to_string();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(remaining));
|
||||||
|
let release_keys = {
|
||||||
|
let mut gs = GRAB_STATE.lock().unwrap();
|
||||||
|
// Release only if no new Run has refreshed the grab since.
|
||||||
|
if gs.owner == Some(session_id) && gs.last_grab == snapshot {
|
||||||
|
let to_release = take_remote_keys();
|
||||||
|
gs.deferred_pending = false;
|
||||||
|
log::debug!(
|
||||||
|
"[grab] Wait(0x{:x}): deferred release",
|
||||||
|
session_id
|
||||||
|
);
|
||||||
|
KEYBOARD_HOOKED.store(false, Ordering::SeqCst);
|
||||||
|
gs.owner = None;
|
||||||
|
gs.last_grab = None;
|
||||||
|
Some(to_release)
|
||||||
|
} else {
|
||||||
|
log::debug!(
|
||||||
|
"[grab] Wait(0x{:x}): deferred release cancelled (grab refreshed)",
|
||||||
|
session_id,
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(to_release) = release_keys {
|
||||||
|
disable_grab_if_released();
|
||||||
|
release_remote_keys_for_events(&mode, to_release);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log::debug!(
|
||||||
|
"[grab] Wait(0x{:x}): debounced, deferred release already pending",
|
||||||
|
session_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("[grab] Wait(0x{:x}): releasing grab", session_id);
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
rdev::set_get_key_unicode(false);
|
rdev::set_get_key_unicode(false);
|
||||||
|
|
||||||
release_remote_keys(keyboard_mode);
|
|
||||||
|
|
||||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||||
KEYBOARD_HOOKED.swap(false, Ordering::SeqCst);
|
KEYBOARD_HOOKED.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
|
gs.owner = None;
|
||||||
|
gs.last_grab = None;
|
||||||
|
gs.deferred_pending = false;
|
||||||
|
release_after_unlock = Some(take_remote_keys());
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
rdev::disable_grab();
|
{
|
||||||
|
disable_after_unlock = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
GrabState::Exit => {}
|
GrabState::Exit => {}
|
||||||
}
|
}
|
||||||
|
drop(gs);
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
if disable_after_unlock {
|
||||||
|
disable_grab_if_released();
|
||||||
|
}
|
||||||
|
if let Some(disable_first) = run_grab_after_unlock {
|
||||||
|
apply_run_grab_if_owner(session_id, disable_first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(to_release) = release_after_unlock {
|
||||||
|
release_remote_keys_for_events(keyboard_mode, to_release);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_event(keyboard_mode: &str, event: &Event, lock_modes: Option<i32>) {
|
pub fn process_event(keyboard_mode: &str, event: &Event, lock_modes: Option<i32>) {
|
||||||
@@ -341,7 +531,6 @@ fn notify_exit_relative_mouse_mode() {
|
|||||||
flutter::push_session_event(&session_id, "exit_relative_mouse_mode", vec![]);
|
flutter::push_session_event(&session_id, "exit_relative_mouse_mode", vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Handle relative mouse mode shortcuts in the rdev grab loop.
|
/// Handle relative mouse mode shortcuts in the rdev grab loop.
|
||||||
/// Returns true if the event should be blocked from being sent to the peer.
|
/// Returns true if the event should be blocked from being sent to the peer.
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
@@ -540,10 +729,12 @@ pub fn is_long_press(event: &Event) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn release_remote_keys(keyboard_mode: &str) {
|
fn take_remote_keys() -> HashMap<Key, Event> {
|
||||||
// todo!: client quit suddenly, how to release keys?
|
let mut to_release = TO_RELEASE.lock().unwrap();
|
||||||
let to_release = TO_RELEASE.lock().unwrap().clone();
|
std::mem::take(&mut *to_release)
|
||||||
TO_RELEASE.lock().unwrap().clear();
|
}
|
||||||
|
|
||||||
|
fn release_remote_keys_for_events(keyboard_mode: &str, to_release: HashMap<Key, Event>) {
|
||||||
for (key, mut event) in to_release.into_iter() {
|
for (key, mut event) in to_release.into_iter() {
|
||||||
event.event_type = EventType::KeyRelease(key);
|
event.event_type = EventType::KeyRelease(key);
|
||||||
client::process_event(keyboard_mode, &event, None);
|
client::process_event(keyboard_mode, &event, None);
|
||||||
@@ -558,6 +749,12 @@ pub fn release_remote_keys(keyboard_mode: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn release_remote_keys(keyboard_mode: &str) {
|
||||||
|
// todo!: client quit suddenly, how to release keys?
|
||||||
|
release_remote_keys_for_events(keyboard_mode, take_remote_keys());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_keyboard_mode_enum(keyboard_mode: &str) -> KeyboardMode {
|
pub fn get_keyboard_mode_enum(keyboard_mode: &str) -> KeyboardMode {
|
||||||
match keyboard_mode {
|
match keyboard_mode {
|
||||||
"map" => KeyboardMode::Map,
|
"map" => KeyboardMode::Map,
|
||||||
@@ -748,7 +945,6 @@ pub fn event_to_key_events(
|
|||||||
) -> Vec<KeyEvent> {
|
) -> Vec<KeyEvent> {
|
||||||
peer.retain(|c| !c.is_whitespace());
|
peer.retain(|c| !c.is_whitespace());
|
||||||
|
|
||||||
let mut key_event = KeyEvent::new();
|
|
||||||
update_modifiers_state(event);
|
update_modifiers_state(event);
|
||||||
|
|
||||||
match event.event_type {
|
match event.event_type {
|
||||||
@@ -761,6 +957,7 @@ pub fn event_to_key_events(
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut key_event = KeyEvent::new();
|
||||||
key_event.mode = keyboard_mode.into();
|
key_event.mode = keyboard_mode.into();
|
||||||
|
|
||||||
let mut key_events = match keyboard_mode {
|
let mut key_events = match keyboard_mode {
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ mod es;
|
|||||||
mod et;
|
mod et;
|
||||||
mod eu;
|
mod eu;
|
||||||
mod fa;
|
mod fa;
|
||||||
|
mod gu;
|
||||||
mod fr;
|
mod fr;
|
||||||
mod he;
|
mod he;
|
||||||
|
mod hi;
|
||||||
mod hr;
|
mod hr;
|
||||||
mod hu;
|
mod hu;
|
||||||
mod id;
|
mod id;
|
||||||
@@ -47,6 +49,7 @@ mod vi;
|
|||||||
mod ta;
|
mod ta;
|
||||||
mod ge;
|
mod ge;
|
||||||
mod fi;
|
mod fi;
|
||||||
|
mod ml;
|
||||||
|
|
||||||
pub const LANGS: &[(&str, &str)] = &[
|
pub const LANGS: &[(&str, &str)] = &[
|
||||||
("en", "English"),
|
("en", "English"),
|
||||||
@@ -95,6 +98,9 @@ pub const LANGS: &[(&str, &str)] = &[
|
|||||||
("ta", "தமிழ்"),
|
("ta", "தமிழ்"),
|
||||||
("ge", "ქართული"),
|
("ge", "ქართული"),
|
||||||
("fi", "Suomi"),
|
("fi", "Suomi"),
|
||||||
|
("ml", "മലയാളം"),
|
||||||
|
("hi", "हिंदी"),
|
||||||
|
("gu", "ગુજરાતી"),
|
||||||
];
|
];
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
@@ -173,6 +179,9 @@ pub fn translate_locale(name: String, locale: &str) -> String {
|
|||||||
"sc" => sc::T.deref(),
|
"sc" => sc::T.deref(),
|
||||||
"ta" => ta::T.deref(),
|
"ta" => ta::T.deref(),
|
||||||
"ge" => ge::T.deref(),
|
"ge" => ge::T.deref(),
|
||||||
|
"ml" => ml::T.deref(),
|
||||||
|
"hi" => hi::T.deref(),
|
||||||
|
"gu" => gu::T.deref(),
|
||||||
_ => en::T.deref(),
|
_ => en::T.deref(),
|
||||||
};
|
};
|
||||||
let (name, placeholder_value) = extract_placeholder(&name);
|
let (name, placeholder_value) = extract_placeholder(&name);
|
||||||
|
|||||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Keyboard Settings", "اعدادات لوحة المفاتيح"),
|
("Keyboard Settings", "اعدادات لوحة المفاتيح"),
|
||||||
("Full Access", "وصول كامل"),
|
("Full Access", "وصول كامل"),
|
||||||
("Screen Share", "مشاركة الشاشة"),
|
("Screen Share", "مشاركة الشاشة"),
|
||||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland يتطلب نسخة ابونتو 21.04 او اعلى."),
|
("ubuntu-21-04-required", "Wayland يتطلب نسخة ابونتو 21.04 او اعلى."),
|
||||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland يتطلب نسخة اعلى من توزيعة لينكس. الرجاء تجربة سطح مكتب X11 او غير نظام تشغيلك."),
|
("wayland-requires-higher-linux-version", "Wayland يتطلب نسخة اعلى من توزيعة لينكس. الرجاء تجربة سطح مكتب X11 او غير نظام تشغيلك."),
|
||||||
|
("xdp-portal-unavailable", "لاقط شاشة Wayland فشل. بوابة سطح مكتب XDG ربما توقفت عن العمل او حدث خطأ بها. جرب اعادة تشغليها عن طريق 'systemctl --user restart xdg-desktop-portal'."),
|
||||||
("JumpLink", "رابط القفز"),
|
("JumpLink", "رابط القفز"),
|
||||||
("Please Select the screen to be shared(Operate on the peer side).", "الرجاء اختيار شاشة لمشاركتها (تعمل على جانب القرين)."),
|
("Please Select the screen to be shared(Operate on the peer side).", "الرجاء اختيار شاشة لمشاركتها (تعمل على جانب القرين)."),
|
||||||
("Show RustDesk", "عرض RustDesk"),
|
("Show RustDesk", "عرض RustDesk"),
|
||||||
@@ -728,16 +729,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("server-oss-not-support-tip", "هذه الميزة غير مدعومة من قبل خادمك"),
|
("server-oss-not-support-tip", "هذه الميزة غير مدعومة من قبل خادمك"),
|
||||||
("input note here", "أدخل الملاحظة هنا"),
|
("input note here", "أدخل الملاحظة هنا"),
|
||||||
("note-at-conn-end-tip", "سيتم عرض هذه الملاحظة عند نهاية الاتصال"),
|
("note-at-conn-end-tip", "سيتم عرض هذه الملاحظة عند نهاية الاتصال"),
|
||||||
("Show terminal extra keys", ""),
|
("Show terminal extra keys", "إظهار مفاتيح إضافية في الطرفية"),
|
||||||
("Relative mouse mode", ""),
|
("Relative mouse mode", "وضع الماوس النسبي"),
|
||||||
("rel-mouse-not-supported-peer-tip", ""),
|
("rel-mouse-not-supported-peer-tip", "وضع الماوس النسبي غير مدعوم على الجهاز الآخر"),
|
||||||
("rel-mouse-not-ready-tip", ""),
|
("rel-mouse-not-ready-tip", "وضع الماوس النسبي غير جاهز"),
|
||||||
("rel-mouse-lock-failed-tip", ""),
|
("rel-mouse-lock-failed-tip", "فشل قفل الماوس النسبي"),
|
||||||
("rel-mouse-exit-{}-tip", ""),
|
("rel-mouse-exit-{}-tip", "للخروج من وضع الماوس النسبي اضغط على {}"),
|
||||||
("rel-mouse-permission-lost-tip", ""),
|
("rel-mouse-permission-lost-tip", "تم فقدان إذن الماوس النسبي"),
|
||||||
("Changelog", ""),
|
("Changelog", "سجل التغييرات"),
|
||||||
("keep-awake-during-outgoing-sessions-label", ""),
|
("keep-awake-during-outgoing-sessions-label", "إبقاء الجهاز نشطًا أثناء الجلسات الصادرة"),
|
||||||
("keep-awake-during-incoming-sessions-label", ""),
|
("keep-awake-during-incoming-sessions-label", "إبقاء الجهاز نشطًا أثناء الجلسات الواردة"),
|
||||||
("Continue with {}", "متابعة مع {}"),
|
("Continue with {}", "متابعة مع {}"),
|
||||||
|
("Display Name", "اسم العرض"),
|
||||||
|
("password-hidden-tip", "كلمة المرور مخفية"),
|
||||||
|
("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
743
src/lang/be.rs
743
src/lang/be.rs
File diff suppressed because it is too large
Load Diff
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Keyboard Settings", "Настройки на клавиатурата"),
|
("Keyboard Settings", "Настройки на клавиатурата"),
|
||||||
("Full Access", "Пълен достъп"),
|
("Full Access", "Пълен достъп"),
|
||||||
("Screen Share", "Споделяне на екрана"),
|
("Screen Share", "Споделяне на екрана"),
|
||||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland изисква Ubuntu 21.04 или по-нов"),
|
("ubuntu-21-04-required", "Wayland изисква Ubuntu 21.04 или по-нов"),
|
||||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."),
|
("wayland-requires-higher-linux-version", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."),
|
||||||
|
("xdp-portal-unavailable", ""),
|
||||||
("JumpLink", "Препратка"),
|
("JumpLink", "Препратка"),
|
||||||
("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (спрямо отдалечената страна)."),
|
("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (спрямо отдалечената страна)."),
|
||||||
("Show RustDesk", "Покажи RustDesk"),
|
("Show RustDesk", "Покажи RustDesk"),
|
||||||
@@ -739,5 +740,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("keep-awake-during-outgoing-sessions-label", ""),
|
("keep-awake-during-outgoing-sessions-label", ""),
|
||||||
("keep-awake-during-incoming-sessions-label", ""),
|
("keep-awake-during-incoming-sessions-label", ""),
|
||||||
("Continue with {}", "Продължи с {}"),
|
("Continue with {}", "Продължи с {}"),
|
||||||
|
("Display Name", ""),
|
||||||
|
("password-hidden-tip", ""),
|
||||||
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Keyboard Settings", "Configuració del teclat"),
|
("Keyboard Settings", "Configuració del teclat"),
|
||||||
("Full Access", "Accés complet"),
|
("Full Access", "Accés complet"),
|
||||||
("Screen Share", "Compartició de pantalla"),
|
("Screen Share", "Compartició de pantalla"),
|
||||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o superior"),
|
("ubuntu-21-04-required", "Wayland requereix Ubuntu 21.04 o superior"),
|
||||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."),
|
("wayland-requires-higher-linux-version", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."),
|
||||||
|
("xdp-portal-unavailable", ""),
|
||||||
("JumpLink", "Marcador"),
|
("JumpLink", "Marcador"),
|
||||||
("Please Select the screen to be shared(Operate on the peer side).", "Seleccioneu la pantalla que compartireu (quina serà visible al client)"),
|
("Please Select the screen to be shared(Operate on the peer side).", "Seleccioneu la pantalla que compartireu (quina serà visible al client)"),
|
||||||
("Show RustDesk", "Mostra el RustDesk"),
|
("Show RustDesk", "Mostra el RustDesk"),
|
||||||
@@ -739,5 +740,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("keep-awake-during-outgoing-sessions-label", ""),
|
("keep-awake-during-outgoing-sessions-label", ""),
|
||||||
("keep-awake-during-incoming-sessions-label", ""),
|
("keep-awake-during-incoming-sessions-label", ""),
|
||||||
("Continue with {}", "Continua amb {}"),
|
("Continue with {}", "Continua amb {}"),
|
||||||
|
("Display Name", ""),
|
||||||
|
("password-hidden-tip", ""),
|
||||||
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Keyboard Settings", "键盘设置"),
|
("Keyboard Settings", "键盘设置"),
|
||||||
("Full Access", "完全访问"),
|
("Full Access", "完全访问"),
|
||||||
("Screen Share", "仅共享屏幕"),
|
("Screen Share", "仅共享屏幕"),
|
||||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"),
|
("ubuntu-21-04-required", "Wayland 需要 Ubuntu 21.04 或更高版本。"),
|
||||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"),
|
("wayland-requires-higher-linux-version", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"),
|
||||||
|
("xdp-portal-unavailable", ""),
|
||||||
("JumpLink", "查看"),
|
("JumpLink", "查看"),
|
||||||
("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"),
|
("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"),
|
||||||
("Show RustDesk", "显示 RustDesk"),
|
("Show RustDesk", "显示 RustDesk"),
|
||||||
@@ -736,8 +737,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("rel-mouse-exit-{}-tip", "按下 {} 退出"),
|
("rel-mouse-exit-{}-tip", "按下 {} 退出"),
|
||||||
("rel-mouse-permission-lost-tip", "键盘权限被撤销。相对鼠标模式已被禁用。"),
|
("rel-mouse-permission-lost-tip", "键盘权限被撤销。相对鼠标模式已被禁用。"),
|
||||||
("Changelog", "更新日志"),
|
("Changelog", "更新日志"),
|
||||||
("keep-awake-during-outgoing-sessions-label", ""),
|
("keep-awake-during-outgoing-sessions-label", "传出会话期间保持屏幕常亮"),
|
||||||
("keep-awake-during-incoming-sessions-label", ""),
|
("keep-awake-during-incoming-sessions-label", "传入会话期间保持屏幕常亮"),
|
||||||
("Continue with {}", "使用 {} 登录"),
|
("Continue with {}", "使用 {} 登录"),
|
||||||
|
("Display Name", "显示名称"),
|
||||||
|
("password-hidden-tip", "永久密码已设置(已隐藏)"),
|
||||||
|
("preset-password-in-use-tip", "当前使用预设密码"),
|
||||||
|
("Enable privacy mode", "允许隐私模式"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Keyboard Settings", "Nastavení klávesnice"),
|
("Keyboard Settings", "Nastavení klávesnice"),
|
||||||
("Full Access", "Úplný přístup"),
|
("Full Access", "Úplný přístup"),
|
||||||
("Screen Share", "Sdílení obrazovky"),
|
("Screen Share", "Sdílení obrazovky"),
|
||||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04, nebo vyšší verzi."),
|
("ubuntu-21-04-required", "Wayland vyžaduje Ubuntu 21.04, nebo vyšší verzi."),
|
||||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop, nebo změňte OS."),
|
("wayland-requires-higher-linux-version", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop, nebo změňte OS."),
|
||||||
|
("xdp-portal-unavailable", ""),
|
||||||
("JumpLink", "JumpLink"),
|
("JumpLink", "JumpLink"),
|
||||||
("Please Select the screen to be shared(Operate on the peer side).", "Vyberte prosím obrazovku, kterou chcete sdílet (Ovládejte na straně protistrany)."),
|
("Please Select the screen to be shared(Operate on the peer side).", "Vyberte prosím obrazovku, kterou chcete sdílet (Ovládejte na straně protistrany)."),
|
||||||
("Show RustDesk", "Zobrazit RustDesk"),
|
("Show RustDesk", "Zobrazit RustDesk"),
|
||||||
@@ -739,5 +740,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("keep-awake-during-outgoing-sessions-label", ""),
|
("keep-awake-during-outgoing-sessions-label", ""),
|
||||||
("keep-awake-during-incoming-sessions-label", ""),
|
("keep-awake-during-incoming-sessions-label", ""),
|
||||||
("Continue with {}", "Pokračovat s {}"),
|
("Continue with {}", "Pokračovat s {}"),
|
||||||
|
("Display Name", ""),
|
||||||
|
("password-hidden-tip", ""),
|
||||||
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Keyboard Settings", "Tastaturindstillinger"),
|
("Keyboard Settings", "Tastaturindstillinger"),
|
||||||
("Full Access", "Fuld adgang"),
|
("Full Access", "Fuld adgang"),
|
||||||
("Screen Share", "Skærmdeling"),
|
("Screen Share", "Skærmdeling"),
|
||||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kræver Ubuntu version 21.04 eller nyere."),
|
("ubuntu-21-04-required", "Wayland kræver Ubuntu version 21.04 eller nyere."),
|
||||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kræver en højere version af Linux distro. Prøv venligst X11 desktop eller skift dit OS."),
|
("wayland-requires-higher-linux-version", "Wayland kræver en højere version af Linux distro. Prøv venligst X11 desktop eller skift dit OS."),
|
||||||
|
("xdp-portal-unavailable", ""),
|
||||||
("JumpLink", "JumpLink"),
|
("JumpLink", "JumpLink"),
|
||||||
("Please Select the screen to be shared(Operate on the peer side).", "Vælg venligst den skærm, der skal deles (Betjen på modtagersiden)."),
|
("Please Select the screen to be shared(Operate on the peer side).", "Vælg venligst den skærm, der skal deles (Betjen på modtagersiden)."),
|
||||||
("Show RustDesk", "Vis RustDesk"),
|
("Show RustDesk", "Vis RustDesk"),
|
||||||
@@ -739,5 +740,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("keep-awake-during-outgoing-sessions-label", ""),
|
("keep-awake-during-outgoing-sessions-label", ""),
|
||||||
("keep-awake-during-incoming-sessions-label", ""),
|
("keep-awake-during-incoming-sessions-label", ""),
|
||||||
("Continue with {}", "Fortsæt med {}"),
|
("Continue with {}", "Fortsæt med {}"),
|
||||||
|
("Display Name", ""),
|
||||||
|
("password-hidden-tip", ""),
|
||||||
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,9 +377,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Keyboard Settings", "Tastatureinstellungen"),
|
("Keyboard Settings", "Tastatureinstellungen"),
|
||||||
("Full Access", "Vollzugriff"),
|
("Full Access", "Vollzugriff"),
|
||||||
("Screen Share", "Bildschirmfreigabe"),
|
("Screen Share", "Bildschirmfreigabe"),
|
||||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."),
|
("ubuntu-21-04-required", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."),
|
||||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."),
|
("wayland-requires-higher-linux-version", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."),
|
||||||
("JumpLink", "View"),
|
("xdp-portal-unavailable", "Die Bildschirmaufnahme mit Wayland ist fehlgeschlagen. Das XDG-Desktop-Portal ist möglicherweise abgestürzt oder nicht verfügbar. Versuchen Sie, es mit `systemctl --user restart xdg-desktop-portal` neu zu starten."),
|
||||||
|
("JumpLink", "Anzeigen"),
|
||||||
("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Gegenseite)."),
|
("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Gegenseite)."),
|
||||||
("Show RustDesk", "RustDesk anzeigen"),
|
("Show RustDesk", "RustDesk anzeigen"),
|
||||||
("This PC", "Dieser PC"),
|
("This PC", "Dieser PC"),
|
||||||
@@ -739,5 +740,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"),
|
("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"),
|
||||||
("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"),
|
("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"),
|
||||||
("Continue with {}", "Fortfahren mit {}"),
|
("Continue with {}", "Fortfahren mit {}"),
|
||||||
|
("Display Name", "Anzeigename"),
|
||||||
|
("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."),
|
||||||
|
("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
445
src/lang/el.rs
445
src/lang/el.rs
@@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
[
|
[
|
||||||
("Status", "Κατάσταση"),
|
("Status", "Κατάσταση"),
|
||||||
("Your Desktop", "Ο σταθμός εργασίας σας"),
|
("Your Desktop", "Ο σταθμός εργασίας σας"),
|
||||||
("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το αναγνωριστικό και τον κωδικό πρόσβασης."),
|
("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το ID και τον κωδικό πρόσβασης."),
|
||||||
("Password", "Κωδικός πρόσβασης"),
|
("Password", "Κωδικός πρόσβασης"),
|
||||||
("Ready", "Έτοιμο"),
|
("Ready", "Έτοιμο"),
|
||||||
("Established", "Συνδέθηκε"),
|
("Established", "Συνδέθηκε"),
|
||||||
@@ -19,16 +19,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Recent sessions", "Πρόσφατες συνεδρίες"),
|
("Recent sessions", "Πρόσφατες συνεδρίες"),
|
||||||
("Address book", "Βιβλίο διευθύνσεων"),
|
("Address book", "Βιβλίο διευθύνσεων"),
|
||||||
("Confirmation", "Επιβεβαίωση"),
|
("Confirmation", "Επιβεβαίωση"),
|
||||||
("TCP tunneling", "TCP tunneling"),
|
("TCP tunneling", "Σήραγγα TCP"),
|
||||||
("Remove", "Κατάργηση"),
|
("Remove", "Κατάργηση"),
|
||||||
("Refresh random password", "Νέος τυχαίος κωδικός πρόσβασης"),
|
("Refresh random password", "Ανανέωση τυχαίου κωδικού πρόσβασης"),
|
||||||
("Set your own password", "Ορίστε τον δικό σας κωδικό πρόσβασης"),
|
("Set your own password", "Ορίστε τον δικό σας κωδικό πρόσβασης"),
|
||||||
("Enable keyboard/mouse", "Ενεργοποίηση πληκτρολογίου/ποντικιού"),
|
("Enable keyboard/mouse", "Ενεργοποίηση πληκτρολογίου/ποντικιού"),
|
||||||
("Enable clipboard", "Ενεργοποίηση προχείρου"),
|
("Enable clipboard", "Ενεργοποίηση προχείρου"),
|
||||||
("Enable file transfer", "Ενεργοποίηση μεταφοράς αρχείων"),
|
("Enable file transfer", "Ενεργοποίηση μεταφοράς αρχείων"),
|
||||||
("Enable TCP tunneling", "Ενεργοποίηση TCP tunneling"),
|
("Enable TCP tunneling", "Ενεργοποίηση σήραγγας TCP"),
|
||||||
("IP Whitelisting", "Λίστα επιτρεπόμενων IP"),
|
("IP Whitelisting", "Λίστα επιτρεπόμενων IP"),
|
||||||
("ID/Relay Server", "Διακομιστής ID/Αναμετάδοσης"),
|
("ID/Relay Server", "ID/Διακομιστής Αναμετάδοσης"),
|
||||||
("Import server config", "Εισαγωγή διαμόρφωσης διακομιστή"),
|
("Import server config", "Εισαγωγή διαμόρφωσης διακομιστή"),
|
||||||
("Export Server Config", "Εξαγωγή διαμόρφωσης διακομιστή"),
|
("Export Server Config", "Εξαγωγή διαμόρφωσης διακομιστή"),
|
||||||
("Import server configuration successfully", "Επιτυχής εισαγωγή διαμόρφωσης διακομιστή"),
|
("Import server configuration successfully", "Επιτυχής εισαγωγή διαμόρφωσης διακομιστή"),
|
||||||
@@ -36,14 +36,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Invalid server configuration", "Μη έγκυρη διαμόρφωση διακομιστή"),
|
("Invalid server configuration", "Μη έγκυρη διαμόρφωση διακομιστή"),
|
||||||
("Clipboard is empty", "Το πρόχειρο είναι κενό"),
|
("Clipboard is empty", "Το πρόχειρο είναι κενό"),
|
||||||
("Stop service", "Διακοπή υπηρεσίας"),
|
("Stop service", "Διακοπή υπηρεσίας"),
|
||||||
("Change ID", "Αλλαγή αναγνωριστικού ID"),
|
("Change ID", "Αλλαγή του ID σας"),
|
||||||
("Your new ID", "Το νέο σας ID"),
|
("Your new ID", "Το νέο σας ID"),
|
||||||
("length %min% to %max%", "μέγεθος από %min% έως %max%"),
|
("length %min% to %max%", "μέγεθος από %min% έως %max%"),
|
||||||
("starts with a letter", "ξεκινά με γράμμα"),
|
("starts with a letter", "ξεκινά με γράμμα"),
|
||||||
("allowed characters", "επιτρεπόμενοι χαρακτήρες"),
|
("allowed characters", "επιτρεπόμενοι χαρακτήρες"),
|
||||||
("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9, - (παύλα) και _ (κάτω παύλα). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."),
|
("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9, - (παύλα) και _ (κάτω παύλα). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."),
|
||||||
("Website", "Ιστότοπος"),
|
("Website", "Ιστότοπος"),
|
||||||
("About", "Πληροφορίες"),
|
("About", "Σχετικά"),
|
||||||
("Slogan_tip", "Φτιαγμένο με πάθος - σε έναν κόσμο που βυθίζεται στο χάος!"),
|
("Slogan_tip", "Φτιαγμένο με πάθος - σε έναν κόσμο που βυθίζεται στο χάος!"),
|
||||||
("Privacy Statement", "Πολιτική απορρήτου"),
|
("Privacy Statement", "Πολιτική απορρήτου"),
|
||||||
("Mute", "Σίγαση"),
|
("Mute", "Σίγαση"),
|
||||||
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Audio Input", "Είσοδος ήχου"),
|
("Audio Input", "Είσοδος ήχου"),
|
||||||
("Enhancements", "Βελτιώσεις"),
|
("Enhancements", "Βελτιώσεις"),
|
||||||
("Hardware Codec", "Κωδικοποιητής υλικού"),
|
("Hardware Codec", "Κωδικοποιητής υλικού"),
|
||||||
("Adaptive bitrate", "Adaptive bitrate"),
|
("Adaptive bitrate", "Προσαρμοστικός ρυθμός μετάδοσης bit"),
|
||||||
("ID Server", "Διακομιστής ID"),
|
("ID Server", "Διακομιστής ID"),
|
||||||
("Relay Server", "Διακομιστής αναμετάδοσης"),
|
("Relay Server", "Διακομιστής αναμετάδοσης"),
|
||||||
("API Server", "Διακομιστής API"),
|
("API Server", "Διακομιστής API"),
|
||||||
@@ -67,18 +67,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Skip", "Παράλειψη"),
|
("Skip", "Παράλειψη"),
|
||||||
("Close", "Κλείσιμο"),
|
("Close", "Κλείσιμο"),
|
||||||
("Retry", "Δοκίμασε ξανά"),
|
("Retry", "Δοκίμασε ξανά"),
|
||||||
("OK", "ΟΚ"),
|
("OK", "Εντάξει"),
|
||||||
("Password Required", "Απαιτείται κωδικός πρόσβασης"),
|
("Password Required", "Απαιτείται κωδικός πρόσβασης"),
|
||||||
("Please enter your password", "Παρακαλώ εισάγετε τον κωδικό πρόσβασης"),
|
("Please enter your password", "Παρακαλώ εισάγετε τον κωδικό πρόσβασης"),
|
||||||
("Remember password", "Απομνημόνευση κωδικού πρόσβασης"),
|
("Remember password", "Απομνημόνευση κωδικού πρόσβασης"),
|
||||||
("Wrong Password", "Λάθος κωδικός πρόσβασης"),
|
("Wrong Password", "Λάθος κωδικός πρόσβασης"),
|
||||||
("Do you want to enter again?", "Επανασύνδεση;"),
|
("Do you want to enter again?", "Θέλετε να γίνει επανασύνδεση;"),
|
||||||
("Connection Error", "Σφάλμα σύνδεσης"),
|
("Connection Error", "Σφάλμα σύνδεσης"),
|
||||||
("Error", "Σφάλμα"),
|
("Error", "Σφάλμα"),
|
||||||
("Reset by the peer", "Η σύνδεση επαναφέρθηκε από τον απομακρυσμένο σταθμό"),
|
("Reset by the peer", "Η σύνδεση επαναφέρθηκε από τον απομακρυσμένο σταθμό"),
|
||||||
("Connecting...", "Σύνδεση..."),
|
("Connecting...", "Σύνδεση..."),
|
||||||
("Connection in progress. Please wait.", "Σύνδεση σε εξέλιξη. Παρακαλώ περιμένετε."),
|
("Connection in progress. Please wait.", "Σύνδεση σε εξέλιξη. Παρακαλώ περιμένετε."),
|
||||||
("Please try 1 minute later", "Παρακαλώ ξαναδοκιμάστε σε 1 λεπτό"),
|
("Please try 1 minute later", "Παρακαλώ δοκιμάστε ξανά σε 1 λεπτό"),
|
||||||
("Login Error", "Σφάλμα εισόδου"),
|
("Login Error", "Σφάλμα εισόδου"),
|
||||||
("Successful", "Επιτυχής"),
|
("Successful", "Επιτυχής"),
|
||||||
("Connected, waiting for image...", "Συνδέθηκε, αναμονή για εικόνα..."),
|
("Connected, waiting for image...", "Συνδέθηκε, αναμονή για εικόνα..."),
|
||||||
@@ -101,10 +101,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Select All", "Επιλογή όλων"),
|
("Select All", "Επιλογή όλων"),
|
||||||
("Unselect All", "Κατάργηση επιλογής όλων"),
|
("Unselect All", "Κατάργηση επιλογής όλων"),
|
||||||
("Empty Directory", "Κενός φάκελος"),
|
("Empty Directory", "Κενός φάκελος"),
|
||||||
("Not an empty directory", "Ο φάκελος δεν είναι κενός"),
|
("Not an empty directory", "Η διαδρομή δεν είναι κενή"),
|
||||||
("Are you sure you want to delete this file?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το αρχείο;"),
|
("Are you sure you want to delete this file?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το αρχείο;"),
|
||||||
("Are you sure you want to delete this empty directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον κενό φάκελο;"),
|
("Are you sure you want to delete this empty directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την κενή διαδρομή;"),
|
||||||
("Are you sure you want to delete the file of this directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αρχείο αυτού του φακέλου;"),
|
("Are you sure you want to delete the file of this directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αρχείο αυτής της διαδρομής;"),
|
||||||
("Do this for all conflicts", "Κάνε αυτό για όλες τις διενέξεις"),
|
("Do this for all conflicts", "Κάνε αυτό για όλες τις διενέξεις"),
|
||||||
("This is irreversible!", "Αυτό είναι μη αναστρέψιμο!"),
|
("This is irreversible!", "Αυτό είναι μη αναστρέψιμο!"),
|
||||||
("Deleting", "Διαγραφή"),
|
("Deleting", "Διαγραφή"),
|
||||||
@@ -133,8 +133,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Insert Ctrl + Alt + Del", "Εισαγωγή Ctrl + Alt + Del"),
|
("Insert Ctrl + Alt + Del", "Εισαγωγή Ctrl + Alt + Del"),
|
||||||
("Insert Lock", "Κλείδωμα απομακρυσμένου σταθμού"),
|
("Insert Lock", "Κλείδωμα απομακρυσμένου σταθμού"),
|
||||||
("Refresh", "Ανανέωση"),
|
("Refresh", "Ανανέωση"),
|
||||||
("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"),
|
("ID does not exist", "Το ID αυτό δεν υπάρχει"),
|
||||||
("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με διακομιστή"),
|
("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με τον διακομιστή"),
|
||||||
("Please try later", "Παρακαλώ δοκιμάστε αργότερα"),
|
("Please try later", "Παρακαλώ δοκιμάστε αργότερα"),
|
||||||
("Remote desktop is offline", "Ο απομακρυσμένος σταθμός εργασίας είναι εκτός σύνδεσης"),
|
("Remote desktop is offline", "Ο απομακρυσμένος σταθμός εργασίας είναι εκτός σύνδεσης"),
|
||||||
("Key mismatch", "Μη έγκυρο κλειδί"),
|
("Key mismatch", "Μη έγκυρο κλειδί"),
|
||||||
@@ -146,17 +146,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Set Password", "Ορίστε κωδικό πρόσβασης"),
|
("Set Password", "Ορίστε κωδικό πρόσβασης"),
|
||||||
("OS Password", "Κωδικός πρόσβασης λειτουργικού συστήματος"),
|
("OS Password", "Κωδικός πρόσβασης λειτουργικού συστήματος"),
|
||||||
("install_tip", "Λόγω UAC, το RustDesk ενδέχεται να μην λειτουργεί σωστά σε ορισμένες περιπτώσεις. Για να αποφύγετε το UAC, κάντε κλικ στο κουμπί παρακάτω για να εγκαταστήσετε το RustDesk στο σύστημα"),
|
("install_tip", "Λόγω UAC, το RustDesk ενδέχεται να μην λειτουργεί σωστά σε ορισμένες περιπτώσεις. Για να αποφύγετε το UAC, κάντε κλικ στο κουμπί παρακάτω για να εγκαταστήσετε το RustDesk στο σύστημα"),
|
||||||
("Click to upgrade", "Αναβάθμιση τώρα"),
|
("Click to upgrade", "Κάντε κλίκ για αναβάθμιση τώρα"),
|
||||||
("Configure", "Διαμόρφωση"),
|
("Configure", "Διαμόρφωση"),
|
||||||
("config_acc", "Για τον απομακρυσμένο έλεγχο του υπολογιστή σας, πρέπει να εκχωρήσετε δικαιώματα πρόσβασης στο RustDesk."),
|
("config_acc", "Για να ελέγξετε την επιφάνεια εργασίας σας από απόσταση, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Προσβασιμότητας\"."),
|
||||||
("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στον υπολογιστή σας, πρέπει να εκχωρήσετε το δικαίωμα RustDesk \"Screen Capture\"."),
|
("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στην επιφάνεια εργασίας σας, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Εγγραφή οθόνης\"."),
|
||||||
("Installing ...", "Γίνεται εγκατάσταση ..."),
|
("Installing ...", "Γίνεται εγκατάσταση ..."),
|
||||||
("Install", "Εγκατάσταση"),
|
("Install", "Εγκατάσταση"),
|
||||||
("Installation", "Η εγκατάσταση"),
|
("Installation", "Η εγκατάσταση"),
|
||||||
("Installation Path", "Διαδρομή εγκατάστασης"),
|
("Installation Path", "Διαδρομή εγκατάστασης"),
|
||||||
("Create start menu shortcuts", "Δημιουργία συντομεύσεων μενού έναρξης"),
|
("Create start menu shortcuts", "Δημιουργία συντομεύσεων μενού έναρξης"),
|
||||||
("Create desktop icon", "Δημιουργία εικονιδίου επιφάνειας εργασίας"),
|
("Create desktop icon", "Δημιουργία εικονιδίου επιφάνειας εργασίας"),
|
||||||
("agreement_tip", "Με την εγκατάσταση αποδέχεστε την άδεια χρήσης"),
|
("agreement_tip", "Με την εγκατάσταση, αποδέχεστε την άδεια χρήσης"),
|
||||||
("Accept and Install", "Αποδοχή και εγκατάσταση"),
|
("Accept and Install", "Αποδοχή και εγκατάσταση"),
|
||||||
("End-user license agreement", "Σύμβαση άδειας χρήσης τελικού χρήστη"),
|
("End-user license agreement", "Σύμβαση άδειας χρήσης τελικού χρήστη"),
|
||||||
("Generating ...", "Δημιουργία ..."),
|
("Generating ...", "Δημιουργία ..."),
|
||||||
@@ -170,8 +170,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Local Port", "Τοπική θύρα"),
|
("Local Port", "Τοπική θύρα"),
|
||||||
("Local Address", "Τοπική διεύθυνση"),
|
("Local Address", "Τοπική διεύθυνση"),
|
||||||
("Change Local Port", "Αλλαγή τοπικής θύρας"),
|
("Change Local Port", "Αλλαγή τοπικής θύρας"),
|
||||||
("setup_server_tip", "Για πιο γρήγορη σύνδεση, ρυθμίστε τον δικό σας διακομιστή σύνδεσης"),
|
("setup_server_tip", "Για πιο γρήγορη σύνδεση, παρακαλούμε να ρυθμίστε τον δικό σας διακομιστή σύνδεσης"),
|
||||||
("Too short, at least 6 characters.", "Πολύ μικρό, τουλάχιστον 6 χαρακτήρες."),
|
("Too short, at least 6 characters.", "Πολύ μικρό, χρειάζεται τουλάχιστον 6 χαρακτήρες."),
|
||||||
("The confirmation is not identical.", "Η επιβεβαίωση δεν είναι πανομοιότυπη."),
|
("The confirmation is not identical.", "Η επιβεβαίωση δεν είναι πανομοιότυπη."),
|
||||||
("Permissions", "Άδειες"),
|
("Permissions", "Άδειες"),
|
||||||
("Accept", "Αποδοχή"),
|
("Accept", "Αποδοχή"),
|
||||||
@@ -183,7 +183,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relayed and encrypted connection", "Κρυπτογραφημένη σύνδεση με αναμετάδοση"),
|
("Relayed and encrypted connection", "Κρυπτογραφημένη σύνδεση με αναμετάδοση"),
|
||||||
("Direct and unencrypted connection", "Άμεση και μη κρυπτογραφημένη σύνδεση"),
|
("Direct and unencrypted connection", "Άμεση και μη κρυπτογραφημένη σύνδεση"),
|
||||||
("Relayed and unencrypted connection", "Μη κρυπτογραφημένη σύνδεση με αναμετάδοση"),
|
("Relayed and unencrypted connection", "Μη κρυπτογραφημένη σύνδεση με αναμετάδοση"),
|
||||||
("Enter Remote ID", "Εισαγωγή απομακρυσμένου ID"),
|
("Enter Remote ID", "Εισαγωγή του απομακρυσμένου ID"),
|
||||||
("Enter your password", "Εισάγετε τον κωδικό σας"),
|
("Enter your password", "Εισάγετε τον κωδικό σας"),
|
||||||
("Logging in...", "Γίνεται σύνδεση..."),
|
("Logging in...", "Γίνεται σύνδεση..."),
|
||||||
("Enable RDP session sharing", "Ενεργοποίηση κοινής χρήσης RDP"),
|
("Enable RDP session sharing", "Ενεργοποίηση κοινής χρήσης RDP"),
|
||||||
@@ -200,35 +200,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Login screen using Wayland is not supported", "Η οθόνη εισόδου με χρήση του Wayland δεν υποστηρίζεται"),
|
("Login screen using Wayland is not supported", "Η οθόνη εισόδου με χρήση του Wayland δεν υποστηρίζεται"),
|
||||||
("Reboot required", "Απαιτείται επανεκκίνηση"),
|
("Reboot required", "Απαιτείται επανεκκίνηση"),
|
||||||
("Unsupported display server", "Μη υποστηριζόμενος διακομιστής εμφάνισης "),
|
("Unsupported display server", "Μη υποστηριζόμενος διακομιστής εμφάνισης "),
|
||||||
("x11 expected", "απαιτείται X11"),
|
("x11 expected", "αναμένεται X11"),
|
||||||
("Port", "Θύρα"),
|
("Port", "Θύρα"),
|
||||||
("Settings", "Ρυθμίσεις"),
|
("Settings", "Ρυθμίσεις"),
|
||||||
("Username", "Όνομα χρήστη"),
|
("Username", "Όνομα χρήστη"),
|
||||||
("Invalid port", "Μη έγκυρη θύρα"),
|
("Invalid port", "Μη έγκυρη θύρα"),
|
||||||
("Closed manually by the peer", "Έκλεισε από τον απομακρυσμένο σταθμό"),
|
("Closed manually by the peer", "Τερματίστηκε από τον απομακρυσμένο σταθμό"),
|
||||||
("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης ρυθμίσεων"),
|
("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης διαμόρφωσης"),
|
||||||
("Run without install", "Εκτέλεση χωρίς εγκατάσταση"),
|
("Run without install", "Εκτέλεση χωρίς εγκατάσταση"),
|
||||||
("Connect via relay", "Πραγματοποίηση σύνδεση μέσω αναμεταδότη"),
|
("Connect via relay", "Σύνδεση μέσω αναμεταδότη"),
|
||||||
("Always connect via relay", "Σύνδεση πάντα μέσω αναμεταδότη"),
|
("Always connect via relay", "Να γίνεται σύνδεση πάντα μέσω αναμεταδότη"),
|
||||||
("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"),
|
("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων να έχουν πρόσβαση σε εμένα"),
|
||||||
("Login", "Σύνδεση"),
|
("Login", "Σύνδεση"),
|
||||||
("Verify", "Επαλήθευση"),
|
("Verify", "Επαλήθευση"),
|
||||||
("Remember me", "Να με θυμάσαι"),
|
("Remember me", "Να με θυμάσαι"),
|
||||||
("Trust this device", "Εμπιστεύομαι αυτή την συσκευή"),
|
("Trust this device", "Να εμπιστεύομαι αυτή την συσκευή"),
|
||||||
("Verification code", "Κωδικός επαλήθευσης"),
|
("Verification code", "Κωδικός επαλήθευσης"),
|
||||||
("verification_tip", "Εντοπίστηκε νέα συσκευή και εστάλη ένας κωδικός επαλήθευσης στην καταχωρισμένη διεύθυνση email. Εισαγάγετε τον κωδικό επαλήθευσης για να συνδεθείτε ξανά."),
|
("verification_tip", "Ένας κωδικός επαλήθευσης έχει σταλεί στην καταχωρημένη διεύθυνση email. Εισαγάγετε τον κωδικό επαλήθευσης για να συνεχίσετε τη σύνδεση."),
|
||||||
("Logout", "Αποσύνδεση"),
|
("Logout", "Αποσύνδεση"),
|
||||||
("Tags", "Ετικέτες"),
|
("Tags", "Ετικέτες"),
|
||||||
("Search ID", "Αναζήτηση ID"),
|
("Search ID", "Αναζήτηση ID"),
|
||||||
("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, διάστημα ή νέα γραμμή"),
|
("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, κενό ή νέα γραμμή"),
|
||||||
("Add ID", "Προσθήκη αναγνωριστικού ID"),
|
("Add ID", "Προσθήκη ID"),
|
||||||
("Add Tag", "Προσθήκη ετικέτας"),
|
("Add Tag", "Προσθήκη ετικέτας"),
|
||||||
("Unselect all tags", "Κατάργηση επιλογής όλων των ετικετών"),
|
("Unselect all tags", "Αποεπιλογή όλων των ετικετών"),
|
||||||
("Network error", "Σφάλμα δικτύου"),
|
("Network error", "Σφάλμα δικτύου"),
|
||||||
("Username missed", "Δεν συμπληρώσατε το όνομα χρήστη"),
|
("Username missed", "Δεν συμπληρώσατε το όνομα χρήστη"),
|
||||||
("Password missed", "Δεν συμπληρώσατε τον κωδικό πρόσβασης"),
|
("Password missed", "Δεν συμπληρώσατε τον κωδικό πρόσβασης"),
|
||||||
("Wrong credentials", "Λάθος διαπιστευτήρια"),
|
("Wrong credentials", "Λάθος διαπιστευτήρια"),
|
||||||
("The verification code is incorrect or has expired", ""),
|
("The verification code is incorrect or has expired", "Ο κωδικός επαλήθευσης είναι λανθασμένος ή έχει λήξει"),
|
||||||
("Edit Tag", "Επεξεργασία ετικέτας"),
|
("Edit Tag", "Επεξεργασία ετικέτας"),
|
||||||
("Forget Password", "Διαγραφή απομνημονευμένου κωδικού"),
|
("Forget Password", "Διαγραφή απομνημονευμένου κωδικού"),
|
||||||
("Favorites", "Αγαπημένα"),
|
("Favorites", "Αγαπημένα"),
|
||||||
@@ -239,7 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Socks5 Proxy", "Διαμεσολαβητής Socks5"),
|
("Socks5 Proxy", "Διαμεσολαβητής Socks5"),
|
||||||
("Socks5/Http(s) Proxy", "Διαμεσολαβητής Socks5/Http(s)"),
|
("Socks5/Http(s) Proxy", "Διαμεσολαβητής Socks5/Http(s)"),
|
||||||
("Discovered", "Ανακαλύφθηκαν"),
|
("Discovered", "Ανακαλύφθηκαν"),
|
||||||
("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος"),
|
("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος."),
|
||||||
("Remote ID", "Απομακρυσμένο ID"),
|
("Remote ID", "Απομακρυσμένο ID"),
|
||||||
("Paste", "Επικόλληση"),
|
("Paste", "Επικόλληση"),
|
||||||
("Paste here?", "Επικόλληση εδώ;"),
|
("Paste here?", "Επικόλληση εδώ;"),
|
||||||
@@ -262,28 +262,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Pinch to Zoom", "Τσίμπημα για ζουμ"),
|
("Pinch to Zoom", "Τσίμπημα για ζουμ"),
|
||||||
("Canvas Zoom", "Ζουμ σε καμβά"),
|
("Canvas Zoom", "Ζουμ σε καμβά"),
|
||||||
("Reset canvas", "Επαναφορά καμβά"),
|
("Reset canvas", "Επαναφορά καμβά"),
|
||||||
("No permission of file transfer", "Δεν υπάρχει άδεια για μεταφορά αρχείων"),
|
("No permission of file transfer", "Δεν υπάρχει άδεια για την μεταφορά αρχείων"),
|
||||||
("Note", "Σημείωση"),
|
("Note", "Σημείωση"),
|
||||||
("Connection", "Σύνδεση"),
|
("Connection", "Σύνδεση"),
|
||||||
("Share screen", "Κοινή χρήση οθόνης"),
|
("Share screen", "Κοινή χρήση οθόνης"),
|
||||||
("Chat", "Κουβέντα"),
|
("Chat", "Κουβέντα"),
|
||||||
("Total", "Σύνολο"),
|
("Total", "Σύνολο"),
|
||||||
("items", "στοιχεία"),
|
("items", "στοιχεία"),
|
||||||
("Selected", "Επιλεγμένο"),
|
("Selected", "Επιλεγμένα"),
|
||||||
("Screen Capture", "Αποτύπωση οθόνης"),
|
("Screen Capture", "Καταγραφή οθόνης"),
|
||||||
("Input Control", "Έλεγχος εισόδου"),
|
("Input Control", "Έλεγχος εισόδου"),
|
||||||
("Audio Capture", "Εγγραφή ήχου"),
|
("Audio Capture", "Εγγραφή ήχου"),
|
||||||
("Do you accept?", "Δέχεσαι;"),
|
("Do you accept?", "Δέχεσαι;"),
|
||||||
("Open System Setting", "Άνοιγμα ρυθμίσεων συστήματος"),
|
("Open System Setting", "Άνοιγμα ρυθμίσεων συστήματος"),
|
||||||
("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισαγωγής Android;"),
|
("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισόδου για Android;"),
|
||||||
("android_input_permission_tip1", "Για να μπορεί μία απομακρυσμένη συσκευή να ελέγχει τη συσκευή σας Android, πρέπει να επιτρέψετε στο RustDesk να χρησιμοποιεί την υπηρεσία \"Προσβασιμότητα\"."),
|
("android_input_permission_tip1", "Για να μπορεί μία απομακρυσμένη συσκευή να ελέγχει τη συσκευή σας Android, πρέπει να επιτρέψετε στο RustDesk να χρησιμοποιεί την υπηρεσία \"Προσβασιμότητα\"."),
|
||||||
("android_input_permission_tip2", "Παρακαλώ μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."),
|
("android_input_permission_tip2", "Παρακαλούμε να μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."),
|
||||||
("android_new_connection_tip", "θέλω να ελέγξω τη συσκευή σου."),
|
("android_new_connection_tip", "Έχει ληφθεί νέο αίτημα ελέγχου, το οποίο θέλει να ελέγξει την τρέχουσα συσκευή σας."),
|
||||||
("android_service_will_start_tip", "Η ενεργοποίηση της κοινής χρήσης οθόνης θα ξεκινήσει αυτόματα την υπηρεσία, ώστε άλλες συσκευές να μπορούν να ελέγχουν αυτήν τη συσκευή Android."),
|
("android_service_will_start_tip", "Η ενεργοποίηση της \"Καταγραφής οθόνης\" θα ξεκινήσει αυτόματα την υπηρεσία, επιτρέποντας σε άλλες συσκευές να ζητήσουν σύνδεση με τη συσκευή σας."),
|
||||||
("android_stop_service_tip", "Η απενεργοποίηση της υπηρεσίας θα αποσυνδέσει αυτόματα όλες τις εγκατεστημένες συνδέσεις."),
|
("android_stop_service_tip", "Το κλείσιμο της υπηρεσίας αυτής θα κλείσει αυτόματα όλες τις υπάρχουσες συνδέσεις."),
|
||||||
("android_version_audio_tip", "Η έκδοση Android που διαθέτετε δεν υποστηρίζει εγγραφή ήχου, ενημερώστε το σε Android 10 ή νεότερη έκδοση, εάν είναι δυνατόν."),
|
("android_version_audio_tip", "Η τρέχουσα έκδοση Android δεν υποστηρίζει εγγραφή ήχου, αναβαθμίστε σε Android 10 ή νεότερη έκδοση."),
|
||||||
("android_start_service_tip", ""),
|
("android_start_service_tip", "Πατήστε [Έναρξη υπηρεσίας] ή ενεργοποιήστε την άδεια [Καταγραφή οθόνης] για να ξεκινήσετε την υπηρεσία κοινής χρήσης οθόνης."),
|
||||||
("android_permission_may_not_change_tip", ""),
|
("android_permission_may_not_change_tip", "Τα δικαιώματα για τις καθιερωμένες συνδέσεις δεν μπορούν να αλλάξουν άμεσα μέχρι να επανασυνδεθούν."),
|
||||||
("Account", "Λογαριασμός"),
|
("Account", "Λογαριασμός"),
|
||||||
("Overwrite", "Αντικατάσταση"),
|
("Overwrite", "Αντικατάσταση"),
|
||||||
("This file exists, skip or overwrite this file?", "Αυτό το αρχείο υπάρχει, παράβλεψη ή αντικατάσταση αυτού του αρχείου;"),
|
("This file exists, skip or overwrite this file?", "Αυτό το αρχείο υπάρχει, παράβλεψη ή αντικατάσταση αυτού του αρχείου;"),
|
||||||
@@ -293,14 +293,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Succeeded", "Επιτυχής"),
|
("Succeeded", "Επιτυχής"),
|
||||||
("Someone turns on privacy mode, exit", "Κάποιος ενεργοποιεί τη λειτουργία απορρήτου, έξοδος"),
|
("Someone turns on privacy mode, exit", "Κάποιος ενεργοποιεί τη λειτουργία απορρήτου, έξοδος"),
|
||||||
("Unsupported", "Δεν υποστηρίζεται"),
|
("Unsupported", "Δεν υποστηρίζεται"),
|
||||||
("Peer denied", "Ο απομακρυσμένος σταθμός απέρριψε τη σύνδεση"),
|
("Peer denied", "Ο απομακρυσμένος σταθμός έχει απορριφθεί"),
|
||||||
("Please install plugins", "Παρακαλώ εγκαταστήστε τα πρόσθετα"),
|
("Please install plugins", "Παρακαλώ εγκαταστήστε τα πρόσθετα"),
|
||||||
("Peer exit", "Ο απομακρυσμένος σταθμός έχει αποσυνδεθεί"),
|
("Peer exit", "Ο απομακρυσμένος σταθμός έχει αποσυνδεθεί"),
|
||||||
("Failed to turn off", "Αποτυχία απενεργοποίησης"),
|
("Failed to turn off", "Αποτυχία απενεργοποίησης"),
|
||||||
("Turned off", "Απενεργοποιημένο"),
|
("Turned off", "Απενεργοποιημένο"),
|
||||||
("Language", "Γλώσσα"),
|
("Language", "Γλώσσα"),
|
||||||
("Keep RustDesk background service", "Εκτέλεση του RustDesk στο παρασκήνιο"),
|
("Keep RustDesk background service", "Διατήρηση της υπηρεσίας παρασκηνίου του RustDesk"),
|
||||||
("Ignore Battery Optimizations", "Παράβλεψη βελτιστοποιήσεων μπαταρίας"),
|
("Ignore Battery Optimizations", "Αγνόηση βελτιστοποιήσεων μπαταρίας"),
|
||||||
("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"),
|
("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"),
|
||||||
("Start on boot", "Έναρξη κατά την εκκίνηση"),
|
("Start on boot", "Έναρξη κατά την εκκίνηση"),
|
||||||
("Start the screen sharing service on boot, requires special permissions", "Η έναρξη της υπηρεσίας κοινής χρήσης οθόνης κατά την εκκίνηση, απαιτεί ειδικά δικαιώματα"),
|
("Start the screen sharing service on boot, requires special permissions", "Η έναρξη της υπηρεσίας κοινής χρήσης οθόνης κατά την εκκίνηση, απαιτεί ειδικά δικαιώματα"),
|
||||||
@@ -315,11 +315,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restart remote device", "Επανεκκίνηση απομακρυσμένης συσκευής"),
|
("Restart remote device", "Επανεκκίνηση απομακρυσμένης συσκευής"),
|
||||||
("Are you sure you want to restart", "Είστε βέβαιοι ότι θέλετε να κάνετε επανεκκίνηση"),
|
("Are you sure you want to restart", "Είστε βέβαιοι ότι θέλετε να κάνετε επανεκκίνηση"),
|
||||||
("Restarting remote device", "Γίνεται επανεκκίνηση της απομακρυσμένης συσκευής"),
|
("Restarting remote device", "Γίνεται επανεκκίνηση της απομακρυσμένης συσκευής"),
|
||||||
("remote_restarting_tip", "Η απομακρυσμένη συσκευή επανεκκινείται, κλείστε αυτό το μήνυμα και επανασυνδεθείτε χρησιμοποιώντας τον μόνιμο κωδικό πρόσβασης."),
|
("remote_restarting_tip", "Γίνεται επανεκκίνηση της απομακρυσμένης συσκευής. Κλείστε αυτό το πλαίσιο μηνύματος και επανασυνδεθείτε με τον μόνιμο κωδικό πρόσβασης μετά από λίγο."),
|
||||||
("Copied", "Αντιγράφηκε"),
|
("Copied", "Αντιγράφηκε"),
|
||||||
("Exit Fullscreen", "Έξοδος από πλήρη οθόνη"),
|
("Exit Fullscreen", "Έξοδος από πλήρη οθόνη"),
|
||||||
("Fullscreen", "Πλήρης οθόνη"),
|
("Fullscreen", "Πλήρης οθόνη"),
|
||||||
("Mobile Actions", "Mobile Actions"),
|
("Mobile Actions", "Ενέργειες για κινητά"),
|
||||||
("Select Monitor", "Επιλογή οθόνης"),
|
("Select Monitor", "Επιλογή οθόνης"),
|
||||||
("Control Actions", "Ενέργειες ελέγχου"),
|
("Control Actions", "Ενέργειες ελέγχου"),
|
||||||
("Display Settings", "Ρυθμίσεις οθόνης"),
|
("Display Settings", "Ρυθμίσεις οθόνης"),
|
||||||
@@ -347,7 +347,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Enable audio", "Ενεργοποίηση ήχου"),
|
("Enable audio", "Ενεργοποίηση ήχου"),
|
||||||
("Unlock Network Settings", "Ξεκλείδωμα ρυθμίσεων δικτύου"),
|
("Unlock Network Settings", "Ξεκλείδωμα ρυθμίσεων δικτύου"),
|
||||||
("Server", "Διακομιστής"),
|
("Server", "Διακομιστής"),
|
||||||
("Direct IP Access", "Πρόσβαση με χρήση IP"),
|
("Direct IP Access", "Άμεση πρόσβαση IP"),
|
||||||
("Proxy", "Διαμεσολαβητής"),
|
("Proxy", "Διαμεσολαβητής"),
|
||||||
("Apply", "Εφαρμογή"),
|
("Apply", "Εφαρμογή"),
|
||||||
("Disconnect all devices?", "Αποσύνδεση όλων των συσκευών;"),
|
("Disconnect all devices?", "Αποσύνδεση όλων των συσκευών;"),
|
||||||
@@ -358,7 +358,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Pin Toolbar", "Καρφίτσωμα γραμμής εργαλείων"),
|
("Pin Toolbar", "Καρφίτσωμα γραμμής εργαλείων"),
|
||||||
("Unpin Toolbar", "Ξεκαρφίτσωμα γραμμής εργαλείων"),
|
("Unpin Toolbar", "Ξεκαρφίτσωμα γραμμής εργαλείων"),
|
||||||
("Recording", "Εγγραφή"),
|
("Recording", "Εγγραφή"),
|
||||||
("Directory", "Φάκελος εγγραφών"),
|
("Directory", "Διαδρομή"),
|
||||||
("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"),
|
("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"),
|
||||||
("Automatically record outgoing sessions", "Αυτόματη εγγραφή εξερχόμενων συνεδριών"),
|
("Automatically record outgoing sessions", "Αυτόματη εγγραφή εξερχόμενων συνεδριών"),
|
||||||
("Change", "Αλλαγή"),
|
("Change", "Αλλαγή"),
|
||||||
@@ -373,23 +373,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("elevated_foreground_window_tip", "Το τρέχον παράθυρο της απομακρυσμένης επιφάνειας εργασίας απαιτεί υψηλότερα δικαιώματα για να λειτουργήσει, επομένως δεν μπορεί να χρησιμοποιήσει προσωρινά το ποντίκι και το πληκτρολόγιο. Μπορείτε να ζητήσετε από τον απομακρυσμένο χρήστη να ελαχιστοποιήσει το τρέχον παράθυρο ή να κάνετε κλικ στο κουμπί ανύψωσης στο παράθυρο διαχείρισης σύνδεσης. Για να αποφύγετε αυτό το πρόβλημα, συνιστάται η εγκατάσταση του λογισμικού στην απομακρυσμένη συσκευή."),
|
("elevated_foreground_window_tip", "Το τρέχον παράθυρο της απομακρυσμένης επιφάνειας εργασίας απαιτεί υψηλότερα δικαιώματα για να λειτουργήσει, επομένως δεν μπορεί να χρησιμοποιήσει προσωρινά το ποντίκι και το πληκτρολόγιο. Μπορείτε να ζητήσετε από τον απομακρυσμένο χρήστη να ελαχιστοποιήσει το τρέχον παράθυρο ή να κάνετε κλικ στο κουμπί ανύψωσης στο παράθυρο διαχείρισης σύνδεσης. Για να αποφύγετε αυτό το πρόβλημα, συνιστάται η εγκατάσταση του λογισμικού στην απομακρυσμένη συσκευή."),
|
||||||
("Disconnected", "Αποσυνδέθηκε"),
|
("Disconnected", "Αποσυνδέθηκε"),
|
||||||
("Other", "Άλλα"),
|
("Other", "Άλλα"),
|
||||||
("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσετε πολλές καρτέλες"),
|
("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσουν πολλαπλές καρτέλες"),
|
||||||
("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"),
|
("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"),
|
||||||
("Full Access", "Πλήρης πρόσβαση"),
|
("Full Access", "Πλήρης πρόσβαση"),
|
||||||
("Screen Share", "Κοινή χρήση οθόνης"),
|
("Screen Share", "Κοινή χρήση οθόνης"),
|
||||||
("Wayland requires Ubuntu 21.04 or higher version.", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."),
|
("ubuntu-21-04-required", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."),
|
||||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Το Wayland απαιτεί υψηλότερη έκδοση του linux distro. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."),
|
("wayland-requires-higher-linux-version", "Το Wayland απαιτεί υψηλότερη έκδοση διανομής του linux. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."),
|
||||||
("JumpLink", "Προβολή"),
|
("xdp-portal-unavailable", ""),
|
||||||
|
("JumpLink", "Σύνδεσμος μετάβασης"),
|
||||||
("Please Select the screen to be shared(Operate on the peer side).", "Επιλέξτε την οθόνη που θέλετε να μοιραστείτε (Λειτουργία στην πλευρά του απομακρυσμένου σταθμού)."),
|
("Please Select the screen to be shared(Operate on the peer side).", "Επιλέξτε την οθόνη που θέλετε να μοιραστείτε (Λειτουργία στην πλευρά του απομακρυσμένου σταθμού)."),
|
||||||
("Show RustDesk", "Εμφάνιση RustDesk"),
|
("Show RustDesk", "Εμφάνιση του RustDesk"),
|
||||||
("This PC", "Αυτός ο υπολογιστής"),
|
("This PC", "Αυτός ο υπολογιστής"),
|
||||||
("or", "ή"),
|
("or", "ή"),
|
||||||
("Elevate", "Ανύψωση"),
|
("Elevate", "Ανύψωση"),
|
||||||
("Zoom cursor", "Kέρσορας μεγέθυνσης"),
|
("Zoom cursor", "Δρομέας ζουμ"),
|
||||||
("Accept sessions via password", "Αποδοχή συνεδριών με κωδικό πρόσβασης"),
|
("Accept sessions via password", "Αποδοχή συνεδριών με κωδικό πρόσβασης"),
|
||||||
("Accept sessions via click", "Αποδοχή συνεδριών με κλικ"),
|
("Accept sessions via click", "Αποδοχή συνεδριών με κλικ"),
|
||||||
("Accept sessions via both", "Αποδοχή συνεδριών και με τα δύο"),
|
("Accept sessions via both", "Αποδοχή συνεδριών και με τα δύο"),
|
||||||
("Please wait for the remote side to accept your session request...", "Παρακαλώ περιμένετε μέχρι η απομακρυσμένη πλευρά να αποδεχτεί το αίτημα συνεδρίας σας..."),
|
("Please wait for the remote side to accept your session request...", "Παρακαλώ περιμένετε μέχρι η απομακρυσμένη πλευρά να αποδεχτεί το αίτημα της συνεδρίας σας..."),
|
||||||
("One-time Password", "Κωδικός μίας χρήσης"),
|
("One-time Password", "Κωδικός μίας χρήσης"),
|
||||||
("Use one-time password", "Χρήση κωδικού πρόσβασης μίας χρήσης"),
|
("Use one-time password", "Χρήση κωδικού πρόσβασης μίας χρήσης"),
|
||||||
("One-time password length", "Μήκος κωδικού πρόσβασης μίας χρήσης"),
|
("One-time password length", "Μήκος κωδικού πρόσβασης μίας χρήσης"),
|
||||||
@@ -398,27 +399,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"),
|
("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"),
|
||||||
("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."),
|
("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."),
|
||||||
("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"),
|
("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"),
|
||||||
("Skipped", "Παράλειψη"),
|
("Skipped", "Παραλήφθηκε"),
|
||||||
("Add to address book", "Προσθήκη στο Βιβλίο Διευθύνσεων"),
|
("Add to address book", "Προσθήκη στο βιβλίο διευθύνσεων"),
|
||||||
("Group", "Ομάδα"),
|
("Group", "Ομάδα"),
|
||||||
("Search", "Αναζήτηση"),
|
("Search", "Αναζήτηση"),
|
||||||
("Closed manually by web console", "Κλειστό χειροκίνητα από την κονσόλα web"),
|
("Closed manually by web console", "Κλείσιμο χειροκίνητα από την κονσόλα ιστού"),
|
||||||
("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"),
|
("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"),
|
||||||
("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"),
|
("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"),
|
||||||
("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."),
|
("software_render_tip", "Εάν χρησιμοποιείτε κάρτα γραφικών της Nvidia σε Linux και το παράθυρο απομακρυσμένης πρόσβασης κλείνει αμέσως μετά τη σύνδεση, η μετάβαση στο πρόγραμμα οδήγησης της Nouveau ανοιχτού κώδικα και η επιλογή χρήσης απόδοσης λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση του λογισμικού."),
|
||||||
("Always use software rendering", "Επιτάχυνση γραφικών μέσω λογισμικού"),
|
("Always use software rendering", "Να χρησιμοποιείτε πάντα η απόδοση λογισμικού"),
|
||||||
("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο RustDesk"),
|
("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με το πληκτρολόγιο, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Παρακολούθηση εισόδου\"."),
|
||||||
("config_microphone", "Ρύθμιση μικροφώνου"),
|
("config_microphone", "Για να μιλήσετε εξ αποστάσεως, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Εγγραφή ήχου\"."),
|
||||||
("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"),
|
("request_elevation_tip", "Μπορείτε επίσης να ζητήσετε ανύψωση εάν υπάρχει κάποιος στην απομακρυσμένη πλευρά."),
|
||||||
("Wait", "Περιμένετε"),
|
("Wait", "Περιμένετε"),
|
||||||
("Elevation Error", "Σφάλμα ανύψωσης δικαιωμάτων χρήστη"),
|
("Elevation Error", "Σφάλμα ανύψωσης"),
|
||||||
("Ask the remote user for authentication", "Ζητήστε από τον απομακρυσμένο χρήστη έλεγχο ταυτότητας"),
|
("Ask the remote user for authentication", "Ζητήστε από τον απομακρυσμένο χρήστη έλεγχο ταυτότητας"),
|
||||||
("Choose this if the remote account is administrator", "Επιλέξτε αυτό εάν ο απομακρυσμένος λογαριασμός είναι διαχειριστής"),
|
("Choose this if the remote account is administrator", "Επιλέξτε αυτό εάν ο απομακρυσμένος λογαριασμός είναι διαχειριστής"),
|
||||||
("Transmit the username and password of administrator", "Αποστολή του ονόματος χρήστη και του κωδικού πρόσβασης του διαχειριστή"),
|
("Transmit the username and password of administrator", "Μεταδώστε το όνομα χρήστη και τον κωδικό πρόσβασης του διαχειριστή"),
|
||||||
("still_click_uac_tip", "Εξακολουθεί να απαιτεί από τον απομακρυσμένο χρήστη να κάνει κλικ στο OK στο παράθυρο UAC όπου εκτελείται το RustDesk."),
|
("still_click_uac_tip", "Εξακολουθεί να απαιτεί από τον απομακρυσμένο χρήστη να κάνει κλικ στο πλήκτρο Εντάξει στο παράθυρο UAC όπου εκτελείται το RustDesk."),
|
||||||
("Request Elevation", "Αίτημα ανύψωσης δικαιωμάτων χρήστη"),
|
("Request Elevation", "Αίτημα ανύψωσης"),
|
||||||
("wait_accept_uac_tip", "Περιμένετε να αποδεχτεί ο απομακρυσμένος χρήστης το παράθυρο διαλόγου UAC."),
|
("wait_accept_uac_tip", "Περιμένετε μέχρι ο απομακρυσμένος χρήστης να αποδεχτεί το παράθυρο διαλόγου UAC."),
|
||||||
("Elevate successfully", "Επιτυχής ανύψωση δικαιωμάτων χρήστη"),
|
("Elevate successfully", "Επιτυχής ανύψωση"),
|
||||||
("uppercase", "κεφαλαία γράμματα"),
|
("uppercase", "κεφαλαία γράμματα"),
|
||||||
("lowercase", "πεζά γράμματα"),
|
("lowercase", "πεζά γράμματα"),
|
||||||
("digit", "αριθμός"),
|
("digit", "αριθμός"),
|
||||||
@@ -427,7 +428,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Weak", "Αδύναμο"),
|
("Weak", "Αδύναμο"),
|
||||||
("Medium", "Μέτριο"),
|
("Medium", "Μέτριο"),
|
||||||
("Strong", "Δυνατό"),
|
("Strong", "Δυνατό"),
|
||||||
("Switch Sides", "Εναλλαγή πλευράς"),
|
("Switch Sides", "Αλλαγή πλευρών"),
|
||||||
("Please confirm if you want to share your desktop?", "Παρακαλώ επιβεβαιώστε αν επιθυμείτε την κοινή χρήση της επιφάνειας εργασίας;"),
|
("Please confirm if you want to share your desktop?", "Παρακαλώ επιβεβαιώστε αν επιθυμείτε την κοινή χρήση της επιφάνειας εργασίας;"),
|
||||||
("Display", "Εμφάνιση"),
|
("Display", "Εμφάνιση"),
|
||||||
("Default View Style", "Προκαθορισμένος τρόπος εμφάνισης"),
|
("Default View Style", "Προκαθορισμένος τρόπος εμφάνισης"),
|
||||||
@@ -441,11 +442,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Voice call", "Φωνητική κλήση"),
|
("Voice call", "Φωνητική κλήση"),
|
||||||
("Text chat", "Συνομιλία κειμένου"),
|
("Text chat", "Συνομιλία κειμένου"),
|
||||||
("Stop voice call", "Διακοπή φωνητικής κλήσης"),
|
("Stop voice call", "Διακοπή φωνητικής κλήσης"),
|
||||||
("relay_hint_tip", "Εάν δεν είναι δυνατή η απευθείας σύνδεση, μπορείτε να δοκιμάσετε να συνδεθείτε μέσω διακομιστή αναμετάδοσης"),
|
("relay_hint_tip", "Ενδέχεται να μην είναι δυνατή η απευθείας σύνδεση: μπορείτε να δοκιμάσετε να συνδεθείτε μέσω αναμετάδοσης. Επιπλέον, εάν θέλετε να χρησιμοποιήσετε την αναμετάδοση στην πρώτη σας προσπάθεια, μπορείτε να προσθέσετε την \"/r\" κατάληξη στο ID ή να επιλέξετε την επιλογή \"Πάντα σύνδεση μέσω αναμετάδοσης\" στην κάρτα πρόσφατων συνεδριών, εάν υπάρχει."),
|
||||||
("Reconnect", "Επανασύνδεση"),
|
("Reconnect", "Επανασύνδεση"),
|
||||||
("Codec", "Κωδικοποίηση"),
|
("Codec", "Κωδικοποίηση"),
|
||||||
("Resolution", "Ανάλυση"),
|
("Resolution", "Ανάλυση"),
|
||||||
("No transfers in progress", "Δεν υπάρχει μεταφορά σε εξέλιξη"),
|
("No transfers in progress", "Δεν υπάρχουν μεταφορές σε εξέλιξη"),
|
||||||
("Set one-time password length", "Μέγεθος κωδικού μιας χρήσης"),
|
("Set one-time password length", "Μέγεθος κωδικού μιας χρήσης"),
|
||||||
("RDP Settings", "Ρυθμίσεις RDP"),
|
("RDP Settings", "Ρυθμίσεις RDP"),
|
||||||
("Sort by", "Ταξινόμηση κατά"),
|
("Sort by", "Ταξινόμηση κατά"),
|
||||||
@@ -454,35 +455,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Minimize", "Ελαχιστοποίηση"),
|
("Minimize", "Ελαχιστοποίηση"),
|
||||||
("Maximize", "Μεγιστοποίηση"),
|
("Maximize", "Μεγιστοποίηση"),
|
||||||
("Your Device", "Η συσκευή σας"),
|
("Your Device", "Η συσκευή σας"),
|
||||||
("empty_recent_tip", "Δεν υπάρχουν πρόσφατες συνεδρίες!\nΔοκιμάστε να ξεκινήσετε μια νέα."),
|
("empty_recent_tip", "Ωχ, δεν υπάρχουν πρόσφατες συνεδρίες!\nΏρα να προγραμματίσετε μια νέα."),
|
||||||
("empty_favorite_tip", "Δεν υπάρχουν ακόμη αγαπημένες συνδέσεις;\nΑφού πραγματοποιήσετε σύνδεση με κάποιο απομακρυσμένο σταθμό, μπορείτε να τον προσθέσετε στα αγαπημένα σας!"),
|
("empty_favorite_tip", "Δεν έχετε ακόμα αγαπημένους απομακρυσμένους σταθμούς;\nΑς βρούμε κάποιον για να συνδεθούμε και ας τον προσθέσουμε στα αγαπημένα σας!"),
|
||||||
("empty_lan_tip", "Δεν έχουμε ανακαλυφθεί ακόμη απομακρυσμένοι σταθμοί."),
|
("empty_lan_tip", "Ωχ όχι, φαίνεται ότι δεν έχουμε ανακαλύψει ακόμη κανέναν απομακρυσμένο σταθμό."),
|
||||||
("empty_address_book_tip", "Φαίνεται ότι αυτή τη στιγμή δεν υπάρχουν αγαπημένες συνδέσεις στο βιβλίο διευθύνσεών σας."),
|
("empty_address_book_tip", "Ω, Αγαπητέ/ή μου, φαίνεται ότι αυτήν τη στιγμή δεν υπάρχουν απομακρυσμένοι σταθμοί στο βιβλίο διευθύνσεών σας."),
|
||||||
("Empty Username", "Κενό όνομα χρήστη"),
|
("Empty Username", "Κενό όνομα χρήστη"),
|
||||||
("Empty Password", "Κενός κωδικός πρόσβασης"),
|
("Empty Password", "Κενός κωδικός πρόσβασης"),
|
||||||
("Me", "Εγώ"),
|
("Me", "Εγώ"),
|
||||||
("identical_file_tip", "Το αρχείο είναι πανομοιότυπο με αυτό του άλλου υπολογιστή."),
|
("identical_file_tip", "Αυτό το αρχείο είναι πανομοιότυπο με αυτό του απομακρυσμένου σταθμού."),
|
||||||
("show_monitors_tip", "Εμφάνιση οθονών στη γραμμή εργαλείων"),
|
("show_monitors_tip", "Εμφάνιση οθονών στη γραμμή εργαλείων"),
|
||||||
("View Mode", "Λειτουργία προβολής"),
|
("View Mode", "Λειτουργία προβολής"),
|
||||||
("login_linux_tip", "Απαιτείται είσοδος σε απομακρυσμένο λογαριασμό Linux για την ενεργοποίηση του περιβάλλον εργασίας Χ."),
|
("login_linux_tip", "Πρέπει να συνδεθείτε σε έναν απομακρυσμένο λογαριασμό Linux για να ενεργοποιήσετε μια συνεδρία επιφάνειας εργασίας X"),
|
||||||
("verify_rustdesk_password_tip", "Επιβεβαιώστε τον κωδικό του RustDesk"),
|
("verify_rustdesk_password_tip", "Επιβεβαιώστε τον κωδικό του RustDesk"),
|
||||||
("remember_account_tip", "Απομνημόνευση αυτού του λογαριασμού"),
|
("remember_account_tip", "Απομνημόνευση αυτού του λογαριασμού"),
|
||||||
("os_account_desk_tip", "Αυτός ο λογαριασμός θα χρησιμοποιηθεί για την είσοδο και διαχείριση του απομακρυσμένου λειτουργικού συστήματος"),
|
("os_account_desk_tip", "Αυτός ο λογαριασμός χρησιμοποιείται για σύνδεση στο απομακρυσμένο λειτουργικό σύστημα και ενεργοποίηση της συνεδρίας επιφάνειας εργασίας σε headless"),
|
||||||
("OS Account", "Λογαριασμός λειτουργικού συστήματος"),
|
("OS Account", "Λογαριασμός λειτουργικού συστήματος"),
|
||||||
("another_user_login_title_tip", "Υπάρχει ήδη άλλος συνδεδεμένος χρήστης"),
|
("another_user_login_title_tip", "Υπάρχει ήδη άλλος συνδεδεμένος χρήστης"),
|
||||||
("another_user_login_text_tip", "Αποσύνδεση"),
|
("another_user_login_text_tip", "Αποσύνδεση"),
|
||||||
("xorg_not_found_title_tip", "Δεν βρέθηκε το Xorg"),
|
("xorg_not_found_title_tip", "Δεν βρέθηκε το Xorg"),
|
||||||
("xorg_not_found_text_tip", "Παρακαλώ εγκαταστήστε το Xorg"),
|
("xorg_not_found_text_tip", "Παρακαλώ εγκαταστήστε το Xorg"),
|
||||||
("no_desktop_title_tip", "Δεν υπάρχει διαθέσιμη επιφάνεια εργασίας"),
|
("no_desktop_title_tip", "Δεν υπάρχει διαθέσιμο περιβάλλον επιφάνειας εργασίας"),
|
||||||
("no_desktop_text_tip", "Παρακαλώ εγκαταστήστε το περιβάλλον GNOME"),
|
("no_desktop_text_tip", "Παρακαλώ εγκαταστήστε το περιβάλλον GNOME"),
|
||||||
("No need to elevate", "Δεν χρειάζονται αυξημένα δικαιώματα"),
|
("No need to elevate", "Δεν χρειάζεται ανύψωση"),
|
||||||
("System Sound", "Ήχος συστήματος"),
|
("System Sound", "Ήχος συστήματος"),
|
||||||
("Default", "Προκαθορισμένο"),
|
("Default", "Προκαθορισμένο"),
|
||||||
("New RDP", "Νέα απομακρυσμένη σύνδεση"),
|
("New RDP", "Νέα RDP"),
|
||||||
("Fingerprint", ""),
|
("Fingerprint", "Δακτυλικό αποτύπωμα"),
|
||||||
("Copy Fingerprint", ""),
|
("Copy Fingerprint", "Αντιγραφή δακτυλικού αποτυπώματος"),
|
||||||
("no fingerprints", ""),
|
("no fingerprints", "χωρίς δακτυλικά αποτυπώματα"),
|
||||||
("Select a peer", "Επιλέξτε σταθμό"),
|
("Select a peer", "Επιλέξτε έναν σταθμό"),
|
||||||
("Select peers", "Επιλέξτε σταθμούς"),
|
("Select peers", "Επιλέξτε σταθμούς"),
|
||||||
("Plugins", "Επεκτάσεις"),
|
("Plugins", "Επεκτάσεις"),
|
||||||
("Uninstall", "Κατάργηση εγκατάστασης"),
|
("Uninstall", "Κατάργηση εγκατάστασης"),
|
||||||
@@ -493,10 +494,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("resolution_original_tip", "Αρχική ανάλυση"),
|
("resolution_original_tip", "Αρχική ανάλυση"),
|
||||||
("resolution_fit_local_tip", "Προσαρμογή στην τοπική ανάλυση"),
|
("resolution_fit_local_tip", "Προσαρμογή στην τοπική ανάλυση"),
|
||||||
("resolution_custom_tip", "Προσαρμοσμένη ανάλυση"),
|
("resolution_custom_tip", "Προσαρμοσμένη ανάλυση"),
|
||||||
("Collapse toolbar", "Εμφάνιση γραμμής εργαλείων"),
|
("Collapse toolbar", "Σύμπτυξη γραμμής εργαλείων"),
|
||||||
("Accept and Elevate", "Αποδοχή με αυξημένα δικαιώματα"),
|
("Accept and Elevate", "Αποδοχή και ανύψωση"),
|
||||||
("accept_and_elevate_btn_tooltip", "Αποδοχή της σύνδεσης με αυξημένα δικαιώματα χρήστη"),
|
("accept_and_elevate_btn_tooltip", "Αποδεχτείτε τη σύνδεση και ανυψώστε τα δικαιώματα UAC."),
|
||||||
("clipboard_wait_response_timeout_tip", "Έληξε ο χρόνος αναμονής για την ανταπόκριση της αντιγραφής"),
|
("clipboard_wait_response_timeout_tip", "Λήξη χρονικού ορίου αναμονής για απάντηση αντιγραφής."),
|
||||||
("Incoming connection", "Εισερχόμενη σύνδεση"),
|
("Incoming connection", "Εισερχόμενη σύνδεση"),
|
||||||
("Outgoing connection", "Εξερχόμενη σύνδεση"),
|
("Outgoing connection", "Εξερχόμενη σύνδεση"),
|
||||||
("Exit", "Έξοδος"),
|
("Exit", "Έξοδος"),
|
||||||
@@ -505,7 +506,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Service", "Υπηρεσία"),
|
("Service", "Υπηρεσία"),
|
||||||
("Start", "Έναρξη"),
|
("Start", "Έναρξη"),
|
||||||
("Stop", "Διακοπή"),
|
("Stop", "Διακοπή"),
|
||||||
("exceed_max_devices", "Υπέρβαση μέγιστου ορίου αποθηκευμένων συνδέσεων"),
|
("exceed_max_devices", "Έχετε φτάσει τον μέγιστο αριθμό διαχειριζόμενων συσκευών."),
|
||||||
("Sync with recent sessions", "Συγχρονισμός των πρόσφατων συνεδριών"),
|
("Sync with recent sessions", "Συγχρονισμός των πρόσφατων συνεδριών"),
|
||||||
("Sort tags", "Ταξινόμηση ετικετών"),
|
("Sort tags", "Ταξινόμηση ετικετών"),
|
||||||
("Open connection in new tab", "Άνοιγμα σύνδεσης σε νέα καρτέλα"),
|
("Open connection in new tab", "Άνοιγμα σύνδεσης σε νέα καρτέλα"),
|
||||||
@@ -514,14 +515,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Already exists", "Υπάρχει ήδη"),
|
("Already exists", "Υπάρχει ήδη"),
|
||||||
("Change Password", "Αλλαγή κωδικού"),
|
("Change Password", "Αλλαγή κωδικού"),
|
||||||
("Refresh Password", "Ανανέωση κωδικού"),
|
("Refresh Password", "Ανανέωση κωδικού"),
|
||||||
("ID", ""),
|
("ID", "ID"),
|
||||||
("Grid View", "Προβολή σε πλακίδια"),
|
("Grid View", "Προβολή σε πλακίδια"),
|
||||||
("List View", "Προβολή σε λίστα"),
|
("List View", "Προβολή σε λίστα"),
|
||||||
("Select", "Επιλογή"),
|
("Select", "Επιλογή"),
|
||||||
("Toggle Tags", "Εναλλαγή ετικετών"),
|
("Toggle Tags", "Εναλλαγή ετικετών"),
|
||||||
("pull_ab_failed_tip", "Αποτυχία ανανέωσης βιβλίου διευθύνσεων"),
|
("pull_ab_failed_tip", "Η ανανέωση του βιβλίου διευθύνσεων απέτυχε"),
|
||||||
("push_ab_failed_tip", "Αποτυχία συγχρονισμού βιβλίου διευθύνσεων"),
|
("push_ab_failed_tip", "Αποτυχία συγχρονισμού του βιβλίου διευθύνσεων με τον διακομιστή"),
|
||||||
("synced_peer_readded_tip", "Οι συσκευές των τρεχουσών συνεδριών θα συγχρονιστούν με το βιβλίο διευθύνσεων"),
|
("synced_peer_readded_tip", "Οι συσκευές που υπήρχαν στις πρόσφατες συνεδρίες θα συγχρονιστούν ξανά με το βιβλίο διευθύνσεων."),
|
||||||
("Change Color", "Αλλαγή χρώματος"),
|
("Change Color", "Αλλαγή χρώματος"),
|
||||||
("Primary Color", "Κυρίως χρώμα"),
|
("Primary Color", "Κυρίως χρώμα"),
|
||||||
("HSV Color", "Χρώμα HSV"),
|
("HSV Color", "Χρώμα HSV"),
|
||||||
@@ -536,31 +537,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("I Agree", "Συμφωνώ"),
|
("I Agree", "Συμφωνώ"),
|
||||||
("Decline", "Διαφωνώ"),
|
("Decline", "Διαφωνώ"),
|
||||||
("Timeout in minutes", "Τέλος χρόνου σε λεπτά"),
|
("Timeout in minutes", "Τέλος χρόνου σε λεπτά"),
|
||||||
("auto_disconnect_option_tip", "Αυτόματη αποσύνδεση απομακρυσμένης συνεδρίας έπειτα από την πάροδο του χρονικού ορίου αδράνειας "),
|
("auto_disconnect_option_tip", "Αυτόματο κλείσιμο εισερχόμενων συνεδριών σε περίπτωση αδράνειας χρήστη"),
|
||||||
("Connection failed due to inactivity", "Η σύνδεση τερματίστηκε έπειτα από την πάροδο του χρόνου αδράνειας"),
|
("Connection failed due to inactivity", "Η σύνδεση τερματίστηκε έπειτα από την πάροδο του χρόνου αδράνειας"),
|
||||||
("Check for software update on startup", "Έλεγχος για ενημερώσεις κατα την εκκίνηση"),
|
("Check for software update on startup", "Έλεγχος για ενημερώσεις κατά την εκκίνηση"),
|
||||||
("upgrade_rustdesk_server_pro_to_{}_tip", "Παρακαλώ ενημερώστε τον RustDesk Server Pro στην έκδοση {} ή νεότερη!"),
|
("upgrade_rustdesk_server_pro_to_{}_tip", "Παρακαλώ ενημερώστε το RustDesk Server Pro στην έκδοση {} ή νεότερη!"),
|
||||||
("pull_group_failed_tip", "Αποτυχία ανανέωσης της ομάδας"),
|
("pull_group_failed_tip", "Αποτυχία ανανέωσης της ομάδας"),
|
||||||
("Filter by intersection", ""),
|
("Filter by intersection", "Φιλτράρισμα κατά διασταύρωση"),
|
||||||
("Remove wallpaper during incoming sessions", "Αφαίρεση εικόνας φόντου στις εισερχόμενες συνδέσεις"),
|
("Remove wallpaper during incoming sessions", "Αφαίρεση εικόνας φόντου στις εισερχόμενες συνδέσεις"),
|
||||||
("Test", "Δοκιμή"),
|
("Test", "Δοκιμή"),
|
||||||
("display_is_plugged_out_msg", "Η οθόνη έχει αποσυνδεθεί, επιστρέψτε στην κύρια οθόνη προβολής"),
|
("display_is_plugged_out_msg", "Η οθόνη είναι αποσυνδεδεμένη από την πρίζα, μεταβείτε στην πρώτη οθόνη."),
|
||||||
("No displays", "Δεν υπάρχουν οθόνες"),
|
("No displays", "Δεν υπάρχουν οθόνες"),
|
||||||
("Open in new window", "Άνοιγμα σε νέο παράθυρο"),
|
("Open in new window", "Άνοιγμα σε νέο παράθυρο"),
|
||||||
("Show displays as individual windows", "Εμφάνιση οθονών σε ξεχωριστά παράθυρα"),
|
("Show displays as individual windows", "Εμφάνιση οθονών σε ξεχωριστά παράθυρα"),
|
||||||
("Use all my displays for the remote session", "Χρήση όλων των οθονών της απομακρυσμένης σύνδεσης"),
|
("Use all my displays for the remote session", "Χρήση όλων των οθονών της απομακρυσμένης σύνδεσης"),
|
||||||
("selinux_tip", "Έχετε ενεργοποιημένο το SELinux, το οποίο πιθανόν εμποδίζει την ορθή λειτουργία του RustDesk."),
|
("selinux_tip", "Το SELinux είναι ενεργοποιημένο στη συσκευή σας, κάτι που ενδέχεται να εμποδίσει την σωστή λειτουργία του RustDesk ως ελεγχόμενης πλευράς."),
|
||||||
("Change view", "Αλλαγή απεικόνισης"),
|
("Change view", "Αλλαγή απεικόνισης"),
|
||||||
("Big tiles", "Μεγάλα εικονίδια"),
|
("Big tiles", "Μεγάλα εικονίδια"),
|
||||||
("Small tiles", "Μικρά εικονίδια"),
|
("Small tiles", "Μικρά εικονίδια"),
|
||||||
("List", "Λίστα"),
|
("List", "Λίστα"),
|
||||||
("Virtual display", "Εινονική οθόνη"),
|
("Virtual display", "Εινονική οθόνη"),
|
||||||
("Plug out all", "Αποσύνδεση όλων"),
|
("Plug out all", "Αποσύνδεση όλων"),
|
||||||
("True color (4:4:4)", ""),
|
("True color (4:4:4)", "Αληθινό χρώμα (4:4:4)"),
|
||||||
("Enable blocking user input", "Ενεργοποίηση αποκλεισμού χειρισμού από τον χρήστη"),
|
("Enable blocking user input", "Ενεργοποίηση αποκλεισμού χειρισμού από τον χρήστη"),
|
||||||
("id_input_tip", "Μπορείτε να εισάγετε ενα ID, μια διεύθυνση IP, ή ένα όνομα τομέα με την αντίστοιχη πόρτα (<domain>:<port>).\nΑν θέλετε να συνδεθείτε σε μια συσκευή σε άλλο διακομιστή, παρακαλώ να προσθέσετε και την διεύθυνση του διακομιστή (<id>@<server_address>?key=<key_value>), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΑν θέλετε να συνδεθείτε σε κάποιο δημόσιο διακομιστή, προσθέστε το όνομά του \"<id>@public\", η παράμετρος key δεν απαιτείται για τους δημόσιους διακομιστές."),
|
("id_input_tip", "Μπορείτε να εισάγετε ένα ID, μια διεύθυνση IP, ή ένα όνομα τομέα με την αντίστοιχη πόρτα (<domain>:<port>).\nΑν θέλετε να συνδεθείτε σε μια συσκευή σε άλλο διακομιστή, παρακαλώ να προσθέσετε και την διεύθυνση του διακομιστή (<id>@<server_address>?key=<key_value>), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΑν θέλετε να συνδεθείτε σε κάποιο δημόσιο διακομιστή, προσθέστε το όνομά του \"<id>@public\", η παράμετρος key δεν απαιτείται για τους δημόσιους διακομιστές."),
|
||||||
("privacy_mode_impl_mag_tip", "Προφύλαξη Οθόνης"),
|
("privacy_mode_impl_mag_tip", "Λειτουργία 1"),
|
||||||
("privacy_mode_impl_virtual_display_tip", "Εικονική Οθόνη"),
|
("privacy_mode_impl_virtual_display_tip", "Λειτουργία 2"),
|
||||||
("Enter privacy mode", "Ενεργοποίηση λειτουργίας απορρήτου"),
|
("Enter privacy mode", "Ενεργοποίηση λειτουργίας απορρήτου"),
|
||||||
("Exit privacy mode", "Διακοπή λειτουργίας απορρήτου"),
|
("Exit privacy mode", "Διακοπή λειτουργίας απορρήτου"),
|
||||||
("idd_not_support_under_win10_2004_tip", "Το πρόγραμμα οδήγησης έμμεσης οθόνης δεν υποστηρίζεται. Απαιτείτε λειτουργικό σύστημα Windows 10 έκδοση 2004 ή νεότερο."),
|
("idd_not_support_under_win10_2004_tip", "Το πρόγραμμα οδήγησης έμμεσης οθόνης δεν υποστηρίζεται. Απαιτείτε λειτουργικό σύστημα Windows 10 έκδοση 2004 ή νεότερο."),
|
||||||
@@ -570,26 +571,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("swap-left-right-mouse", "Εναλλαγή αριστερό-δεξί κουμπί του ποντικιού"),
|
("swap-left-right-mouse", "Εναλλαγή αριστερό-δεξί κουμπί του ποντικιού"),
|
||||||
("2FA code", "κωδικός 2FA"),
|
("2FA code", "κωδικός 2FA"),
|
||||||
("More", "Περισσότερα"),
|
("More", "Περισσότερα"),
|
||||||
("enable-2fa-title", "Ενεργοποίηση Πιστοποίησης Δύο Παραγόντων"),
|
("enable-2fa-title", "Ενεργοποίηση πιστοποίησης δύο παραγόντων"),
|
||||||
("enable-2fa-desc", "Ρυθμίστε τον έλεγχο ταυτότητας τώρα. Μπορείτε να χρησιμοποιήσετε μια εφαρμογή ελέγχου ταυτότητας όπως Authy, Microsoft ή Google Authenticator στο τηλέφωνο ή στην επιφάνεια εργασίας σας.Σαρώστε τον κωδικό QR με την εφαρμογή σας και εισαγάγετε τον κωδικό που εμφανίζει η εφαρμογή σας για να ενεργοποιήσετε τον έλεγχο ταυτότητας δύο παραγόντων."),
|
("enable-2fa-desc", "Παρακαλούμε να ρυθμίστε τώρα τον έλεγχο ταυτότητας. Μπορείτε να χρησιμοποιήσετε μια εφαρμογή ελέγχου ταυτότητας όπως το Authy, το Microsoft ή το Google Authenticator στο τηλέφωνο ή τον υπολογιστή σας.\n\nΣαρώστε τον κωδικό QR με την εφαρμογή σας και εισαγάγετε τον κωδικό που εμφανίζει η εφαρμογή σας για να ενεργοποιήσετε τον έλεγχο ταυτότητας δύο παραγόντων."),
|
||||||
("wrong-2fa-code", "Δεν είναι δυνατή η επαλήθευση του κωδικού. Ελέγξτε ότι ο κωδικός και οι ρυθμίσεις τοπικής ώρας είναι σωστές"),
|
("wrong-2fa-code", "Δεν είναι δυνατή η επαλήθευση του κωδικού. Ελέγξτε ότι οι ρυθμίσεις κωδικού και τοπικής ώρας είναι σωστές."),
|
||||||
("enter-2fa-title", "Έλεγχος ταυτότητας δύο παραγόντων"),
|
("enter-2fa-title", "Έλεγχος ταυτότητας δύο παραγόντων"),
|
||||||
("Email verification code must be 6 characters.", "Ο κωδικός επαλήθευσης email πρέπει να είναι εως 6 χαρακτήρες"),
|
("Email verification code must be 6 characters.", "Ο κωδικός επαλήθευσης email πρέπει να είναι έως 6 χαρακτήρες"),
|
||||||
("2FA code must be 6 digits.", "Ο κωδικός 2FA πρέπει να είναι 6ψήφιος."),
|
("2FA code must be 6 digits.", "Ο κωδικός 2FA πρέπει να είναι 6ψήφιος."),
|
||||||
("Multiple Windows sessions found", ""),
|
("Multiple Windows sessions found", "Βρέθηκαν πολλές συνεδρίες των Windows"),
|
||||||
("Please select the session you want to connect to", "Επιλέξτε τη συνεδρία στην οποία θέλετε να συνδεθείτε"),
|
("Please select the session you want to connect to", "Επιλέξτε τη συνεδρία στην οποία θέλετε να συνδεθείτε"),
|
||||||
("powered_by_me", "Με την υποστήριξη της RustDesk"),
|
("powered_by_me", "Με την υποστήριξη του RustDesk"),
|
||||||
("outgoing_only_desk_tip", ""),
|
("outgoing_only_desk_tip", "Αυτή είναι μια προσαρμοσμένη έκδοση.\nΜπορείτε να συνδεθείτε με άλλες συσκευές, αλλά άλλες συσκευές δεν μπορούν να συνδεθούν με τη δική σας συσκευή."),
|
||||||
("preset_password_warning", "προειδοποίηση προκαθορισμένου κωδικού πρόσβασης"),
|
("preset_password_warning", "Αυτή η προσαρμοσμένη έκδοση συνοδεύεται από έναν προκαθορισμένο κωδικό πρόσβασης. Όποιος γνωρίζει αυτόν τον κωδικό πρόσβασης θα μπορούσε να αποκτήσει τον πλήρη έλεγχο της συσκευής σας. Εάν δεν το περιμένατε αυτό, απεγκαταστήστε αμέσως το λογισμικό."),
|
||||||
("Security Alert", "Ειδοποίηση ασφαλείας"),
|
("Security Alert", "Ειδοποίηση ασφαλείας"),
|
||||||
("My address book", "Το βιβλίο διευθύνσεών μου"),
|
("My address book", "Το βιβλίο διευθύνσεών μου"),
|
||||||
("Personal", "Προσωπικό"),
|
("Personal", "Προσωπικό"),
|
||||||
("Owner", "Ιδιοκτήτης"),
|
("Owner", "Ιδιοκτήτης"),
|
||||||
("Set shared password", "Ορίστε κοινόχρηστο κωδικό πρόσβασης"),
|
("Set shared password", "Ορίστε έναν κοινόχρηστο κωδικό πρόσβασης"),
|
||||||
("Exist in", "Υπάρχει στο"),
|
("Exist in", "Υπάρχει στο"),
|
||||||
("Read-only", "Μόνο για ανάγνωση"),
|
("Read-only", "Μόνο για ανάγνωση"),
|
||||||
("Read/Write", "Ανάγνωση/Εγγραφή"),
|
("Read/Write", "Ανάγνωση/Εγγραφή"),
|
||||||
("Full Control", "Πλήρης Έλεγχος"),
|
("Full Control", "Πλήρης έλεγχος"),
|
||||||
("share_warning_tip", "Τα παραπάνω πεδία είναι κοινόχρηστα και ορατά σε άλλους."),
|
("share_warning_tip", "Τα παραπάνω πεδία είναι κοινόχρηστα και ορατά σε άλλους."),
|
||||||
("Everyone", "Όλοι"),
|
("Everyone", "Όλοι"),
|
||||||
("ab_web_console_tip", "Περισσότερα στην κονσόλα web"),
|
("ab_web_console_tip", "Περισσότερα στην κονσόλα web"),
|
||||||
@@ -597,18 +598,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("no_need_privacy_mode_no_physical_displays_tip", "Δεν υπάρχουν φυσικές οθόνες, δεν χρειάζεται να χρησιμοποιήσετε τη λειτουργία απορρήτου."),
|
("no_need_privacy_mode_no_physical_displays_tip", "Δεν υπάρχουν φυσικές οθόνες, δεν χρειάζεται να χρησιμοποιήσετε τη λειτουργία απορρήτου."),
|
||||||
("Follow remote cursor", "Παρακολούθηση απομακρυσμένου κέρσορα"),
|
("Follow remote cursor", "Παρακολούθηση απομακρυσμένου κέρσορα"),
|
||||||
("Follow remote window focus", "Παρακολούθηση απομακρυσμένου ενεργού παραθύρου"),
|
("Follow remote window focus", "Παρακολούθηση απομακρυσμένου ενεργού παραθύρου"),
|
||||||
("default_proxy_tip", "Προκαθορισμένο πρωτόκολλο Socks5 στην πόρτα 1080"),
|
("default_proxy_tip", "Το προεπιλεγμένο πρωτόκολλο και η θύρα είναι Socks5 και 1080"),
|
||||||
("no_audio_input_device_tip", "Δεν βρέθηκε συσκευή εισόδου ήχου."),
|
("no_audio_input_device_tip", "Δεν βρέθηκε συσκευή εισόδου ήχου."),
|
||||||
("Incoming", "Εισερχόμενη"),
|
("Incoming", "Εισερχόμενη"),
|
||||||
("Outgoing", "Εξερχόμενη"),
|
("Outgoing", "Εξερχόμενη"),
|
||||||
("Clear Wayland screen selection", ""),
|
("Clear Wayland screen selection", "Εκκαθάριση επιλογής οθόνης Wayland"),
|
||||||
("clear_Wayland_screen_selection_tip", ""),
|
("clear_Wayland_screen_selection_tip", "Αφού διαγράψετε την επιλογή οθόνης, μπορείτε να επιλέξετε ξανά την οθόνη για κοινή χρήση."),
|
||||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
("confirm_clear_Wayland_screen_selection_tip", "Είστε βέβαιοι ότι θέλετε να διαγράψετε την επιλογή οθόνης Wayland;"),
|
||||||
("android_new_voice_call_tip", ""),
|
("android_new_voice_call_tip", "Ελήφθη ένα νέο αίτημα φωνητικής κλήσης. Εάν το αποδεχτείτε, ο ήχος θα μεταβεί σε φωνητική επικοινωνία."),
|
||||||
("texture_render_tip", ""),
|
("texture_render_tip", "Χρησιμοποιήστε την απόδοση υφής για να κάνετε τις εικόνες πιο ομαλές. Μπορείτε να δοκιμάσετε να απενεργοποιήσετε αυτήν την επιλογή εάν αντιμετωπίσετε προβλήματα απόδοσης."),
|
||||||
("Use texture rendering", ""),
|
("Use texture rendering", "Χρήση απόδοσης υφής"),
|
||||||
("Floating window", ""),
|
("Floating window", "Πλωτό παράθυρο"),
|
||||||
("floating_window_tip", ""),
|
("floating_window_tip", "Βοηθά στη διατήρηση της υπηρεσίας παρασκηνίου RustDesk"),
|
||||||
("Keep screen on", "Διατήρηση οθόνης Ανοιχτή"),
|
("Keep screen on", "Διατήρηση οθόνης Ανοιχτή"),
|
||||||
("Never", "Ποτέ"),
|
("Never", "Ποτέ"),
|
||||||
("During controlled", "Κατα την διάρκεια απομακρυσμένου ελέγχου"),
|
("During controlled", "Κατα την διάρκεια απομακρυσμένου ελέγχου"),
|
||||||
@@ -618,8 +619,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Apps", "Εφαρμογές"),
|
("Apps", "Εφαρμογές"),
|
||||||
("Volume up", "Αύξηση έντασης"),
|
("Volume up", "Αύξηση έντασης"),
|
||||||
("Volume down", "Μείωση έντασης"),
|
("Volume down", "Μείωση έντασης"),
|
||||||
("Power", ""),
|
("Power", "Ενέργεια"),
|
||||||
("Telegram bot", ""),
|
("Telegram bot", "Telegram bot"),
|
||||||
("enable-bot-tip", "Εάν ενεργοποιήσετε αυτήν τη δυνατότητα, μπορείτε να λάβετε τον κωδικό 2FA από το bot σας. Μπορεί επίσης να λειτουργήσει ως ειδοποίηση σύνδεσης."),
|
("enable-bot-tip", "Εάν ενεργοποιήσετε αυτήν τη δυνατότητα, μπορείτε να λάβετε τον κωδικό 2FA από το bot σας. Μπορεί επίσης να λειτουργήσει ως ειδοποίηση σύνδεσης."),
|
||||||
("enable-bot-desc", "1, Ανοίξτε μια συνομιλία με τον @BotFather., Στείλτε την εντολή \"/newbot\". Θα λάβετε ένα διακριτικό αφού ολοκληρώσετε αυτό το βήμα.3, Ξεκινήστε μια συνομιλία με το bot που μόλις δημιουργήσατε. Στείλτε ένα μήνυμα που αρχίζει με κάθετο (\"/\") όπως \"/hello\" για να το ενεργοποιήσετε."),
|
("enable-bot-desc", "1, Ανοίξτε μια συνομιλία με τον @BotFather., Στείλτε την εντολή \"/newbot\". Θα λάβετε ένα διακριτικό αφού ολοκληρώσετε αυτό το βήμα.3, Ξεκινήστε μια συνομιλία με το bot που μόλις δημιουργήσατε. Στείλτε ένα μήνυμα που αρχίζει με κάθετο (\"/\") όπως \"/hello\" για να το ενεργοποιήσετε."),
|
||||||
("cancel-2fa-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το 2FA;"),
|
("cancel-2fa-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το 2FA;"),
|
||||||
@@ -639,11 +640,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Parent directory", "Γονικός φάκελος"),
|
("Parent directory", "Γονικός φάκελος"),
|
||||||
("Resume", "Συνέχεια"),
|
("Resume", "Συνέχεια"),
|
||||||
("Invalid file name", "Μη έγκυρο όνομα αρχείου"),
|
("Invalid file name", "Μη έγκυρο όνομα αρχείου"),
|
||||||
("one-way-file-transfer-tip", ""),
|
("one-way-file-transfer-tip", "Η μονόδρομη μεταφορά αρχείων είναι ενεργοποιημένη στην ελεγχόμενη πλευρά."),
|
||||||
("Authentication Required", "Απαιτείται έλεγχος ταυτότητας"),
|
("Authentication Required", "Απαιτείται έλεγχος ταυτότητας"),
|
||||||
("Authenticate", "Πιστοποίηση"),
|
("Authenticate", "Πιστοποίηση"),
|
||||||
("web_id_input_tip", ""),
|
("web_id_input_tip", "Μπορείτε να εισαγάγετε ένα ID στον ίδιο διακομιστή, η άμεση πρόσβαση IP δεν υποστηρίζεται στον web client.\nΕάν θέλετε να αποκτήσετε πρόσβαση σε μια συσκευή σε άλλον διακομιστή, παρακαλούμε να προσθέστε τη διεύθυνση διακομιστή (<id>@<server_address>?key=<key_value>), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΕάν θέλετε να αποκτήσετε πρόσβαση σε μια συσκευή σε δημόσιο διακομιστή, παρακαλούμε να εισαγάγετε \"<id>@public\". Το κλειδί δεν είναι απαραίτητο για δημόσιο διακομιστή."),
|
||||||
("Download", ""),
|
("Download", "Λήψη"),
|
||||||
("Upload folder", "Μεταφόρτωση φακέλου"),
|
("Upload folder", "Μεταφόρτωση φακέλου"),
|
||||||
("Upload files", "Μεταφόρτωση αρχείων"),
|
("Upload files", "Μεταφόρτωση αρχείων"),
|
||||||
("Clipboard is synchronized", "Το πρόχειρο έχει συγχρονιστεί"),
|
("Clipboard is synchronized", "Το πρόχειρο έχει συγχρονιστεί"),
|
||||||
@@ -652,92 +653,96 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"),
|
("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"),
|
||||||
("Accessible devices", "Προσβάσιμες συσκευές"),
|
("Accessible devices", "Προσβάσιμες συσκευές"),
|
||||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότερη στην απομακρυσμένη πλευρά!"),
|
("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότερη στην απομακρυσμένη πλευρά!"),
|
||||||
("d3d_render_tip", ""),
|
("d3d_render_tip", "Όταν είναι ενεργοποιημένη η απόδοση D3D, η οθόνη του τηλεχειριστηρίου ενδέχεται να είναι μαύρη σε ορισμένα μηχανήματα."),
|
||||||
("Use D3D rendering", ""),
|
("Use D3D rendering", "Χρήση απόδοσης D3D"),
|
||||||
("Printer", ""),
|
("Printer", "Εκτυπωτής"),
|
||||||
("printer-os-requirement-tip", ""),
|
("printer-os-requirement-tip", "Η λειτουργία εξερχόμενης εκτύπωσης του εκτυπωτή απαιτεί Windows 10 ή νεότερη έκδοση."),
|
||||||
("printer-requires-installed-{}-client-tip", ""),
|
("printer-requires-installed-{}-client-tip", "Για να χρησιμοποιήσετε την απομακρυσμένη εκτύπωση, πρέπει να εγκατασταθεί το {} σε αυτήν τη συσκευή."),
|
||||||
("printer-{}-not-installed-tip", ""),
|
("printer-{}-not-installed-tip", "Ο εκτυπωτής {} δεν είναι εγκατεστημένος."),
|
||||||
("printer-{}-ready-tip", ""),
|
("printer-{}-ready-tip", "Ο εκτυπωτής {} είναι εγκατεστημένος και έτοιμος για χρήση."),
|
||||||
("Install {} Printer", ""),
|
("Install {} Printer", "Εγκατάσταση εκτυπωτή {}"),
|
||||||
("Outgoing Print Jobs", ""),
|
("Outgoing Print Jobs", "Εξερχόμενες εργασίες εκτύπωσης"),
|
||||||
("Incoming Print Jobs", ""),
|
("Incoming Print Jobs", "Εισερχόμενες εργασίες εκτύπωσης"),
|
||||||
("Incoming Print Job", ""),
|
("Incoming Print Job", "Εισερχόμενη εργασία εκτύπωσης"),
|
||||||
("use-the-default-printer-tip", ""),
|
("use-the-default-printer-tip", "Χρήση του προεπιλεγμένου εκτυπωτή"),
|
||||||
("use-the-selected-printer-tip", ""),
|
("use-the-selected-printer-tip", "Χρήση του επιλεγμένου εκτυπωτή"),
|
||||||
("auto-print-tip", ""),
|
("auto-print-tip", "Εκτυπώστε αυτόματα χρησιμοποιώντας τον επιλεγμένο εκτυπωτή."),
|
||||||
("print-incoming-job-confirm-tip", ""),
|
("print-incoming-job-confirm-tip", "Λάβατε μια εργασία εκτύπωσης από απόσταση. Θέλετε να την εκτελέσετε από την πλευρά σας;"),
|
||||||
("remote-printing-disallowed-tile-tip", ""),
|
("remote-printing-disallowed-tile-tip", "Η απομακρυσμένη εκτύπωση δεν επιτρέπεται"),
|
||||||
("remote-printing-disallowed-text-tip", ""),
|
("remote-printing-disallowed-text-tip", "Οι ρυθμίσεις δικαιωμάτων της ελεγχόμενης πλευράς απαγορεύουν την Απομακρυσμένη Εκτύπωση."),
|
||||||
("save-settings-tip", ""),
|
("save-settings-tip", "Αποθήκευση ρυθμίσεων"),
|
||||||
("dont-show-again-tip", ""),
|
("dont-show-again-tip", "Να μην εμφανιστεί ξανά αυτό"),
|
||||||
("Take screenshot", ""),
|
("Take screenshot", "Λήψη στιγμιότυπου οθόνης"),
|
||||||
("Taking screenshot", ""),
|
("Taking screenshot", "Γίνεται λήψη στιγμιότυπου οθόνης"),
|
||||||
("screenshot-merged-screen-not-supported-tip", ""),
|
("screenshot-merged-screen-not-supported-tip", "Η συγχώνευση στιγμιότυπων οθόνης από πολλές οθόνες δεν υποστηρίζεται προς το παρόν. Αλλάξτε σε μία μόνο οθόνη και δοκιμάστε ξανά."),
|
||||||
("screenshot-action-tip", ""),
|
("screenshot-action-tip", "Επιλέξτε πώς θα συνεχίσετε με το στιγμιότυπο οθόνης."),
|
||||||
("Save as", ""),
|
("Save as", "Αποθήκευση ως"),
|
||||||
("Copy to clipboard", ""),
|
("Copy to clipboard", "Αντιγραφή στο πρόχειρο"),
|
||||||
("Enable remote printer", ""),
|
("Enable remote printer", "Ενεργοποίηση απομακρυσμένου εκτυπωτή"),
|
||||||
("Downloading {}", ""),
|
("Downloading {}", "Γίνεται Λήψη {}"),
|
||||||
("{} Update", ""),
|
("{} Update", "{} Ενημέρωση"),
|
||||||
("{}-to-update-tip", ""),
|
("{}-to-update-tip", "Το {} θα κλείσει τώρα και θα εγκαταστήσει τη νέα έκδοση."),
|
||||||
("download-new-version-failed-tip", ""),
|
("download-new-version-failed-tip", "Η λήψη απέτυχε. Μπορείτε να δοκιμάσετε ξανά ή να κάνετε κλικ στο κουμπί \"Λήψη\" για να κάνετε λήψη από τη σελίδα έκδοσης και να κάνετε αναβάθμιση χειροκίνητα."),
|
||||||
("Auto update", ""),
|
("Auto update", "Αυτόματη ενημέρωση"),
|
||||||
("update-failed-check-msi-tip", ""),
|
("update-failed-check-msi-tip", "Η μέθοδος εγκατάστασης απέτυχε. Κάντε κλικ στο κουμπί \"Λήψη\" για λήψη από τη σελίδα έκδοσης και κάντε χειροκίνητα την αναβάθμιση."),
|
||||||
("websocket_tip", ""),
|
("websocket_tip", "Όταν χρησιμοποιείτε το WebSocket, υποστηρίζονται μόνο συνδέσεις αναμετάδοσης."),
|
||||||
("Use WebSocket", ""),
|
("Use WebSocket", "Χρήση WebSocket"),
|
||||||
("Trackpad speed", ""),
|
("Trackpad speed", "Ταχύτητα trackpad"),
|
||||||
("Default trackpad speed", ""),
|
("Default trackpad speed", "Προεπιλεγμένη ταχύτητα trackpad"),
|
||||||
("Numeric one-time password", ""),
|
("Numeric one-time password", "Αριθμητικός κωδικός πρόσβασης μίας χρήσης"),
|
||||||
("Enable IPv6 P2P connection", ""),
|
("Enable IPv6 P2P connection", "Ενεργοποίηση σύνδεσης IPv6 P2P"),
|
||||||
("Enable UDP hole punching", ""),
|
("Enable UDP hole punching", "Ενεργοποίηση διάτρησης οπών UDP"),
|
||||||
("View camera", "Προβολή κάμερας"),
|
("View camera", "Προβολή κάμερας"),
|
||||||
("Enable camera", ""),
|
("Enable camera", "Ενεργοποίηση κάμερας"),
|
||||||
("No cameras", ""),
|
("No cameras", "Δεν υπάρχουν κάμερες"),
|
||||||
("view_camera_unsupported_tip", ""),
|
("view_camera_unsupported_tip", "Η τηλεχειριστήριο δεν υποστηρίζει την προβολή της κάμερας."),
|
||||||
("Terminal", ""),
|
("Terminal", "Τερματικό"),
|
||||||
("Enable terminal", ""),
|
("Enable terminal", "Ενεργοποίηση τερματικού"),
|
||||||
("New tab", ""),
|
("New tab", "Νέα καρτέλα"),
|
||||||
("Keep terminal sessions on disconnect", ""),
|
("Keep terminal sessions on disconnect", "Διατήρηση περιόδων λειτουργίας τερματικού κατά την αποσύνδεση"),
|
||||||
("Terminal (Run as administrator)", ""),
|
("Terminal (Run as administrator)", "Τερματικό (Εκτέλεση ως διαχειριστής)"),
|
||||||
("terminal-admin-login-tip", ""),
|
("terminal-admin-login-tip", "Παρακαλώ εισάγετε το όνομα χρήστη και τον κωδικό πρόσβασης διαχειριστή της ελεγχόμενης πλευράς."),
|
||||||
("Failed to get user token.", ""),
|
("Failed to get user token.", "Αποτυχία λήψης διακριτικού χρήστη."),
|
||||||
("Incorrect username or password.", ""),
|
("Incorrect username or password.", "Λανθασμένο όνομα χρήστη ή κωδικός πρόσβασης."),
|
||||||
("The user is not an administrator.", ""),
|
("The user is not an administrator.", "Ο χρήστης δεν είναι διαχειριστής."),
|
||||||
("Failed to check if the user is an administrator.", ""),
|
("Failed to check if the user is an administrator.", "Αποτυχία ελέγχου εάν ο χρήστης είναι διαχειριστής."),
|
||||||
("Supported only in the installed version.", ""),
|
("Supported only in the installed version.", "Υποστηρίζεται μόνο στην εγκατεστημένη έκδοση."),
|
||||||
("elevation_username_tip", ""),
|
("elevation_username_tip", "Εισαγάγετε όνομα χρήστη ή τομέα\\όνομα χρήστη"),
|
||||||
("Preparing for installation ...", ""),
|
("Preparing for installation ...", "Προετοιμασία για εγκατάσταση..."),
|
||||||
("Show my cursor", ""),
|
("Show my cursor", "Εμφάνιση του κέρσορα μου"),
|
||||||
("Scale custom", ""),
|
("Scale custom", "Προσαρμοσμένη κλίμακα"),
|
||||||
("Custom scale slider", ""),
|
("Custom scale slider", "Ρυθμιστικό προσαρμοσμένης κλίμακας"),
|
||||||
("Decrease", ""),
|
("Decrease", "Μείωση"),
|
||||||
("Increase", ""),
|
("Increase", "Αύξηση"),
|
||||||
("Show virtual mouse", ""),
|
("Show virtual mouse", "Εμφάνιση εικονικού ποντικιού"),
|
||||||
("Virtual mouse size", ""),
|
("Virtual mouse size", "Μέγεθος εικονικού ποντικιού"),
|
||||||
("Small", ""),
|
("Small", "Μικρό"),
|
||||||
("Large", ""),
|
("Large", "Μεγάλο"),
|
||||||
("Show virtual joystick", ""),
|
("Show virtual joystick", "Εμφάνιση εικονικού joystick"),
|
||||||
("Edit note", ""),
|
("Edit note", "Επεξεργασία σημείωσης"),
|
||||||
("Alias", ""),
|
("Alias", "Ψευδώνυμο"),
|
||||||
("ScrollEdge", ""),
|
("ScrollEdge", "Άκρη κύλισης"),
|
||||||
("Allow insecure TLS fallback", ""),
|
("Allow insecure TLS fallback", "Να επιτρέπεται η μη ασφαλής εφεδρική λειτουργία TLS"),
|
||||||
("allow-insecure-tls-fallback-tip", ""),
|
("allow-insecure-tls-fallback-tip", "Από προεπιλογή, το RustDesk επαληθεύει το πιστοποιητικό διακομιστή για πρωτόκολλα που χρησιμοποιούν TLS.\nΜε ενεργοποιημένη αυτήν την επιλογή, το RustDesk θα παρακάμψει το βήμα επαλήθευσης και θα προχωρήσει σε περίπτωση αποτυχίας επαλήθευσης."),
|
||||||
("Disable UDP", ""),
|
("Disable UDP", "Απενεργοποίηση UDP"),
|
||||||
("disable-udp-tip", ""),
|
("disable-udp-tip", "Ελέγχει εάν θα χρησιμοποιείται μόνο TCP.\nΌταν είναι ενεργοποιημένη αυτή η επιλογή, το RustDesk δεν θα χρησιμοποιεί πλέον το UDP 21116, αλλά θα χρησιμοποιείται το TCP 21116."),
|
||||||
("server-oss-not-support-tip", ""),
|
("server-oss-not-support-tip", "ΣΗΜΕΙΩΣΗ: Το OSS του διακομιστή RustDesk δεν περιλαμβάνει αυτήν τη λειτουργία."),
|
||||||
("input note here", ""),
|
("input note here", "εισάγετε σημείωση εδώ"),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", "Ζητήστε σημείωση στο τέλος της σύνδεσης"),
|
||||||
("Show terminal extra keys", ""),
|
("Show terminal extra keys", "Εμφάνιση επιπλέον κλειδιών τερματικού"),
|
||||||
("Relative mouse mode", ""),
|
("Relative mouse mode", "Σχετική λειτουργία ποντικιού"),
|
||||||
("rel-mouse-not-supported-peer-tip", ""),
|
("rel-mouse-not-supported-peer-tip", "Η λειτουργία σχετικού ποντικιού δεν υποστηρίζεται από τον συνδεδεμένο ομότιμο υπολογιστή."),
|
||||||
("rel-mouse-not-ready-tip", ""),
|
("rel-mouse-not-ready-tip", "Η λειτουργία σχετικού ποντικιού δεν είναι ακόμη έτοιμη. Δοκιμάστε ξανά."),
|
||||||
("rel-mouse-lock-failed-tip", ""),
|
("rel-mouse-lock-failed-tip", "Αποτυχία κλειδώματος δρομέα. Η λειτουργία σχετικού ποντικιού έχει απενεργοποιηθεί."),
|
||||||
("rel-mouse-exit-{}-tip", ""),
|
("rel-mouse-exit-{}-tip", "Πιέστε {} για έξοδο."),
|
||||||
("rel-mouse-permission-lost-tip", ""),
|
("rel-mouse-permission-lost-tip", "Η άδεια πληκτρολογίου ανακλήθηκε. Η λειτουργία σχετικού ποντικιού απενεργοποιήθηκε."),
|
||||||
("Changelog", ""),
|
("Changelog", "Αρχείο αλλαγών"),
|
||||||
("keep-awake-during-outgoing-sessions-label", ""),
|
("keep-awake-during-outgoing-sessions-label", "Διατήρηση ενεργής οθόνης κατά τη διάρκεια εξερχόμενων συνεδριών"),
|
||||||
("keep-awake-during-incoming-sessions-label", ""),
|
("keep-awake-during-incoming-sessions-label", "Διατήρηση ενεργής οθόνης κατά τη διάρκεια των εισερχόμενων συνεδριών"),
|
||||||
("Continue with {}", "Συνέχεια με {}"),
|
("Continue with {}", "Συνέχεια με {}"),
|
||||||
|
("Display Name", "Εμφανιζόμενο όνομα"),
|
||||||
|
("password-hidden-tip", ""),
|
||||||
|
("preset-password-in-use-tip", ""),
|
||||||
|
("Enable privacy mode", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user