mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-18 22:59:26 +08:00
Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0450db203 | ||
|
|
3a75947553 | ||
|
|
c565849062 | ||
|
|
40e8f0d307 | ||
|
|
129f6c869b | ||
|
|
924aa515c6 | ||
|
|
c51771c854 | ||
|
|
c8b9031996 | ||
|
|
4da584055d | ||
|
|
bd22b01370 | ||
|
|
b35b48086a | ||
|
|
445e9ac285 | ||
|
|
7a3e1fe648 | ||
|
|
dfa9519d58 | ||
|
|
cc6f919080 | ||
|
|
2cdaca0fa3 | ||
|
|
6159449eba | ||
|
|
6088920f8d | ||
|
|
e8187588c1 | ||
|
|
289076aa70 | ||
|
|
547da31095 | ||
|
|
1bf4ef1f46 | ||
|
|
1212d9fa2d | ||
|
|
8c8a643cce | ||
|
|
675ffe0381 | ||
|
|
844caf8c15 | ||
|
|
0f6d28def7 | ||
|
|
0d3243e6dd | ||
|
|
53d11e99d7 | ||
|
|
defb3e6c73 | ||
|
|
ae8dfe84a0 | ||
|
|
5e920f0fd0 | ||
|
|
1a0814b201 | ||
|
|
ace98d98ad | ||
|
|
09083b3afa | ||
|
|
36e11c61a9 | ||
|
|
55187e9243 | ||
|
|
ae1c1a56e6 | ||
|
|
cdd58e77eb | ||
|
|
ce924cc0d3 | ||
|
|
498b8ba3d6 | ||
|
|
af610b2408 | ||
|
|
6cdbcfc082 | ||
|
|
9c7f51bc76 | ||
|
|
65683cc3e6 | ||
|
|
eb1ef0969c | ||
|
|
29b01e9cef | ||
|
|
cde7620eda | ||
|
|
844b853074 | ||
|
|
97f02ed25e | ||
|
|
22c84bbbd1 | ||
|
|
227f154ee7 | ||
|
|
59d7bf1e86 | ||
|
|
38fcf4e039 | ||
|
|
4b3b31147e | ||
|
|
e6d4067f48 | ||
|
|
507de628c9 | ||
|
|
2591d4f044 | ||
|
|
9bcd0d1b03 | ||
|
|
5555ba6b2f | ||
|
|
28b6bc186f | ||
|
|
00d38260e1 | ||
|
|
e06f456bbd | ||
|
|
cc860b2906 | ||
|
|
839e8180e0 | ||
|
|
83aba804d0 | ||
|
|
560c1effe8 | ||
|
|
e7353be0cd | ||
|
|
ba832362a7 | ||
|
|
9ea09c1515 | ||
|
|
3a97b63e95 | ||
|
|
dec3cde9b3 | ||
|
|
c4d0b02478 | ||
|
|
306dd77b81 | ||
|
|
fd62751cb8 | ||
|
|
b0edfb8f70 | ||
|
|
334526026c | ||
|
|
b5414ec002 | ||
|
|
4eca8b9447 | ||
|
|
1ebc726acd | ||
|
|
d563372a91 | ||
|
|
4a745d82f6 | ||
|
|
2f5f701dc7 | ||
|
|
60a0099ba0 | ||
|
|
30a7847100 | ||
|
|
1e822fa135 | ||
|
|
f6261883e8 | ||
|
|
3365844def | ||
|
|
769bbf1e1c | ||
|
|
81b999cfbe | ||
|
|
9959217cc3 | ||
|
|
3e6938bec6 | ||
|
|
4459406578 | ||
|
|
beb1084e87 | ||
|
|
d4184fd865 | ||
|
|
ffc73f86a0 | ||
|
|
c74bdcdfdb | ||
|
|
6d8b5b289f | ||
|
|
1d6873f622 | ||
|
|
7c55e3266b | ||
|
|
ce5151032e | ||
|
|
ba88bc9e8b | ||
|
|
e0095aebda | ||
|
|
664a3e186e | ||
|
|
e4f7e126e5 | ||
|
|
49989e34e4 | ||
|
|
75a14fea23 | ||
|
|
f535406962 | ||
|
|
f3f3bb538f | ||
|
|
8fefd34c15 | ||
|
|
d98f947824 | ||
|
|
5f52ce2c1b | ||
|
|
1d799483d7 | ||
|
|
3db55a718c |
4
.github/workflows/flutter-build.yml
vendored
4
.github/workflows/flutter-build.yml
vendored
@@ -33,8 +33,8 @@ env:
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2024.07.12
|
||||
VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1"
|
||||
VERSION: "1.3.1"
|
||||
NDK_VERSION: "r27"
|
||||
VERSION: "1.3.2"
|
||||
NDK_VERSION: "r27b"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||
|
||||
2
.github/workflows/playground.yml
vendored
2
.github/workflows/playground.yml
vendored
@@ -18,7 +18,7 @@ env:
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2024.06.15
|
||||
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||
VERSION: "1.3.1"
|
||||
VERSION: "1.3.2"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -54,3 +54,4 @@ examples/**/target/
|
||||
vcpkg_installed
|
||||
flutter/lib/generated_plugin_registrant.dart
|
||||
libsciter.dylib
|
||||
flutter/web/
|
||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -3051,7 +3051,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
[[package]]
|
||||
name = "hwcodec"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/rustdesk-org/hwcodec#f74410edec91435252b8394c38f8eeca87ad2a26"
|
||||
source = "git+https://github.com/rustdesk-org/hwcodec#8bbd05bb300ad07cc345356ad85570f9ea99fbfa"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
@@ -5205,7 +5205,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rdev"
|
||||
version = "0.5.0-2"
|
||||
source = "git+https://github.com/rustdesk-org/rdev#d4c1759926d693ba269e2cb8cf9f87b13e424e4e"
|
||||
source = "git+https://github.com/rustdesk-org/rdev#961d25cc00c6b3ef80f444e6a7bed9872e2c35ea"
|
||||
dependencies = [
|
||||
"cocoa 0.24.1",
|
||||
"core-foundation 0.9.4",
|
||||
@@ -5480,7 +5480,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk"
|
||||
version = "1.3.1"
|
||||
version = "1.3.2"
|
||||
dependencies = [
|
||||
"android-wakelock",
|
||||
"android_logger",
|
||||
@@ -5580,7 +5580,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.3.1"
|
||||
version = "1.3.2"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"dirs 5.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.3.1"
|
||||
version = "1.3.2"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.3.1
|
||||
version: 1.3.2
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.3.1
|
||||
version: 1.3.2
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
2
build.py
2
build.py
@@ -283,6 +283,8 @@ def generate_control_file(version):
|
||||
system2('/bin/rm -rf %s' % control_file_path)
|
||||
|
||||
content = """Package: rustdesk
|
||||
Section: net
|
||||
Priority: optional
|
||||
Version: %s
|
||||
Architecture: %s
|
||||
Maintainer: rustdesk <info@rustdesk.com>
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br>
|
||||
<a href="#безкоштовні-загальнодоступні-сервери">Сервери</a> •
|
||||
<a href="#публічні-сервери">Сервери</a> •
|
||||
<a href="#кроки-для-збірки">Збирання</a> •
|
||||
<a href="#як-зібрати-за-допомогою-docker">Docker</a> •
|
||||
<a href="#структура-файлів">Структура</a> •
|
||||
<a href="#знімки">Знімки</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
|
||||
<b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk на вашу рідну мову</B>
|
||||
<a href="#знімки-екрана">Знімки екрана</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>]<br>
|
||||
<b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk вашою рідною мовою</B>
|
||||
</p>
|
||||
|
||||
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
[](https://console.algora.io/org/rustdesk/bounties?status=open)
|
||||
|
||||
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
@@ -61,19 +59,19 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT
|
||||
```sh
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
@@ -158,18 +156,22 @@ target/release/rustdesk
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: реалізація копіювання та вставлення файлів для Windows, Linux, macOS.
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача
|
||||
- **[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/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого з'єднання
|
||||
- **[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-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого зʼєднання
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для Flutter веб клієнту
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter
|
||||
|
||||
## Знімки
|
||||
## Знімки екрана
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## [Публічні сервери](#публічні-сервери)
|
||||
|
||||
RustDesk підтримується безкоштовним європейським сервером, любʼязно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
</p>
|
||||
|
||||
Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
@@ -32,7 +32,9 @@ RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-Z
|
||||
|
||||
## 依赖
|
||||
|
||||
桌面版本界面使用[sciter](https://sciter.com/), 请自行下载。
|
||||
桌面版本使用 Flutter 或 Sciter(已弃用)作为 GUI,本教程仅适用于 Sciter,因为它更简单且更易于上手。查看我们的[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)以构建 Flutter 版本。
|
||||
|
||||
请自行下载Sciter动态库。
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
@@ -207,12 +209,13 @@ target/release/rustdesk
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS 的文件复制和粘贴实现
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 过时的 Sciter UI(已弃用)
|
||||
- **[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/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
|
||||
- **[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/js)**: Flutter Web版本中的Javascript代码
|
||||
|
||||
## 截图
|
||||
|
||||
1
flutter/assets/message_24dp_5F6368.svg
Normal file
1
flutter/assets/message_24dp_5F6368.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="-4 -4 32 32" width="24px" fill="#5f6368"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></svg>
|
||||
|
After Width: | Height: | Size: 277 B |
@@ -302,6 +302,7 @@ prebuild)
|
||||
|
||||
sed \
|
||||
-i \
|
||||
-e 's/extended_text: .*/extended_text: 11.1.0/' \
|
||||
-e 's/uni_links_desktop/#uni_links_desktop/g' \
|
||||
flutter/pubspec.yaml
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import 'common/widgets/overlay.dart';
|
||||
import 'mobile/pages/file_manager_page.dart';
|
||||
import 'mobile/pages/remote_page.dart';
|
||||
import 'desktop/pages/remote_page.dart' as desktop_remote;
|
||||
import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
import 'models/model.dart';
|
||||
import 'models/platform_model.dart';
|
||||
@@ -554,7 +555,7 @@ class MyTheme {
|
||||
return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
|
||||
}
|
||||
|
||||
static void changeDarkMode(ThemeMode mode) async {
|
||||
static Future<void> changeDarkMode(ThemeMode mode) async {
|
||||
Get.changeThemeMode(mode);
|
||||
if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) {
|
||||
if (mode == ThemeMode.system) {
|
||||
@@ -680,10 +681,12 @@ closeConnection({String? id}) {
|
||||
overlays: SystemUiOverlay.values);
|
||||
gFFI.chatModel.hideChatOverlay();
|
||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||
stateGlobal.isInMainPage = true;
|
||||
}();
|
||||
} else {
|
||||
if (isWeb) {
|
||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||
stateGlobal.isInMainPage = true;
|
||||
} else {
|
||||
final controller = Get.find<DesktopTabController>();
|
||||
controller.closeBy(id);
|
||||
@@ -1171,33 +1174,21 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
||||
dialogManager.dismissAll();
|
||||
}));
|
||||
}
|
||||
if (reconnect != null && title == "Connection Error") {
|
||||
if (reconnect != null &&
|
||||
title == "Connection Error" &&
|
||||
reconnectTimeout != null) {
|
||||
// `enabled` is used to disable the dialog button once the button is clicked.
|
||||
final enabled = true.obs;
|
||||
final button = reconnectTimeout != null
|
||||
? Obx(() => _ReconnectCountDownButton(
|
||||
second: reconnectTimeout,
|
||||
onPressed: enabled.isTrue
|
||||
? () {
|
||||
// Disable the button
|
||||
enabled.value = false;
|
||||
reconnect(dialogManager, sessionId, false);
|
||||
}
|
||||
: null,
|
||||
))
|
||||
: Obx(
|
||||
() => dialogButton(
|
||||
'Reconnect',
|
||||
isOutline: true,
|
||||
onPressed: enabled.isTrue
|
||||
? () {
|
||||
// Disable the button
|
||||
enabled.value = false;
|
||||
reconnect(dialogManager, sessionId, false);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
final button = Obx(() => _ReconnectCountDownButton(
|
||||
second: reconnectTimeout,
|
||||
onPressed: enabled.isTrue
|
||||
? () {
|
||||
// Disable the button
|
||||
enabled.value = false;
|
||||
reconnect(dialogManager, sessionId, false);
|
||||
}
|
||||
: null,
|
||||
));
|
||||
buttons.insert(0, button);
|
||||
}
|
||||
if (link.isNotEmpty) {
|
||||
@@ -2035,6 +2026,8 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
return false;
|
||||
}
|
||||
|
||||
var webInitialLink = "";
|
||||
|
||||
/// Initialize uni links for macos/windows
|
||||
///
|
||||
/// [Availability]
|
||||
@@ -2051,7 +2044,12 @@ Future<bool> initUniLinks() async {
|
||||
if (initialLink == null || initialLink.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return handleUriLink(uriString: initialLink);
|
||||
if (isWeb) {
|
||||
webInitialLink = initialLink;
|
||||
return false;
|
||||
} else {
|
||||
return handleUriLink(uriString: initialLink);
|
||||
}
|
||||
} catch (err) {
|
||||
debugPrintStack(label: "$err");
|
||||
return false;
|
||||
@@ -2064,7 +2062,7 @@ Future<bool> initUniLinks() async {
|
||||
///
|
||||
/// Returns a [StreamSubscription] which can listen the uni links.
|
||||
StreamSubscription? listenUniLinks({handleByFlutter = true}) {
|
||||
if (isLinux) {
|
||||
if (isLinux || isWeb) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2294,16 +2292,19 @@ connectMainDesktop(String id,
|
||||
required bool isRDP,
|
||||
bool? forceRelay,
|
||||
String? password,
|
||||
String? connToken,
|
||||
bool? isSharedPassword}) async {
|
||||
if (isFileTransfer) {
|
||||
await rustDeskWinManager.newFileTransfer(id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else if (isTcpTunneling || isRDP) {
|
||||
await rustDeskWinManager.newPortForward(id, isRDP,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else {
|
||||
await rustDeskWinManager.newRemoteDesktop(id,
|
||||
@@ -2323,6 +2324,7 @@ connect(BuildContext context, String id,
|
||||
bool isRDP = false,
|
||||
bool forceRelay = false,
|
||||
String? password,
|
||||
String? connToken,
|
||||
bool? isSharedPassword}) async {
|
||||
if (id == '') return;
|
||||
if (!isDesktop || desktopType == DesktopType.main) {
|
||||
@@ -2364,24 +2366,40 @@ connect(BuildContext context, String id,
|
||||
'password': password,
|
||||
'isSharedPassword': isSharedPassword,
|
||||
'forceRelay': forceRelay,
|
||||
'connToken': connToken,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (isFileTransfer) {
|
||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
|
||||
return;
|
||||
if (isAndroid) {
|
||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => FileManagerPage(
|
||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||
),
|
||||
);
|
||||
if (isWeb) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) =>
|
||||
desktop_file_manager.FileManagerPage(
|
||||
id: id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => FileManagerPage(
|
||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (isWebDesktop) {
|
||||
if (isWeb) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -2405,6 +2423,7 @@ connect(BuildContext context, String id,
|
||||
);
|
||||
}
|
||||
}
|
||||
stateGlobal.isInMainPage = false;
|
||||
}
|
||||
|
||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||
@@ -3158,6 +3177,9 @@ importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
|
||||
if (text != null && text.isNotEmpty) {
|
||||
try {
|
||||
final sc = ServerConfig.decode(text);
|
||||
if (isWeb || isIOS) {
|
||||
sc.relayServer = '';
|
||||
}
|
||||
if (sc.idServer.isNotEmpty) {
|
||||
Future<bool> success = setServerConfig(controllers, errMsgs, sc);
|
||||
success.then((value) {
|
||||
@@ -3597,3 +3619,7 @@ List<SubWindowResizeEdge>? get subWindowManagerEnableResizeEdges => isWindows
|
||||
SubWindowResizeEdge.topRight,
|
||||
]
|
||||
: null;
|
||||
|
||||
void earlyAssert() {
|
||||
assert('\1' == '1');
|
||||
}
|
||||
|
||||
@@ -241,14 +241,15 @@ class _AddressBookState extends State<AddressBook> {
|
||||
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
||||
}
|
||||
},
|
||||
customButton: Obx(()=>Container(
|
||||
height: stateGlobal.isPortrait.isFalse ? 48 : 40,
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: buildItem(gFFI.abModel.currentName.value, button: true)),
|
||||
Icon(Icons.arrow_drop_down),
|
||||
]),
|
||||
)),
|
||||
customButton: Obx(() => Container(
|
||||
height: stateGlobal.isPortrait.isFalse ? 48 : 40,
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child:
|
||||
buildItem(gFFI.abModel.currentName.value, button: true)),
|
||||
Icon(Icons.arrow_drop_down),
|
||||
]),
|
||||
)),
|
||||
underline: Container(
|
||||
height: 0.7,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
@@ -358,7 +359,6 @@ class _AddressBookState extends State<AddressBook> {
|
||||
alignment: Alignment.topLeft,
|
||||
child: AddressBookPeersView(
|
||||
menuPadding: widget.menuPadding,
|
||||
getInitPeers: () => gFFI.abModel.currentAbPeers,
|
||||
)),
|
||||
);
|
||||
}
|
||||
@@ -509,19 +509,19 @@ class _AddressBookState extends State<AddressBook> {
|
||||
|
||||
row({required Widget lable, required Widget input}) {
|
||||
makeChild(bool isPortrait) => Row(
|
||||
children: [
|
||||
!isPortrait
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: lable.marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 200),
|
||||
child: input),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: !isPortrait ? 8 : 0);
|
||||
children: [
|
||||
!isPortrait
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: lable.marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 200),
|
||||
child: input),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: !isPortrait ? 8 : 0);
|
||||
return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
|
||||
}
|
||||
|
||||
@@ -546,23 +546,28 @@ class _AddressBookState extends State<AddressBook> {
|
||||
],
|
||||
),
|
||||
input: Obx(() => TextField(
|
||||
controller: idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse ? null : translate('ID'),
|
||||
errorText: errorMsg,
|
||||
errorMaxLines: 5),
|
||||
))),
|
||||
controller: idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse
|
||||
? null
|
||||
: translate('ID'),
|
||||
errorText: errorMsg,
|
||||
errorMaxLines: 5),
|
||||
))),
|
||||
row(
|
||||
lable: Text(
|
||||
translate('Alias'),
|
||||
style: style,
|
||||
),
|
||||
input: Obx(() => TextField(
|
||||
controller: aliasController,
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse ? null : translate('Alias'),
|
||||
),)),
|
||||
controller: aliasController,
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse
|
||||
? null
|
||||
: translate('Alias'),
|
||||
),
|
||||
)),
|
||||
),
|
||||
if (isCurrentAbShared)
|
||||
row(
|
||||
@@ -570,25 +575,29 @@ class _AddressBookState extends State<AddressBook> {
|
||||
translate('Password'),
|
||||
style: style,
|
||||
),
|
||||
input: Obx(() => TextField(
|
||||
controller: passwordController,
|
||||
obscureText: !passwordVisible,
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse ? null : translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: MyTheme.lightTheme.primaryColor),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
passwordVisible = !passwordVisible;
|
||||
});
|
||||
},
|
||||
input: Obx(
|
||||
() => TextField(
|
||||
controller: passwordController,
|
||||
obscureText: !passwordVisible,
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse
|
||||
? null
|
||||
: translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: MyTheme.lightTheme.primaryColor),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
passwordVisible = !passwordVisible;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),)),
|
||||
)),
|
||||
if (gFFI.abModel.currentAbTags.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
|
||||
38
flutter/lib/common/widgets/connection_page_title.dart
Normal file
38
flutter/lib/common/widgets/connection_page_title.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
|
||||
Widget getConnectionPageTitle(BuildContext context, bool isWeb) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
AutoSizeText(
|
||||
translate('Control Remote Desktop'),
|
||||
maxLines: 1,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.merge(TextStyle(height: 1)),
|
||||
).marginOnly(right: 4),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 300),
|
||||
message: translate(isWeb ? "web_id_input_tip" : "id_input_tip"),
|
||||
child: Icon(
|
||||
Icons.help_outline_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -83,8 +83,8 @@ class _MyGroupState extends State<MyGroup> {
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: MyGroupPeerView(
|
||||
menuPadding: widget.menuPadding,
|
||||
getInitPeers: () => gFFI.groupModel.peers)),
|
||||
menuPadding: widget.menuPadding,
|
||||
)),
|
||||
)
|
||||
],
|
||||
);
|
||||
@@ -115,8 +115,8 @@ class _MyGroupState extends State<MyGroup> {
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: MyGroupPeerView(
|
||||
menuPadding: widget.menuPadding,
|
||||
getInitPeers: () => gFFI.groupModel.peers)),
|
||||
menuPadding: widget.menuPadding,
|
||||
)),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
@@ -595,8 +595,7 @@ class QualityMonitor extends StatelessWidget {
|
||||
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
||||
_row(
|
||||
"Codec", qualityMonitorModel.data.codecFormat ?? '-'),
|
||||
if (!isWeb)
|
||||
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
|
||||
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -58,27 +58,33 @@ class _PeerCardState extends State<_PeerCard>
|
||||
stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
|
||||
}
|
||||
|
||||
Widget gestureDetector({required Widget child}) {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
final peer = super.widget.peer;
|
||||
return GestureDetector(
|
||||
onDoubleTap: peerTabModel.multiSelectionMode
|
||||
? null
|
||||
: () => widget.connect(context, peer.id),
|
||||
onTap: () {
|
||||
if (peerTabModel.multiSelectionMode) {
|
||||
peerTabModel.select(peer);
|
||||
} else {
|
||||
if (isMobile) {
|
||||
widget.connect(context, peer.id);
|
||||
} else {
|
||||
peerTabModel.select(peer);
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongPress: () => peerTabModel.select(peer),
|
||||
child: child);
|
||||
}
|
||||
|
||||
Widget _buildPortrait() {
|
||||
final peer = super.widget.peer;
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 2),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (peerTabModel.multiSelectionMode) {
|
||||
peerTabModel.select(peer);
|
||||
} else {
|
||||
if (!isWebDesktop) {
|
||||
connectInPeerTab(context, peer, widget.tab);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDoubleTap: isWebDesktop
|
||||
? () => connectInPeerTab(context, peer, widget.tab)
|
||||
: null,
|
||||
onLongPress: () {
|
||||
peerTabModel.select(peer);
|
||||
},
|
||||
child: gestureDetector(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
|
||||
child: _buildPeerTile(context, peer, null)),
|
||||
@@ -86,7 +92,6 @@ class _PeerCardState extends State<_PeerCard>
|
||||
}
|
||||
|
||||
Widget _buildLandscape() {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
final peer = super.widget.peer;
|
||||
var deco = Rx<BoxDecoration?>(
|
||||
BoxDecoration(
|
||||
@@ -115,30 +120,21 @@ class _PeerCardState extends State<_PeerCard>
|
||||
),
|
||||
);
|
||||
},
|
||||
child: GestureDetector(
|
||||
onDoubleTap:
|
||||
peerTabModel.multiSelectionMode || peerTabModel.isShiftDown
|
||||
? null
|
||||
: () => widget.connect(context, peer.id),
|
||||
onTap: () => peerTabModel.select(peer),
|
||||
onLongPress: () => peerTabModel.select(peer),
|
||||
child: gestureDetector(
|
||||
child: Obx(() => peerCardUiType.value == PeerUiType.grid
|
||||
? _buildPeerCard(context, peer, deco)
|
||||
: _buildPeerTile(context, peer, deco))),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPeerTile(
|
||||
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
||||
hideUsernameOnCard ??=
|
||||
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||
makeChild(bool isPortrait, Peer peer) {
|
||||
final name = hideUsernameOnCard == true
|
||||
? peer.hostname
|
||||
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||
final greyStyle = TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
||||
makeChild(bool isPortrait) => Row(
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
@@ -210,6 +206,12 @@ class _PeerCardState extends State<_PeerCard>
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPeerTile(
|
||||
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
||||
hideUsernameOnCard ??=
|
||||
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||
final colors = _frontN(peer.tags, 25)
|
||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||
.toList();
|
||||
@@ -220,21 +222,22 @@ class _PeerCardState extends State<_PeerCard>
|
||||
? '${translate('Tags')}: ${peer.tags.join(', ')}'
|
||||
: '',
|
||||
child: Stack(children: [
|
||||
Obx(() => deco == null
|
||||
? makeChild(stateGlobal.isPortrait.isTrue)
|
||||
: Container(
|
||||
Obx(
|
||||
() => deco == null
|
||||
? makeChild(stateGlobal.isPortrait.isTrue, peer)
|
||||
: Container(
|
||||
foregroundDecoration: deco.value,
|
||||
child: makeChild(stateGlobal.isPortrait.isTrue),
|
||||
child: makeChild(stateGlobal.isPortrait.isTrue, peer),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (colors.isNotEmpty)
|
||||
Obx(()=> Positioned(
|
||||
top: 2,
|
||||
right: stateGlobal.isPortrait.isTrue ? 20 : 10,
|
||||
child: CustomPaint(
|
||||
painter: TagPainter(radius: 3, colors: colors),
|
||||
),
|
||||
))
|
||||
Obx(() => Positioned(
|
||||
top: 2,
|
||||
right: stateGlobal.isPortrait.isTrue ? 20 : 10,
|
||||
child: CustomPaint(
|
||||
painter: TagPainter(radius: 3, colors: colors),
|
||||
),
|
||||
))
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -876,7 +879,7 @@ class RecentPeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
@@ -935,7 +938,7 @@ class FavoritePeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -988,7 +991,7 @@ class DiscoveredPeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
@@ -1041,7 +1044,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -1173,7 +1176,7 @@ class MyGroupPeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -1259,54 +1262,53 @@ void _rdpDialog(String id) async {
|
||||
],
|
||||
).marginOnly(bottom: isDesktop ? 8 : 0),
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Username')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: isDesktop
|
||||
? null
|
||||
: translate('Username')),
|
||||
controller: userController,
|
||||
),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Password')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Obx(() => TextField(
|
||||
obscureText: secure.value,
|
||||
maxLength: maxLength,
|
||||
children: [
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Username')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: isDesktop
|
||||
? null
|
||||
: translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () => secure.value = !secure.value,
|
||||
icon: Icon(secure.value
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility))),
|
||||
controller: passwordController,
|
||||
)),
|
||||
),
|
||||
],
|
||||
))
|
||||
labelText:
|
||||
isDesktop ? null : translate('Username')),
|
||||
controller: userController,
|
||||
),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Password')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Obx(() => TextField(
|
||||
obscureText: secure.value,
|
||||
maxLength: maxLength,
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
isDesktop ? null : translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () =>
|
||||
secure.value = !secure.value,
|
||||
icon: Icon(secure.value
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility))),
|
||||
controller: passwordController,
|
||||
)),
|
||||
),
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -42,6 +43,14 @@ class LoadEvent {
|
||||
static const String group = 'load_group_peers';
|
||||
}
|
||||
|
||||
class PeersModelName {
|
||||
static const String recent = 'recent peer';
|
||||
static const String favorite = 'fav peer';
|
||||
static const String lan = 'discovered peer';
|
||||
static const String addressBook = 'address book peer';
|
||||
static const String group = 'group peer';
|
||||
}
|
||||
|
||||
/// for peer search text, global obs value
|
||||
final peerSearchText = "".obs;
|
||||
|
||||
@@ -128,8 +137,9 @@ class _PeersViewState extends State<_PeersView>
|
||||
//
|
||||
// Although `onWindowRestore()` is called after `onWindowBlur()` in my test,
|
||||
// we need the following comparison to ensure that `_isActive` is true in the end.
|
||||
if (isWindows && DateTime.now().difference(_lastWindowRestoreTime) <
|
||||
const Duration(milliseconds: 300)) {
|
||||
if (isWindows &&
|
||||
DateTime.now().difference(_lastWindowRestoreTime) <
|
||||
const Duration(milliseconds: 300)) {
|
||||
return;
|
||||
}
|
||||
_queryCount = _maxQueryCount;
|
||||
@@ -170,8 +180,8 @@ class _PeersViewState extends State<_PeersView>
|
||||
// We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6.
|
||||
// Continious rebuilds of `ChangeNotifierProvider` will cause memory leak.
|
||||
// Simple demo can reproduce this issue.
|
||||
return ChangeNotifierProvider<Peers>(
|
||||
create: (context) => widget.peers,
|
||||
return ChangeNotifierProvider<Peers>.value(
|
||||
value: widget.peers,
|
||||
child: Consumer<Peers>(builder: (context, peers, child) {
|
||||
if (peers.peers.isEmpty) {
|
||||
gFFI.peerTabModel.setCurrentTabCachedPeers([]);
|
||||
@@ -322,7 +332,12 @@ class _PeersViewState extends State<_PeersView>
|
||||
_queryOnlines(false);
|
||||
}
|
||||
} else {
|
||||
if (_isActive && (_queryCount < _maxQueryCount || !p)) {
|
||||
final skipIfIsWeb =
|
||||
isWeb && !(stateGlobal.isWebVisible && stateGlobal.isInMainPage);
|
||||
final skipIfMobile =
|
||||
(isAndroid || isIOS) && !stateGlobal.isInMainPage;
|
||||
final skipIfNotActive = skipIfIsWeb || skipIfMobile || !_isActive;
|
||||
if (!skipIfNotActive && (_queryCount < _maxQueryCount || !p)) {
|
||||
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
||||
if (_curPeers.isNotEmpty) {
|
||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
@@ -403,28 +418,39 @@ class _PeersViewState extends State<_PeersView>
|
||||
}
|
||||
|
||||
abstract class BasePeersView extends StatelessWidget {
|
||||
final String name;
|
||||
final String loadEvent;
|
||||
final PeerTabIndex peerTabIndex;
|
||||
final PeerFilter? peerFilter;
|
||||
final PeerCardBuilder peerCardBuilder;
|
||||
final GetInitPeers? getInitPeers;
|
||||
|
||||
const BasePeersView({
|
||||
Key? key,
|
||||
required this.name,
|
||||
required this.loadEvent,
|
||||
required this.peerTabIndex,
|
||||
this.peerFilter,
|
||||
required this.peerCardBuilder,
|
||||
required this.getInitPeers,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Peers peers;
|
||||
switch (peerTabIndex) {
|
||||
case PeerTabIndex.recent:
|
||||
peers = gFFI.recentPeersModel;
|
||||
break;
|
||||
case PeerTabIndex.fav:
|
||||
peers = gFFI.favoritePeersModel;
|
||||
break;
|
||||
case PeerTabIndex.lan:
|
||||
peers = gFFI.lanPeersModel;
|
||||
break;
|
||||
case PeerTabIndex.ab:
|
||||
peers = gFFI.abModel.peersModel;
|
||||
break;
|
||||
case PeerTabIndex.group:
|
||||
peers = gFFI.groupModel.peersModel;
|
||||
break;
|
||||
}
|
||||
return _PeersView(
|
||||
peers:
|
||||
Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers),
|
||||
peerFilter: peerFilter,
|
||||
peerCardBuilder: peerCardBuilder);
|
||||
peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,13 +459,11 @@ class RecentPeersView extends BasePeersView {
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'recent peer',
|
||||
loadEvent: LoadEvent.recent,
|
||||
peerTabIndex: PeerTabIndex.recent,
|
||||
peerCardBuilder: (Peer peer) => RecentPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: null,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -455,13 +479,11 @@ class FavoritePeersView extends BasePeersView {
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'favorite peer',
|
||||
loadEvent: LoadEvent.favorite,
|
||||
peerTabIndex: PeerTabIndex.fav,
|
||||
peerCardBuilder: (Peer peer) => FavoritePeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: null,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -477,13 +499,11 @@ class DiscoveredPeersView extends BasePeersView {
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'discovered peer',
|
||||
loadEvent: LoadEvent.lan,
|
||||
peerTabIndex: PeerTabIndex.lan,
|
||||
peerCardBuilder: (Peer peer) => DiscoveredPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: null,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -496,21 +516,16 @@ class DiscoveredPeersView extends BasePeersView {
|
||||
|
||||
class AddressBookPeersView extends BasePeersView {
|
||||
AddressBookPeersView(
|
||||
{Key? key,
|
||||
EdgeInsets? menuPadding,
|
||||
ScrollController? scrollController,
|
||||
required GetInitPeers getInitPeers})
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'address book peer',
|
||||
loadEvent: LoadEvent.addressBook,
|
||||
peerTabIndex: PeerTabIndex.ab,
|
||||
peerFilter: (Peer peer) =>
|
||||
_hitTag(gFFI.abModel.selectedTags, peer.tags),
|
||||
peerCardBuilder: (Peer peer) => AddressBookPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: getInitPeers,
|
||||
);
|
||||
|
||||
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
|
||||
@@ -537,20 +552,15 @@ class AddressBookPeersView extends BasePeersView {
|
||||
|
||||
class MyGroupPeerView extends BasePeersView {
|
||||
MyGroupPeerView(
|
||||
{Key? key,
|
||||
EdgeInsets? menuPadding,
|
||||
ScrollController? scrollController,
|
||||
required GetInitPeers getInitPeers})
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'group peer',
|
||||
loadEvent: LoadEvent.group,
|
||||
peerTabIndex: PeerTabIndex.group,
|
||||
peerFilter: filter,
|
||||
peerCardBuilder: (Peer peer) => MyGroupPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: getInitPeers,
|
||||
);
|
||||
|
||||
static bool filter(Peer peer) {
|
||||
|
||||
@@ -147,12 +147,23 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
child: Text(translate('Reset canvas')),
|
||||
onPressed: () => ffi.cursorModel.reset()));
|
||||
}
|
||||
|
||||
connectWithToken(
|
||||
{required bool isFileTransfer, required bool isTcpTunneling}) {
|
||||
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
|
||||
connect(context, id,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
connToken: connToken);
|
||||
}
|
||||
|
||||
// transferFile
|
||||
if (isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Transfer file')),
|
||||
onPressed: () => connect(context, id, isFileTransfer: true)),
|
||||
onPressed: () =>
|
||||
connectWithToken(isFileTransfer: true, isTcpTunneling: false)),
|
||||
);
|
||||
}
|
||||
// tcpTunneling
|
||||
@@ -160,7 +171,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('TCP tunneling')),
|
||||
onPressed: () => connect(context, id, isTcpTunneling: true)),
|
||||
onPressed: () =>
|
||||
connectWithToken(isFileTransfer: false, isTcpTunneling: true)),
|
||||
);
|
||||
}
|
||||
// note
|
||||
@@ -183,7 +195,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text('${translate("Insert")} Ctrl + Alt + Del'),
|
||||
child: Text('${translate("Insert Ctrl + Alt + Del")}'),
|
||||
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ const String kOptionAllowAutoDisconnect = "allow-auto-disconnect";
|
||||
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
|
||||
const String kOptionEnableHwcodec = "enable-hwcodec";
|
||||
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
|
||||
const String kOptionAllowAutoRecordOutgoing = "allow-auto-record-outgoing";
|
||||
const String kOptionVideoSaveDirectory = "video-save-directory";
|
||||
const String kOptionAccessMode = "access-mode";
|
||||
const String kOptionEnableKeyboard = "enable-keyboard";
|
||||
@@ -201,7 +202,7 @@ const double kMinFps = 5;
|
||||
const double kDefaultFps = 30;
|
||||
const double kMaxFps = 120;
|
||||
|
||||
const double kMinQuality = 10;
|
||||
const double kMinQuality = 5;
|
||||
const double kDefaultQuality = 50;
|
||||
const double kMaxQuality = 100;
|
||||
const double kMaxMoreQuality = 2000;
|
||||
@@ -570,3 +571,5 @@ enum WindowsTarget {
|
||||
extension WindowsTargetExt on int {
|
||||
WindowsTarget get windowsVersion => getWindowsTarget(this);
|
||||
}
|
||||
|
||||
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -323,36 +323,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
child: Ink(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
AutoSizeText(
|
||||
translate('Control Remote Desktop'),
|
||||
maxLines: 1,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.merge(TextStyle(height: 1)),
|
||||
).marginOnly(right: 4),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 300),
|
||||
message: translate("id_input_tip"),
|
||||
child: Icon(
|
||||
Icons.help_outline_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
).marginOnly(bottom: 15),
|
||||
getConnectionPageTitle(context, false).marginOnly(bottom: 15),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
||||
@@ -664,9 +664,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.registerEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
|
||||
(Map<String, dynamic> evt) async {
|
||||
if (evt['url'] is String) {
|
||||
setState(() {
|
||||
updateUrl = evt['url'];
|
||||
});
|
||||
}
|
||||
});
|
||||
Timer(const Duration(seconds: 1), () async {
|
||||
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||
if (updateUrl.isNotEmpty) setState(() {});
|
||||
bind.mainGetSoftwareUpdateUrl();
|
||||
});
|
||||
}
|
||||
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
||||
@@ -766,6 +774,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
isRDP: call.arguments['isRDP'],
|
||||
password: call.arguments['password'],
|
||||
forceRelay: call.arguments['forceRelay'],
|
||||
connToken: call.arguments['connToken'],
|
||||
);
|
||||
} else if (call.method == kWindowEventMoveTabToNewWindow) {
|
||||
final args = call.arguments.split(',');
|
||||
@@ -824,6 +833,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
_uniLinksSubscription?.cancel();
|
||||
Get.delete<RxBool>(tag: 'stop-service');
|
||||
_updateTimer?.cancel();
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.unregisterEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -369,8 +369,8 @@ class _GeneralState extends State<_General> {
|
||||
|
||||
Widget theme() {
|
||||
final current = MyTheme.getThemeModePreference().toShortString();
|
||||
onChanged(String value) {
|
||||
MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
|
||||
onChanged(String value) async {
|
||||
await MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@@ -575,12 +575,18 @@ class _GeneralState extends State<_General> {
|
||||
bool root_dir_exists = map['root_dir_exists']!;
|
||||
bool user_dir_exists = map['user_dir_exists']!;
|
||||
return _Card(title: 'Recording', children: [
|
||||
_OptionCheckBox(context, 'Automatically record incoming sessions',
|
||||
kOptionAllowAutoRecordIncoming),
|
||||
if (showRootDir)
|
||||
if (!bind.isOutgoingOnly())
|
||||
_OptionCheckBox(context, 'Automatically record incoming sessions',
|
||||
kOptionAllowAutoRecordIncoming),
|
||||
if (!bind.isIncomingOnly())
|
||||
_OptionCheckBox(context, 'Automatically record outgoing sessions',
|
||||
kOptionAllowAutoRecordOutgoing,
|
||||
isServer: false),
|
||||
if (showRootDir && !bind.isOutgoingOnly())
|
||||
Row(
|
||||
children: [
|
||||
Text('${translate("Incoming")}:'),
|
||||
Text(
|
||||
'${translate(bind.isIncomingOnly() ? "Directory" : "Incoming")}:'),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: root_dir_exists
|
||||
@@ -597,45 +603,49 @@ class _GeneralState extends State<_General> {
|
||||
),
|
||||
],
|
||||
).marginOnly(left: _kContentHMargin),
|
||||
Row(
|
||||
children: [
|
||||
Text('${translate(showRootDir ? "Outgoing" : "Directory")}:'),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: user_dir_exists
|
||||
? () => launchUrl(Uri.file(user_dir))
|
||||
: null,
|
||||
child: Text(
|
||||
user_dir,
|
||||
softWrap: true,
|
||||
style: user_dir_exists
|
||||
? const TextStyle(decoration: TextDecoration.underline)
|
||||
if (!(showRootDir && bind.isIncomingOnly()))
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'${translate((showRootDir && !bind.isOutgoingOnly()) ? "Outgoing" : "Directory")}:'),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: user_dir_exists
|
||||
? () => launchUrl(Uri.file(user_dir))
|
||||
: null,
|
||||
)).marginOnly(left: 10),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: isOptionFixed(kOptionVideoSaveDirectory)
|
||||
? null
|
||||
: () async {
|
||||
String? initialDirectory;
|
||||
if (await Directory.fromUri(Uri.directory(user_dir))
|
||||
.exists()) {
|
||||
initialDirectory = user_dir;
|
||||
}
|
||||
String? selectedDirectory =
|
||||
await FilePicker.platform.getDirectoryPath(
|
||||
initialDirectory: initialDirectory);
|
||||
if (selectedDirectory != null) {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionVideoSaveDirectory,
|
||||
value: selectedDirectory);
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
child: Text(translate('Change')))
|
||||
.marginOnly(left: 5),
|
||||
],
|
||||
).marginOnly(left: _kContentHMargin),
|
||||
child: Text(
|
||||
user_dir,
|
||||
softWrap: true,
|
||||
style: user_dir_exists
|
||||
? const TextStyle(
|
||||
decoration: TextDecoration.underline)
|
||||
: null,
|
||||
)).marginOnly(left: 10),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: isOptionFixed(kOptionVideoSaveDirectory)
|
||||
? null
|
||||
: () async {
|
||||
String? initialDirectory;
|
||||
if (await Directory.fromUri(
|
||||
Uri.directory(user_dir))
|
||||
.exists()) {
|
||||
initialDirectory = user_dir;
|
||||
}
|
||||
String? selectedDirectory =
|
||||
await FilePicker.platform.getDirectoryPath(
|
||||
initialDirectory: initialDirectory);
|
||||
if (selectedDirectory != null) {
|
||||
await bind.mainSetLocalOption(
|
||||
key: kOptionVideoSaveDirectory,
|
||||
value: selectedDirectory);
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
child: Text(translate('Change')))
|
||||
.marginOnly(left: 5),
|
||||
],
|
||||
).marginOnly(left: _kContentHMargin),
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -1451,8 +1461,9 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
children: [
|
||||
Obx(() => _LabeledTextField(context, 'ID Server', idController,
|
||||
idErrMsg.value, enabled, secure)),
|
||||
Obx(() => _LabeledTextField(context, 'Relay Server',
|
||||
relayController, relayErrMsg.value, enabled, secure)),
|
||||
if (!isWeb)
|
||||
Obx(() => _LabeledTextField(context, 'Relay Server',
|
||||
relayController, relayErrMsg.value, enabled, secure)),
|
||||
Obx(() => _LabeledTextField(context, 'API Server',
|
||||
apiController, apiErrMsg.value, enabled, secure)),
|
||||
_LabeledTextField(
|
||||
|
||||
@@ -17,6 +17,8 @@ import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter_hbb/web/dummy.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
|
||||
|
||||
import '../../consts.dart';
|
||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||
@@ -55,14 +57,16 @@ class FileManagerPage extends StatefulWidget {
|
||||
required this.id,
|
||||
required this.password,
|
||||
required this.isSharedPassword,
|
||||
required this.tabController,
|
||||
this.tabController,
|
||||
this.connToken,
|
||||
this.forceRelay})
|
||||
: super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
final DesktopTabController tabController;
|
||||
final String? connToken;
|
||||
final DesktopTabController? tabController;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _FileManagerPageState();
|
||||
@@ -88,6 +92,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
isFileTransfer: true,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
connToken: widget.connToken,
|
||||
forceRelay: widget.forceRelay);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_ffi.dialogManager
|
||||
@@ -97,11 +102,14 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
if (isWeb) {
|
||||
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
|
||||
}
|
||||
debugPrint("File manager page init success with id ${widget.id}");
|
||||
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
widget.tabController?.onSelected?.call(widget.id);
|
||||
});
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
@@ -140,10 +148,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: dropArea(FileManagerView(
|
||||
model.localController, _ffi, _mouseFocusScope))),
|
||||
if (!isWeb)
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: dropArea(FileManagerView(
|
||||
model.localController, _ffi, _mouseFocusScope))),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: dropArea(FileManagerView(
|
||||
@@ -192,7 +201,13 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
return Icon(Icons.delete_outline, color: color);
|
||||
default:
|
||||
return Transform.rotate(
|
||||
angle: job.isRemoteToLocal ? pi : 0,
|
||||
angle: isWeb
|
||||
? job.isRemoteToLocal
|
||||
? pi / 2
|
||||
: pi / 2 * 3
|
||||
: job.isRemoteToLocal
|
||||
? pi
|
||||
: 0,
|
||||
child: Icon(Icons.arrow_forward_ios, color: color),
|
||||
);
|
||||
}
|
||||
@@ -478,6 +493,9 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
}
|
||||
|
||||
Widget headTools() {
|
||||
var uploadButtonTapPosition = RelativeRect.fill;
|
||||
RxBool isUploadFolder =
|
||||
(bind.mainGetLocalOption(key: 'upload-folder-button') == 'Y').obs;
|
||||
return Container(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -800,6 +818,66 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isWeb)
|
||||
Obx(() => ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
||||
isLocal
|
||||
? EdgeInsets.only(left: 10)
|
||||
: EdgeInsets.only(right: 10)),
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
selectedItems.items.isEmpty
|
||||
? MyTheme.accent80
|
||||
: MyTheme.accent,
|
||||
),
|
||||
),
|
||||
onPressed: () =>
|
||||
{webselectFiles(is_folder: isUploadFolder.value)},
|
||||
label: InkWell(
|
||||
hoverColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
focusColor: Colors.transparent,
|
||||
onTapDown: (e) {
|
||||
final x = e.globalPosition.dx;
|
||||
final y = e.globalPosition.dy;
|
||||
uploadButtonTapPosition =
|
||||
RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
onTap: () async {
|
||||
final value = await showMenu<bool>(
|
||||
context: context,
|
||||
position: uploadButtonTapPosition,
|
||||
items: [
|
||||
PopupMenuItem<bool>(
|
||||
value: false,
|
||||
child: Text(translate('Upload files')),
|
||||
),
|
||||
PopupMenuItem<bool>(
|
||||
value: true,
|
||||
child: Text(translate('Upload folder')),
|
||||
),
|
||||
]);
|
||||
if (value != null) {
|
||||
isUploadFolder.value = value;
|
||||
bind.mainSetLocalOption(
|
||||
key: 'upload-folder-button',
|
||||
value: value ? 'Y' : '');
|
||||
webselectFiles(is_folder: value);
|
||||
}
|
||||
},
|
||||
child: Icon(Icons.arrow_drop_down),
|
||||
),
|
||||
icon: Text(
|
||||
translate(isUploadFolder.isTrue
|
||||
? 'Upload folder'
|
||||
: 'Upload files'),
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
).marginOnly(left: 8),
|
||||
)).marginOnly(left: 16),
|
||||
Obx(() => ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
||||
@@ -833,19 +911,22 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
: Colors.white,
|
||||
),
|
||||
)
|
||||
: RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
colorFilter: svgColor(selectedItems.items.isEmpty
|
||||
? Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? MyTheme.grayBg
|
||||
: MyTheme.darkGray
|
||||
: Colors.white),
|
||||
alignment: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
: isWeb
|
||||
? Offstage()
|
||||
: RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
colorFilter: svgColor(
|
||||
selectedItems.items.isEmpty
|
||||
? Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? MyTheme.grayBg
|
||||
: MyTheme.darkGray
|
||||
: Colors.white),
|
||||
alignment: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
label: isLocal
|
||||
? SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
@@ -857,7 +938,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
: Colors.white),
|
||||
)
|
||||
: Text(
|
||||
translate('Receive'),
|
||||
translate(isWeb ? 'Download' : 'Receive'),
|
||||
style: TextStyle(
|
||||
color: selectedItems.items.isEmpty
|
||||
? Theme.of(context).brightness ==
|
||||
@@ -1020,7 +1101,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
if (!entry.isDrive &&
|
||||
versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0)
|
||||
mod_menu.PopupMenuItem(
|
||||
child: Text("Rename"),
|
||||
child: Text(translate("Rename")),
|
||||
height: CustomPopupMenuTheme.height,
|
||||
onTap: () {
|
||||
controller.renameAction(entry, isLocal);
|
||||
|
||||
@@ -48,6 +48,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
isSharedPassword: params['isSharedPassword'],
|
||||
tabController: tabController,
|
||||
forceRelay: params['forceRelay'],
|
||||
connToken: params['connToken'],
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -56,7 +57,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
super.initState();
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
print(
|
||||
debugPrint(
|
||||
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
|
||||
// for simplify, just replace connectionId
|
||||
if (call.method == kWindowEventNewFileTransfer) {
|
||||
@@ -76,6 +77,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
tabController: tabController,
|
||||
forceRelay: args['forceRelay'],
|
||||
connToken: args['connToken'],
|
||||
)));
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
|
||||
@@ -33,6 +33,7 @@ class PortForwardPage extends StatefulWidget {
|
||||
required this.isRDP,
|
||||
required this.isSharedPassword,
|
||||
this.forceRelay,
|
||||
this.connToken,
|
||||
}) : super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
@@ -40,6 +41,7 @@ class PortForwardPage extends StatefulWidget {
|
||||
final bool isRDP;
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
|
||||
@override
|
||||
State<PortForwardPage> createState() => _PortForwardPageState();
|
||||
@@ -62,6 +64,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
connToken: widget.connToken,
|
||||
isRdp: widget.isRDP);
|
||||
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
|
||||
debugPrint("Port forward page init success with id ${widget.id}");
|
||||
|
||||
@@ -48,6 +48,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
tabController: tabController,
|
||||
isRDP: isRDP,
|
||||
forceRelay: params['forceRelay'],
|
||||
connToken: params['connToken'],
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -82,6 +83,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
isRDP: isRDP,
|
||||
tabController: tabController,
|
||||
forceRelay: args['forceRelay'],
|
||||
connToken: args['connToken'],
|
||||
)));
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
|
||||
@@ -115,6 +115,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||
showKBLayoutTypeChooserIfNeeded(
|
||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||
_ffi.recordingModel
|
||||
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
|
||||
});
|
||||
_ffi.start(
|
||||
widget.id,
|
||||
@@ -253,7 +255,6 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.dialogManager.hideMobileActionsOverlay();
|
||||
_ffi.imageModel.disposeImage();
|
||||
_ffi.cursorModel.disposeImages();
|
||||
_ffi.recordingModel.onClose();
|
||||
_rawKeyFocusNode.dispose();
|
||||
await _ffi.close(closeSession: closeSession);
|
||||
_timer?.cancel();
|
||||
|
||||
@@ -395,7 +395,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
RemoteCountState.find().value = tabController.length;
|
||||
|
||||
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
|
||||
print(
|
||||
debugPrint(
|
||||
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
|
||||
dynamic returnValue;
|
||||
|
||||
@@ -305,7 +305,7 @@ class RemoteMenuEntry {
|
||||
}) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
'${translate("Insert")} Ctrl + Alt + Del',
|
||||
translate("Insert Ctrl + Alt + Del"),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
@@ -436,6 +436,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
shadowColor: MyTheme.color(context).shadow,
|
||||
borderRadius: borderRadius,
|
||||
child: _DraggableShowHide(
|
||||
id: widget.id,
|
||||
sessionId: widget.ffi.sessionId,
|
||||
dragging: _dragging,
|
||||
fractionX: _fractionX,
|
||||
@@ -478,8 +479,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
setFullscreen: _setFullscreen,
|
||||
));
|
||||
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
|
||||
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
|
||||
if (!isWeb) {
|
||||
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
|
||||
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
|
||||
}
|
||||
if (!isWeb) toolbarItems.add(_RecordMenu());
|
||||
@@ -1780,34 +1781,49 @@ class _ChatMenuState extends State<_ChatMenu> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Chat',
|
||||
key: chatButtonKey,
|
||||
svg: 'assets/chat.svg',
|
||||
ffi: widget.ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildrenGetter: () => [textChat(), voiceCall()]);
|
||||
if (isWeb) {
|
||||
return buildTextChatButton();
|
||||
} else {
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Chat',
|
||||
key: chatButtonKey,
|
||||
svg: 'assets/chat.svg',
|
||||
ffi: widget.ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildrenGetter: () => [textChat(), voiceCall()]);
|
||||
}
|
||||
}
|
||||
|
||||
buildTextChatButton() {
|
||||
return _IconMenuButton(
|
||||
assetName: 'assets/message_24dp_5F6368.svg',
|
||||
tooltip: 'Text chat',
|
||||
key: chatButtonKey,
|
||||
onPressed: _textChatOnPressed,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
);
|
||||
}
|
||||
|
||||
textChat() {
|
||||
return MenuButton(
|
||||
child: Text(translate('Text chat')),
|
||||
ffi: widget.ffi,
|
||||
onPressed: () {
|
||||
RenderBox? renderBox =
|
||||
chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
onPressed: _textChatOnPressed);
|
||||
}
|
||||
|
||||
Offset? initPos;
|
||||
if (renderBox != null) {
|
||||
final pos = renderBox.localToGlobal(Offset.zero);
|
||||
initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
|
||||
}
|
||||
|
||||
widget.ffi.chatModel.changeCurrentKey(
|
||||
MessageKey(widget.ffi.id, ChatModel.clientModeID));
|
||||
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
|
||||
});
|
||||
_textChatOnPressed() {
|
||||
RenderBox? renderBox =
|
||||
chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
Offset? initPos;
|
||||
if (renderBox != null) {
|
||||
final pos = renderBox.localToGlobal(Offset.zero);
|
||||
initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
|
||||
}
|
||||
widget.ffi.chatModel
|
||||
.changeCurrentKey(MessageKey(widget.ffi.id, ChatModel.clientModeID));
|
||||
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
|
||||
}
|
||||
|
||||
voiceCall() {
|
||||
@@ -1908,8 +1924,7 @@ class _RecordMenu extends StatelessWidget {
|
||||
var ffi = Provider.of<FfiModel>(context);
|
||||
var recordingModel = Provider.of<RecordingModel>(context);
|
||||
final visible =
|
||||
(recordingModel.start || ffi.permissions['recording'] != false) &&
|
||||
ffi.pi.currentDisplay != kAllDisplayValue;
|
||||
(recordingModel.start || ffi.permissions['recording'] != false);
|
||||
if (!visible) return Offstage();
|
||||
return _IconMenuButton(
|
||||
assetName: 'assets/rec.svg',
|
||||
@@ -2218,6 +2233,7 @@ class RdoMenuButton<T> extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _DraggableShowHide extends StatefulWidget {
|
||||
final String id;
|
||||
final SessionID sessionId;
|
||||
final RxDouble fractionX;
|
||||
final RxBool dragging;
|
||||
@@ -2229,6 +2245,7 @@ class _DraggableShowHide extends StatefulWidget {
|
||||
|
||||
const _DraggableShowHide({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.sessionId,
|
||||
required this.fractionX,
|
||||
required this.dragging,
|
||||
@@ -2318,15 +2335,33 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
);
|
||||
final isFullscreen = stateGlobal.fullscreen;
|
||||
const double iconSize = 20;
|
||||
|
||||
buttonWrapper(VoidCallback? onPressed, Widget child,
|
||||
{Color hoverColor = _ToolbarTheme.blueColor}) {
|
||||
final bgColor = buttonStyle.backgroundColor?.resolve({});
|
||||
return TextButton(
|
||||
onPressed: onPressed,
|
||||
child: child,
|
||||
style: buttonStyle.copyWith(
|
||||
backgroundColor: MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return (bgColor ?? hoverColor).withOpacity(0.15);
|
||||
}
|
||||
return bgColor;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final child = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildDraggable(context),
|
||||
Obx(() => TextButton(
|
||||
onPressed: () {
|
||||
Obx(() => buttonWrapper(
|
||||
() {
|
||||
widget.setFullscreen(!isFullscreen.value);
|
||||
},
|
||||
child: Tooltip(
|
||||
Tooltip(
|
||||
message: translate(
|
||||
isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'),
|
||||
child: Icon(
|
||||
@@ -2337,12 +2372,12 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
),
|
||||
),
|
||||
)),
|
||||
if (!isMacOS)
|
||||
if (!isMacOS && !isWebDesktop)
|
||||
Obx(() => Offstage(
|
||||
offstage: isFullscreen.isFalse,
|
||||
child: TextButton(
|
||||
onPressed: () => widget.setMinimize(),
|
||||
child: Tooltip(
|
||||
child: buttonWrapper(
|
||||
widget.setMinimize,
|
||||
Tooltip(
|
||||
message: translate('Minimize'),
|
||||
child: Icon(
|
||||
Icons.remove,
|
||||
@@ -2351,11 +2386,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
),
|
||||
),
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () => setState(() {
|
||||
buttonWrapper(
|
||||
() => setState(() {
|
||||
widget.toolbarState.switchShow(widget.sessionId);
|
||||
}),
|
||||
child: Obx((() => Tooltip(
|
||||
Obx((() => Tooltip(
|
||||
message:
|
||||
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||
child: Icon(
|
||||
@@ -2364,6 +2399,25 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
),
|
||||
))),
|
||||
),
|
||||
if (isWebDesktop)
|
||||
Obx(() {
|
||||
if (show.isTrue) {
|
||||
return Offstage();
|
||||
} else {
|
||||
return buttonWrapper(
|
||||
() => closeConnection(id: widget.id),
|
||||
Tooltip(
|
||||
message: translate('Close'),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: iconSize,
|
||||
color: _ToolbarTheme.redColor,
|
||||
),
|
||||
),
|
||||
hoverColor: _ToolbarTheme.redColor,
|
||||
).paddingOnly(left: iconSize / 2);
|
||||
}
|
||||
})
|
||||
],
|
||||
);
|
||||
return TextButtonTheme(
|
||||
|
||||
@@ -36,6 +36,7 @@ WindowType? kWindowType;
|
||||
late List<String> kBootArgs;
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
earlyAssert();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
debugPrint("launch args: $args");
|
||||
@@ -161,7 +162,7 @@ void runMobileApp() async {
|
||||
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
||||
gFFI.userModel.refreshCurrentUser();
|
||||
runApp(App());
|
||||
if (!isWeb) await initUniLinks();
|
||||
await initUniLinks();
|
||||
}
|
||||
|
||||
void runMultiWindow(
|
||||
@@ -444,7 +445,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
child: GetMaterialApp(
|
||||
navigatorKey: globalKey,
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'RustDesk',
|
||||
title: isWeb
|
||||
? '${bind.mainGetAppNameSync()} Web Client V2 (Preview)'
|
||||
: bind.mainGetAppNameSync(),
|
||||
theme: MyTheme.lightTheme,
|
||||
darkTheme: MyTheme.darkTheme,
|
||||
themeMode: MyTheme.currentThemeMode(),
|
||||
@@ -475,7 +478,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
: (context, child) {
|
||||
child = _keepScaleBuilder(context, child);
|
||||
child = botToastBuilder(context, child);
|
||||
if (isDesktop && desktopType == DesktopType.main) {
|
||||
if ((isDesktop && desktopType == DesktopType.main) ||
|
||||
isWebDesktop) {
|
||||
child = keyListenerBuilder(context, child);
|
||||
}
|
||||
if (isLinux) {
|
||||
@@ -503,7 +507,7 @@ _registerEventHandler() {
|
||||
platformFFI.registerEventHandler('theme', 'theme', (evt) async {
|
||||
String? dark = evt['dark'];
|
||||
if (dark != null) {
|
||||
MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark));
|
||||
await MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark));
|
||||
}
|
||||
});
|
||||
platformFFI.registerEventHandler('language', 'language', (_) async {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:auto_size_text_field/auto_size_text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -70,9 +71,17 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
}
|
||||
if (isAndroid) {
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.registerEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
|
||||
(Map<String, dynamic> evt) async {
|
||||
if (evt['url'] is String) {
|
||||
setState(() {
|
||||
_updateUrl = evt['url'];
|
||||
});
|
||||
}
|
||||
});
|
||||
Timer(const Duration(seconds: 1), () async {
|
||||
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||
if (_updateUrl.isNotEmpty) setState(() {});
|
||||
bind.mainGetSoftwareUpdateUrl();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -203,6 +212,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
FocusNode fieldFocusNode,
|
||||
VoidCallback onFieldSubmitted) {
|
||||
fieldTextEditingController.text = _idController.text;
|
||||
Get.put<TextEditingController>(
|
||||
fieldTextEditingController);
|
||||
fieldFocusNode.addListener(() async {
|
||||
_idEmpty.value =
|
||||
fieldTextEditingController.text.isEmpty;
|
||||
@@ -341,9 +352,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
),
|
||||
),
|
||||
);
|
||||
final child = Column(children: [
|
||||
if (isWebDesktop)
|
||||
getConnectionPageTitle(context, true)
|
||||
.marginOnly(bottom: 10, top: 15, left: 12),
|
||||
w
|
||||
]);
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(constraints: kMobilePageConstraints, child: w));
|
||||
child: Container(constraints: kMobilePageConstraints, child: child));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -353,6 +370,13 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
if (Get.isRegistered<IDTextEditingController>()) {
|
||||
Get.delete<IDTextEditingController>();
|
||||
}
|
||||
if (Get.isRegistered<TextEditingController>()) {
|
||||
Get.delete<TextEditingController>();
|
||||
}
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.unregisterEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:get/get.dart';
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/chat_page.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../models/state_model.dart';
|
||||
import 'connection_page.dart';
|
||||
|
||||
abstract class PageShape extends Widget {
|
||||
@@ -159,14 +160,75 @@ class WebHomePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
stateGlobal.isInMainPage = true;
|
||||
handleUnilink(context);
|
||||
return Scaffold(
|
||||
// backgroundColor: MyTheme.grayBg,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(bind.mainGetAppNameSync()),
|
||||
title: Text("${bind.mainGetAppNameSync()} (Preview)"),
|
||||
actions: connectionPage.appBarActions,
|
||||
),
|
||||
body: connectionPage,
|
||||
);
|
||||
}
|
||||
|
||||
handleUnilink(BuildContext context) {
|
||||
if (webInitialLink.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final link = webInitialLink;
|
||||
webInitialLink = '';
|
||||
final splitter = ["/#/", "/#", "#/", "#"];
|
||||
var fakelink = '';
|
||||
for (var s in splitter) {
|
||||
if (link.contains(s)) {
|
||||
var list = link.split(s);
|
||||
if (list.length < 2 || list[1].isEmpty) {
|
||||
return;
|
||||
}
|
||||
list.removeAt(0);
|
||||
fakelink = "rustdesk://${list.join(s)}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fakelink.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final uri = Uri.tryParse(fakelink);
|
||||
if (uri == null) {
|
||||
return;
|
||||
}
|
||||
final args = urlLinkToCmdArgs(uri);
|
||||
if (args == null || args.isEmpty) {
|
||||
return;
|
||||
}
|
||||
bool isFileTransfer = false;
|
||||
String? id;
|
||||
String? password;
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--connect':
|
||||
case '--play':
|
||||
isFileTransfer = false;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--file-transfer':
|
||||
isFileTransfer = true;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--password':
|
||||
password = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (id != null) {
|
||||
connect(context, id, isFileTransfer: isFileTransfer, password: password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ class _RemotePageState extends State<RemotePage> {
|
||||
|
||||
final TextEditingController _textController =
|
||||
TextEditingController(text: initText);
|
||||
// This timer is used to check the composing status of the soft keyboard.
|
||||
// It is used for Android, Korean(and other similar) input method.
|
||||
Timer? _composingTimer;
|
||||
|
||||
_RemotePageState(String id) {
|
||||
initSharedStates(id);
|
||||
@@ -89,6 +92,13 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.chatModel
|
||||
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
||||
_blockableOverlayState.applyFfi(gFFI);
|
||||
gFFI.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||
gFFI.recordingModel
|
||||
.updateStatus(bind.sessionGetIsRecording(sessionId: gFFI.sessionId));
|
||||
if (gFFI.recordingModel.start) {
|
||||
showToast(translate('Automatically record outgoing sessions'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -104,6 +114,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
_physicalFocusNode.dispose();
|
||||
await gFFI.close();
|
||||
_timer?.cancel();
|
||||
_composingTimer?.cancel();
|
||||
gFFI.dialogManager.dismissAll();
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
@@ -139,6 +150,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.ffiModel.pi.version.isNotEmpty) {
|
||||
gFFI.invokeMethod("enable_soft_keyboard", false);
|
||||
}
|
||||
_composingTimer?.cancel();
|
||||
} else {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
|
||||
@@ -155,9 +167,9 @@ class _RemotePageState extends State<RemotePage> {
|
||||
var oldValue = _value;
|
||||
_value = newValue;
|
||||
var i = newValue.length - 1;
|
||||
for (; i >= 0 && newValue[i] != '\1'; --i) {}
|
||||
for (; i >= 0 && newValue[i] != '1'; --i) {}
|
||||
var j = oldValue.length - 1;
|
||||
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
|
||||
for (; j >= 0 && oldValue[j] != '1'; --j) {}
|
||||
if (i < j) j = i;
|
||||
var subNewValue = newValue.substring(j + 1);
|
||||
var subOldValue = oldValue.substring(j + 1);
|
||||
@@ -202,12 +214,19 @@ class _RemotePageState extends State<RemotePage> {
|
||||
}
|
||||
|
||||
void _handleNonIOSSoftKeyboardInput(String newValue) {
|
||||
_composingTimer?.cancel();
|
||||
if (_textController.value.isComposingRangeValid) {
|
||||
_composingTimer = Timer(Duration(milliseconds: 25), () {
|
||||
_handleNonIOSSoftKeyboardInput(_textController.value.text);
|
||||
});
|
||||
return;
|
||||
}
|
||||
var oldValue = _value;
|
||||
_value = newValue;
|
||||
if (oldValue.isNotEmpty &&
|
||||
newValue.isNotEmpty &&
|
||||
oldValue[0] == '\1' &&
|
||||
newValue[0] != '\1') {
|
||||
oldValue[0] == '1' &&
|
||||
newValue[0] != '1') {
|
||||
// clipboard
|
||||
oldValue = '';
|
||||
}
|
||||
@@ -242,10 +261,14 @@ class _RemotePageState extends State<RemotePage> {
|
||||
}
|
||||
}
|
||||
|
||||
// handle mobile virtual keyboard
|
||||
void handleSoftKeyboardInput(String newValue) {
|
||||
Future<void> handleSoftKeyboardInput(String newValue) async {
|
||||
if (isIOS) {
|
||||
_handleIOSSoftKeyboardInput(newValue);
|
||||
// fix: TextFormField onChanged event triggered multiple times when Korean input
|
||||
// https://github.com/rustdesk/rustdesk/pull/9644
|
||||
await Future.delayed(const Duration(milliseconds: 10));
|
||||
|
||||
if (newValue != _textController.text) return;
|
||||
_handleIOSSoftKeyboardInput(_textController.text);
|
||||
} else {
|
||||
_handleNonIOSSoftKeyboardInput(newValue);
|
||||
}
|
||||
|
||||
@@ -19,95 +19,48 @@ class ScanPage extends StatefulWidget {
|
||||
class _ScanPageState extends State<ScanPage> {
|
||||
QRViewController? controller;
|
||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
||||
StreamSubscription? scanSubscription;
|
||||
|
||||
// In order to get hot reload to work we need to pause the camera if the platform
|
||||
// is android, or resume the camera if the platform is iOS.
|
||||
@override
|
||||
void reassemble() {
|
||||
super.reassemble();
|
||||
if (isAndroid) {
|
||||
if (isAndroid && controller != null) {
|
||||
controller!.pauseCamera();
|
||||
} else if (controller != null) {
|
||||
controller!.resumeCamera();
|
||||
}
|
||||
controller!.resumeCamera();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Scan QR'),
|
||||
actions: [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.image_search),
|
||||
iconSize: 32.0,
|
||||
onPressed: () async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? file =
|
||||
await picker.pickImage(source: ImageSource.gallery);
|
||||
if (file != null) {
|
||||
var image = img.decodeNamedImage(
|
||||
file.path, File(file.path).readAsBytesSync())!;
|
||||
|
||||
LuminanceSource source = RGBLuminanceSource(
|
||||
image.width,
|
||||
image.height,
|
||||
image
|
||||
.getBytes(order: img.ChannelOrder.abgr)
|
||||
.buffer
|
||||
.asInt32List());
|
||||
var bitmap = BinaryBitmap(HybridBinarizer(source));
|
||||
|
||||
var reader = QRCodeReader();
|
||||
try {
|
||||
var result = reader.decode(bitmap);
|
||||
if (result.text.startsWith(bind.mainUriPrefixSync())) {
|
||||
handleUriLink(uriString: result.text);
|
||||
} else {
|
||||
showServerSettingFromQr(result.text);
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('No QR code found');
|
||||
}
|
||||
}
|
||||
}),
|
||||
IconButton(
|
||||
color: Colors.yellow,
|
||||
icon: Icon(Icons.flash_on),
|
||||
iconSize: 32.0,
|
||||
onPressed: () async {
|
||||
await controller?.toggleFlash();
|
||||
}),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.switch_camera),
|
||||
iconSize: 32.0,
|
||||
onPressed: () async {
|
||||
await controller?.flipCamera();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _buildQrView(context));
|
||||
appBar: AppBar(
|
||||
title: const Text('Scan QR'),
|
||||
actions: [
|
||||
_buildImagePickerButton(),
|
||||
_buildFlashToggleButton(),
|
||||
_buildCameraSwitchButton(),
|
||||
],
|
||||
),
|
||||
body: _buildQrView(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQrView(BuildContext context) {
|
||||
// For this example we check how width or tall the device is and change the scanArea and overlay accordingly.
|
||||
var scanArea = (MediaQuery.of(context).size.width < 400 ||
|
||||
MediaQuery.of(context).size.height < 400)
|
||||
var scanArea = MediaQuery.of(context).size.width < 400 ||
|
||||
MediaQuery.of(context).size.height < 400
|
||||
? 150.0
|
||||
: 300.0;
|
||||
// To ensure the Scanner view is properly sizes after rotation
|
||||
// we need to listen for Flutter SizeChanged notification and update controller
|
||||
return QRView(
|
||||
key: qrKey,
|
||||
onQRViewCreated: _onQRViewCreated,
|
||||
overlay: QrScannerOverlayShape(
|
||||
borderColor: Colors.red,
|
||||
borderRadius: 10,
|
||||
borderLength: 30,
|
||||
borderWidth: 10,
|
||||
cutOutSize: scanArea),
|
||||
borderColor: Colors.red,
|
||||
borderRadius: 10,
|
||||
borderLength: 30,
|
||||
borderWidth: 10,
|
||||
cutOutSize: scanArea,
|
||||
),
|
||||
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
|
||||
);
|
||||
}
|
||||
@@ -116,7 +69,7 @@ class _ScanPageState extends State<ScanPage> {
|
||||
setState(() {
|
||||
this.controller = controller;
|
||||
});
|
||||
controller.scannedDataStream.listen((scanData) {
|
||||
scanSubscription = controller.scannedDataStream.listen((scanData) {
|
||||
if (scanData.code != null) {
|
||||
showServerSettingFromQr(scanData.code!);
|
||||
}
|
||||
@@ -129,8 +82,66 @@ class _ScanPageState extends State<ScanPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickImage() async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? file = await picker.pickImage(source: ImageSource.gallery);
|
||||
if (file != null) {
|
||||
try {
|
||||
var image = img.decodeImage(await File(file.path).readAsBytes())!;
|
||||
LuminanceSource source = RGBLuminanceSource(
|
||||
image.width,
|
||||
image.height,
|
||||
image.getBytes(order: img.ChannelOrder.abgr).buffer.asInt32List(),
|
||||
);
|
||||
var bitmap = BinaryBitmap(HybridBinarizer(source));
|
||||
|
||||
var reader = QRCodeReader();
|
||||
var result = reader.decode(bitmap);
|
||||
if (result.text.startsWith(bind.mainUriPrefixSync())) {
|
||||
handleUriLink(uriString: result.text);
|
||||
} else {
|
||||
showServerSettingFromQr(result.text);
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('No QR code found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildImagePickerButton() {
|
||||
return IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.image_search),
|
||||
iconSize: 32.0,
|
||||
onPressed: _pickImage,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFlashToggleButton() {
|
||||
return IconButton(
|
||||
color: Colors.yellow,
|
||||
icon: Icon(Icons.flash_on),
|
||||
iconSize: 32.0,
|
||||
onPressed: () async {
|
||||
await controller?.toggleFlash();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCameraSwitchButton() {
|
||||
return IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.switch_camera),
|
||||
iconSize: 32.0,
|
||||
onPressed: () async {
|
||||
await controller?.flipCamera();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scanSubscription?.cancel();
|
||||
controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
var _enableRecordSession = false;
|
||||
var _enableHardwareCodec = false;
|
||||
var _autoRecordIncomingSession = false;
|
||||
var _autoRecordOutgoingSession = false;
|
||||
var _allowAutoDisconnect = false;
|
||||
var _localIP = "";
|
||||
var _directAccessPort = "";
|
||||
@@ -104,6 +105,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
|
||||
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
|
||||
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
|
||||
_autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing,
|
||||
bind.mainGetLocalOption(key: kOptionAllowAutoRecordOutgoing));
|
||||
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
|
||||
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
|
||||
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
||||
@@ -231,6 +234,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
Widget build(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
final outgoingOnly = bind.isOutgoingOnly();
|
||||
final incommingOnly = bind.isIncomingOnly();
|
||||
final customClientSection = CustomSettingsSection(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -674,32 +678,55 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
},
|
||||
),
|
||||
]),
|
||||
if (isAndroid && !outgoingOnly)
|
||||
if (isAndroid)
|
||||
SettingsSection(
|
||||
title: Text(translate("Recording")),
|
||||
tiles: [
|
||||
SettingsTile.switchTile(
|
||||
title:
|
||||
Text(translate('Automatically record incoming sessions')),
|
||||
leading: Icon(Icons.videocam),
|
||||
description: Text(
|
||||
"${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"),
|
||||
initialValue: _autoRecordIncomingSession,
|
||||
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
||||
? null
|
||||
: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionAllowAutoRecordIncoming,
|
||||
value:
|
||||
bool2option(kOptionAllowAutoRecordIncoming, v));
|
||||
final newValue = option2bool(
|
||||
kOptionAllowAutoRecordIncoming,
|
||||
await bind.mainGetOption(
|
||||
key: kOptionAllowAutoRecordIncoming));
|
||||
setState(() {
|
||||
_autoRecordIncomingSession = newValue;
|
||||
});
|
||||
},
|
||||
if (!outgoingOnly)
|
||||
SettingsTile.switchTile(
|
||||
title:
|
||||
Text(translate('Automatically record incoming sessions')),
|
||||
initialValue: _autoRecordIncomingSession,
|
||||
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
||||
? null
|
||||
: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionAllowAutoRecordIncoming,
|
||||
value: bool2option(
|
||||
kOptionAllowAutoRecordIncoming, v));
|
||||
final newValue = option2bool(
|
||||
kOptionAllowAutoRecordIncoming,
|
||||
await bind.mainGetOption(
|
||||
key: kOptionAllowAutoRecordIncoming));
|
||||
setState(() {
|
||||
_autoRecordIncomingSession = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!incommingOnly)
|
||||
SettingsTile.switchTile(
|
||||
title:
|
||||
Text(translate('Automatically record outgoing sessions')),
|
||||
initialValue: _autoRecordOutgoingSession,
|
||||
onToggle: isOptionFixed(kOptionAllowAutoRecordOutgoing)
|
||||
? null
|
||||
: (v) async {
|
||||
await bind.mainSetLocalOption(
|
||||
key: kOptionAllowAutoRecordOutgoing,
|
||||
value: bool2option(
|
||||
kOptionAllowAutoRecordOutgoing, v));
|
||||
final newValue = option2bool(
|
||||
kOptionAllowAutoRecordOutgoing,
|
||||
bind.mainGetLocalOption(
|
||||
key: kOptionAllowAutoRecordOutgoing));
|
||||
setState(() {
|
||||
_autoRecordOutgoingSession = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
SettingsTile(
|
||||
title: Text(translate("Directory")),
|
||||
description: Text(bind.mainVideoSaveDirectory(root: false)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -205,14 +205,15 @@ void showServerSettingsWithValue(
|
||||
)
|
||||
] +
|
||||
[
|
||||
TextFormField(
|
||||
controller: relayCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Relay Server'),
|
||||
errorText: relayServerMsg.value.isEmpty
|
||||
? null
|
||||
: relayServerMsg.value),
|
||||
)
|
||||
if (isAndroid)
|
||||
TextFormField(
|
||||
controller: relayCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Relay Server'),
|
||||
errorText: relayServerMsg.value.isEmpty
|
||||
? null
|
||||
: relayServerMsg.value),
|
||||
)
|
||||
] +
|
||||
[
|
||||
TextFormField(
|
||||
|
||||
@@ -66,10 +66,16 @@ class AbModel {
|
||||
var listInitialized = false;
|
||||
var _maxPeerOneAb = 0;
|
||||
|
||||
late final Peers peersModel;
|
||||
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
AbModel(this.parent) {
|
||||
addressbooks.clear();
|
||||
peersModel = Peers(
|
||||
name: PeersModelName.addressBook,
|
||||
getInitPeers: () => currentAbPeers,
|
||||
loadEvent: LoadEvent.addressBook);
|
||||
if (desktopType == DesktopType.main) {
|
||||
Timer.periodic(Duration(milliseconds: 500), (timer) async {
|
||||
if (_timerCounter++ % 6 == 0) {
|
||||
|
||||
@@ -235,13 +235,14 @@ class ChatModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
_isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) ||
|
||||
chatWindowOverlayEntry == null);
|
||||
_isChatOverlayHide() =>
|
||||
((!(isDesktop || isWebDesktop) && chatIconOverlayEntry == null) ||
|
||||
chatWindowOverlayEntry == null);
|
||||
|
||||
toggleChatOverlay({Offset? chatInitPos}) {
|
||||
if (_isChatOverlayHide()) {
|
||||
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||
if (!isDesktop) {
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
showChatIconOverlay();
|
||||
}
|
||||
showChatWindowOverlay(chatInitPos: chatInitPos);
|
||||
|
||||
@@ -7,6 +7,8 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/utils/event_loop.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:flutter_hbb/web/dummy.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
|
||||
|
||||
import '../consts.dart';
|
||||
import 'model.dart';
|
||||
@@ -74,7 +76,7 @@ class FileModel {
|
||||
|
||||
Future<void> onReady() async {
|
||||
await evtLoop.onReady();
|
||||
await localController.onReady();
|
||||
if (!isWeb) await localController.onReady();
|
||||
await remoteController.onReady();
|
||||
}
|
||||
|
||||
@@ -86,7 +88,7 @@ class FileModel {
|
||||
}
|
||||
|
||||
Future<void> refreshAll() async {
|
||||
await localController.refresh();
|
||||
if (!isWeb) await localController.refresh();
|
||||
await remoteController.refresh();
|
||||
}
|
||||
|
||||
@@ -228,6 +230,33 @@ class FileModel {
|
||||
);
|
||||
}, useAnimation: false);
|
||||
}
|
||||
|
||||
void onSelectedFiles(dynamic obj) {
|
||||
localController.selectedItems.clear();
|
||||
|
||||
try {
|
||||
int handleIndex = int.parse(obj['handleIndex']);
|
||||
final file = jsonDecode(obj['file']);
|
||||
var entry = Entry.fromJson(file);
|
||||
entry.path = entry.name;
|
||||
final otherSideData = remoteController.directoryData();
|
||||
final toPath = otherSideData.directory.path;
|
||||
final isWindows = otherSideData.options.isWindows;
|
||||
final showHidden = otherSideData.options.showHidden;
|
||||
final jobID = jobController.addTransferJob(entry, false);
|
||||
webSendLocalFiles(
|
||||
handleIndex: handleIndex,
|
||||
actId: jobID,
|
||||
path: entry.path,
|
||||
to: PathUtil.join(toPath, entry.name, isWindows),
|
||||
fileNum: 0,
|
||||
includeHidden: showHidden,
|
||||
isRemote: false,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint("Failed to decode onSelectedFiles: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DirectoryData {
|
||||
@@ -462,7 +491,8 @@ class FileController {
|
||||
to: PathUtil.join(toPath, from.name, isWindows),
|
||||
fileNum: 0,
|
||||
includeHidden: showHidden,
|
||||
isRemote: isRemoteToLocal);
|
||||
isRemote: isRemoteToLocal,
|
||||
isDir: from.isDirectory);
|
||||
debugPrint(
|
||||
"path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}");
|
||||
}
|
||||
@@ -489,7 +519,7 @@ class FileController {
|
||||
} else if (item.isDirectory) {
|
||||
title = translate("Not an empty directory");
|
||||
dialogManager?.showLoading(translate("Waiting"));
|
||||
final fd = await fileFetcher.fetchDirectoryRecursive(
|
||||
final fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
|
||||
jobID, item.path, items.isLocal, true);
|
||||
if (fd.path.isEmpty) {
|
||||
fd.path = item.path;
|
||||
@@ -809,7 +839,6 @@ class JobController {
|
||||
job.speed = double.parse(evt['speed']);
|
||||
job.finishedSize = int.parse(evt['finished_size']);
|
||||
job.recvJobRes = true;
|
||||
debugPrint("update job $id with $evt");
|
||||
jobTable.refresh();
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1116,11 +1145,11 @@ class FileFetcher {
|
||||
}
|
||||
}
|
||||
|
||||
Future<FileDirectory> fetchDirectoryRecursive(
|
||||
Future<FileDirectory> fetchDirectoryRecursiveToRemove(
|
||||
int actID, String path, bool isLocal, bool showHidden) async {
|
||||
// TODO test Recursive is show hidden default?
|
||||
try {
|
||||
await bind.sessionReadDirRecursive(
|
||||
await bind.sessionReadDirToRemoveRecursive(
|
||||
sessionId: sessionId,
|
||||
actId: actID,
|
||||
path: path,
|
||||
|
||||
@@ -23,7 +23,14 @@ class GroupModel {
|
||||
|
||||
bool get emtpy => users.isEmpty && peers.isEmpty;
|
||||
|
||||
GroupModel(this.parent);
|
||||
late final Peers peersModel;
|
||||
|
||||
GroupModel(this.parent) {
|
||||
peersModel = Peers(
|
||||
name: PeersModelName.group,
|
||||
getInitPeers: () => peers,
|
||||
loadEvent: LoadEvent.group);
|
||||
}
|
||||
|
||||
Future<void> pull({force = true, quiet = false}) async {
|
||||
if (bind.isDisableGroupPanel()) return;
|
||||
|
||||
@@ -469,8 +469,12 @@ class InputModel {
|
||||
|
||||
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
|
||||
if (isViewOnly) return KeyEventResult.handled;
|
||||
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) {
|
||||
return KeyEventResult.handled;
|
||||
if (!isInputSourceFlutter) {
|
||||
if (isDesktop) {
|
||||
return KeyEventResult.handled;
|
||||
} else if (isWeb) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
}
|
||||
|
||||
final key = e.logicalKey;
|
||||
@@ -519,8 +523,12 @@ class InputModel {
|
||||
|
||||
KeyEventResult handleKeyEvent(KeyEvent e) {
|
||||
if (isViewOnly) return KeyEventResult.handled;
|
||||
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) {
|
||||
return KeyEventResult.handled;
|
||||
if (!isInputSourceFlutter) {
|
||||
if (isDesktop) {
|
||||
return KeyEventResult.handled;
|
||||
} else if (isWeb) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
}
|
||||
if (isWindows || isLinux) {
|
||||
// Ignore meta keys. Because flutter window will loose focus if meta key is pressed.
|
||||
@@ -536,8 +544,7 @@ class InputModel {
|
||||
handleKeyDownEventModifiers(e);
|
||||
}
|
||||
|
||||
// * Currently mobile does not enable map mode
|
||||
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||
if (isMobile || (isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||
// FIXME: e.character is wrong for dead keys, eg: ^ in de
|
||||
newKeyboardMode(
|
||||
e.character ?? '',
|
||||
|
||||
@@ -4,15 +4,18 @@ import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/cm_file_model.dart';
|
||||
import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:flutter_hbb/models/group_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/user_model.dart';
|
||||
@@ -267,6 +270,8 @@ class FfiModel with ChangeNotifier {
|
||||
var name = evt['name'];
|
||||
if (name == 'msgbox') {
|
||||
handleMsgBox(evt, sessionId, peerId);
|
||||
} else if (name == 'toast') {
|
||||
handleToast(evt, sessionId, peerId);
|
||||
} else if (name == 'set_multiple_windows_session') {
|
||||
handleMultipleWindowsSession(evt, sessionId, peerId);
|
||||
} else if (name == 'peer_info') {
|
||||
@@ -370,7 +375,7 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'plugin_option') {
|
||||
handleOption(evt);
|
||||
} else if (name == "sync_peer_hash_password_to_personal_ab") {
|
||||
if (desktopType == DesktopType.main) {
|
||||
if (desktopType == DesktopType.main || isWeb || isMobile) {
|
||||
final id = evt['id'];
|
||||
final hash = evt['hash'];
|
||||
if (id != null && hash != null) {
|
||||
@@ -388,6 +393,14 @@ class FfiModel with ChangeNotifier {
|
||||
handleFollowCurrentDisplay(evt, sessionId, peerId);
|
||||
} else if (name == 'use_texture_render') {
|
||||
_handleUseTextureRender(evt, sessionId, peerId);
|
||||
} else if (name == "selected_files") {
|
||||
if (isWeb) {
|
||||
parent.target?.fileModel.onSelectedFiles(evt);
|
||||
}
|
||||
} else if (name == "record_status") {
|
||||
if (desktopType == DesktopType.remote || isMobile) {
|
||||
parent.target?.recordingModel.updateStatus(evt['start'] == 'true');
|
||||
}
|
||||
} else {
|
||||
debugPrint('Event is not handled in the fixed branch: $name');
|
||||
}
|
||||
@@ -518,7 +531,6 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
parent.target?.recordingModel.onSwitchDisplay();
|
||||
if (!_pi.isSupportMultiUiSession || _pi.currentDisplay == display) {
|
||||
handleResolutions(peerId, evt['resolutions']);
|
||||
}
|
||||
@@ -589,13 +601,44 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
handleToast(Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||
final type = evt['type'] ?? 'info';
|
||||
final text = evt['text'] ?? '';
|
||||
final durMsc = evt['dur_msec'] ?? 2000;
|
||||
final duration = Duration(milliseconds: durMsc);
|
||||
if ((text).isEmpty) {
|
||||
BotToast.showLoading(
|
||||
duration: duration,
|
||||
clickClose: true,
|
||||
allowClick: true,
|
||||
);
|
||||
} else {
|
||||
if (type.contains('error')) {
|
||||
BotToast.showText(
|
||||
contentColor: Colors.red,
|
||||
text: translate(text),
|
||||
duration: duration,
|
||||
clickClose: true,
|
||||
onlyOne: true,
|
||||
);
|
||||
} else {
|
||||
BotToast.showText(
|
||||
text: translate(text),
|
||||
duration: duration,
|
||||
clickClose: true,
|
||||
onlyOne: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a message box with [type], [title] and [text].
|
||||
showMsgBox(SessionID sessionId, String type, String title, String text,
|
||||
String link, bool hasRetry, OverlayDialogManager dialogManager,
|
||||
{bool? hasCancel}) {
|
||||
msgBox(sessionId, type, title, text, link, dialogManager,
|
||||
hasCancel: hasCancel,
|
||||
reconnect: reconnect,
|
||||
reconnect: hasRetry ? reconnect : null,
|
||||
reconnectTimeout: hasRetry ? _reconnects : null);
|
||||
_timer?.cancel();
|
||||
if (hasRetry) {
|
||||
@@ -839,7 +882,7 @@ class FfiModel with ChangeNotifier {
|
||||
for (final mode in [kKeyMapMode, kKeyLegacyMode]) {
|
||||
if (bind.sessionIsKeyboardModeSupported(
|
||||
sessionId: sessionId, mode: mode)) {
|
||||
bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode);
|
||||
await bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1095,8 +1138,6 @@ class FfiModel with ChangeNotifier {
|
||||
// Directly switch to the new display without waiting for the response.
|
||||
switchToNewDisplay(int display, SessionID sessionId, String peerId,
|
||||
{bool updateCursorPos = false}) {
|
||||
// VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays.
|
||||
parent.target?.recordingModel.onClose();
|
||||
// no need to wait for the response
|
||||
pi.currentDisplay = display;
|
||||
updateCurDisplay(sessionId, updateCursorPos: updateCursorPos);
|
||||
@@ -1185,6 +1226,27 @@ class ImageModel with ChangeNotifier {
|
||||
|
||||
clearImage() => _image = null;
|
||||
|
||||
bool _webDecodingRgba = false;
|
||||
final List<Uint8List> _webRgbaList = List.empty(growable: true);
|
||||
webOnRgba(int display, Uint8List rgba) async {
|
||||
// deep copy needed, otherwise "instantiateCodec failed: TypeError: Cannot perform Construct on a detached ArrayBuffer"
|
||||
_webRgbaList.add(Uint8List.fromList(rgba));
|
||||
if (_webDecodingRgba) {
|
||||
return;
|
||||
}
|
||||
_webDecodingRgba = true;
|
||||
try {
|
||||
while (_webRgbaList.isNotEmpty) {
|
||||
final rgba2 = _webRgbaList.last;
|
||||
_webRgbaList.clear();
|
||||
await decodeAndUpdate(display, rgba2);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('onRgba error: $e');
|
||||
}
|
||||
_webDecodingRgba = false;
|
||||
}
|
||||
|
||||
onRgba(int display, Uint8List rgba) async {
|
||||
try {
|
||||
await decodeAndUpdate(display, rgba);
|
||||
@@ -1597,11 +1659,25 @@ class CanvasModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
clear([bool notify = false]) {
|
||||
// For reset canvas to the last view style
|
||||
reset() {
|
||||
_scale = _lastViewStyle.scale;
|
||||
_devicePixelRatio = ui.window.devicePixelRatio;
|
||||
if (kIgnoreDpi && _lastViewStyle.style == kRemoteViewStyleOriginal) {
|
||||
_scale = 1.0 / _devicePixelRatio;
|
||||
}
|
||||
final displayWidth = getDisplayWidth();
|
||||
final displayHeight = getDisplayHeight();
|
||||
_x = (size.width - displayWidth * _scale) / 2;
|
||||
_y = (size.height - displayHeight * _scale) / 2;
|
||||
bind.sessionSetViewStyle(sessionId: sessionId, value: _lastViewStyle.style);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
clear() {
|
||||
_x = 0;
|
||||
_y = 0;
|
||||
_scale = 1.0;
|
||||
if (notify) notifyListeners();
|
||||
}
|
||||
|
||||
updateScrollPercent() {
|
||||
@@ -1926,7 +2002,7 @@ class CursorModel with ChangeNotifier {
|
||||
_x = _displayOriginX;
|
||||
_y = _displayOriginY;
|
||||
parent.target?.inputModel.moveMouse(_x, _y);
|
||||
parent.target?.canvasModel.clear(true);
|
||||
parent.target?.canvasModel.reset();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -2281,25 +2357,7 @@ class RecordingModel with ChangeNotifier {
|
||||
WeakReference<FFI> parent;
|
||||
RecordingModel(this.parent);
|
||||
bool _start = false;
|
||||
get start => _start;
|
||||
|
||||
onSwitchDisplay() {
|
||||
if (isIOS || !_start) return;
|
||||
final sessionId = parent.target?.sessionId;
|
||||
int? width = parent.target?.canvasModel.getDisplayWidth();
|
||||
int? height = parent.target?.canvasModel.getDisplayHeight();
|
||||
if (sessionId == null || width == null || height == null) return;
|
||||
final pi = parent.target?.ffiModel.pi;
|
||||
if (pi == null) return;
|
||||
final currentDisplay = pi.currentDisplay;
|
||||
if (currentDisplay == kAllDisplayValue) return;
|
||||
bind.sessionRecordScreen(
|
||||
sessionId: sessionId,
|
||||
start: true,
|
||||
display: currentDisplay,
|
||||
width: width,
|
||||
height: height);
|
||||
}
|
||||
bool get start => _start;
|
||||
|
||||
toggle() async {
|
||||
if (isIOS) return;
|
||||
@@ -2307,48 +2365,16 @@ class RecordingModel with ChangeNotifier {
|
||||
if (sessionId == null) return;
|
||||
final pi = parent.target?.ffiModel.pi;
|
||||
if (pi == null) return;
|
||||
final currentDisplay = pi.currentDisplay;
|
||||
if (currentDisplay == kAllDisplayValue) return;
|
||||
_start = !_start;
|
||||
notifyListeners();
|
||||
await _sendStatusMessage(sessionId, pi, _start);
|
||||
if (_start) {
|
||||
sessionRefreshVideo(sessionId, pi);
|
||||
if (versionCmp(pi.version, '1.2.4') >= 0) {
|
||||
// will not receive SwitchDisplay since 1.2.4
|
||||
onSwitchDisplay();
|
||||
}
|
||||
} else {
|
||||
bind.sessionRecordScreen(
|
||||
sessionId: sessionId,
|
||||
start: false,
|
||||
display: currentDisplay,
|
||||
width: 0,
|
||||
height: 0);
|
||||
bool value = !_start;
|
||||
if (value) {
|
||||
await sessionRefreshVideo(sessionId, pi);
|
||||
}
|
||||
await bind.sessionRecordScreen(sessionId: sessionId, start: value);
|
||||
}
|
||||
|
||||
onClose() async {
|
||||
if (isIOS) return;
|
||||
final sessionId = parent.target?.sessionId;
|
||||
if (sessionId == null) return;
|
||||
if (!_start) return;
|
||||
_start = false;
|
||||
final pi = parent.target?.ffiModel.pi;
|
||||
if (pi == null) return;
|
||||
final currentDisplay = pi.currentDisplay;
|
||||
if (currentDisplay == kAllDisplayValue) return;
|
||||
await _sendStatusMessage(sessionId, pi, false);
|
||||
bind.sessionRecordScreen(
|
||||
sessionId: sessionId,
|
||||
start: false,
|
||||
display: currentDisplay,
|
||||
width: 0,
|
||||
height: 0);
|
||||
}
|
||||
|
||||
_sendStatusMessage(SessionID sessionId, PeerInfo pi, bool status) async {
|
||||
await bind.sessionRecordStatus(sessionId: sessionId, status: status);
|
||||
updateStatus(bool status) {
|
||||
_start = status;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2397,6 +2423,9 @@ class FFI {
|
||||
late final ElevationModel elevationModel; // session
|
||||
late final CmFileModel cmFileModel; // cm
|
||||
late final TextureModel textureModel; //session
|
||||
late final Peers recentPeersModel; // global
|
||||
late final Peers favoritePeersModel; // global
|
||||
late final Peers lanPeersModel; // global
|
||||
|
||||
FFI(SessionID? sId) {
|
||||
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
|
||||
@@ -2417,6 +2446,16 @@ class FFI {
|
||||
elevationModel = ElevationModel(WeakReference(this));
|
||||
cmFileModel = CmFileModel(WeakReference(this));
|
||||
textureModel = TextureModel(WeakReference(this));
|
||||
recentPeersModel = Peers(
|
||||
name: PeersModelName.recent,
|
||||
loadEvent: LoadEvent.recent,
|
||||
getInitPeers: null);
|
||||
favoritePeersModel = Peers(
|
||||
name: PeersModelName.favorite,
|
||||
loadEvent: LoadEvent.favorite,
|
||||
getInitPeers: null);
|
||||
lanPeersModel = Peers(
|
||||
name: PeersModelName.lan, loadEvent: LoadEvent.lan, getInitPeers: null);
|
||||
}
|
||||
|
||||
/// Mobile reuse FFI
|
||||
@@ -2437,6 +2476,7 @@ class FFI {
|
||||
String? switchUuid,
|
||||
String? password,
|
||||
bool? isSharedPassword,
|
||||
String? connToken,
|
||||
bool? forceRelay,
|
||||
int? tabWindowId,
|
||||
int? display,
|
||||
@@ -2473,6 +2513,7 @@ class FFI {
|
||||
forceRelay: forceRelay ?? false,
|
||||
password: password ?? '',
|
||||
isSharedPassword: isSharedPassword ?? false,
|
||||
connToken: connToken,
|
||||
);
|
||||
} else if (display != null) {
|
||||
if (displays == null) {
|
||||
|
||||
@@ -48,6 +48,12 @@ class PlatformFFI {
|
||||
|
||||
static get isMain => instance._appType == kAppTypeMain;
|
||||
|
||||
static String getByName(String name, [String arg = '']) {
|
||||
return '';
|
||||
}
|
||||
|
||||
static void setByName(String name, [String value = '']) {}
|
||||
|
||||
static Future<String> getVersion() async {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
return packageInfo.version;
|
||||
@@ -276,4 +282,6 @@ class PlatformFFI {
|
||||
void syncAndroidServiceAppDirConfigPath() {
|
||||
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
|
||||
}
|
||||
|
||||
void setFullscreenCallback(void Function(bool) fun) {}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ class PeerTabModel with ChangeNotifier {
|
||||
// https://github.com/flutter/flutter/issues/101275#issuecomment-1604541700
|
||||
// After onTap, the shift key should be pressed for a while when not in multiselection mode,
|
||||
// because onTap is delayed when onDoubleTap is not null
|
||||
if (isDesktop && !_isShiftDown) return;
|
||||
if (isDesktop || isWebDesktop) return;
|
||||
_multiSelectionMode = true;
|
||||
}
|
||||
final cached = _currentTabCachedPeers.map((e) => e.id).toList();
|
||||
|
||||
@@ -6,3 +6,11 @@ final platformFFI = PlatformFFI.instance;
|
||||
final localeName = PlatformFFI.localeName;
|
||||
|
||||
RustdeskImpl get bind => platformFFI.ffiBind;
|
||||
|
||||
String ffiGetByName(String name, [String arg = '']) {
|
||||
return PlatformFFI.getByName(name, arg);
|
||||
}
|
||||
|
||||
void ffiSetByName(String name, [String value = '']) {
|
||||
PlatformFFI.setByName(name, value);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ class StateGlobal {
|
||||
final RxBool showRemoteToolBar = false.obs;
|
||||
final svcStatus = SvcStatus.notReady.obs;
|
||||
final RxBool isFocused = false.obs;
|
||||
// for mobile and web
|
||||
bool isInMainPage = true;
|
||||
bool isWebVisible = true;
|
||||
|
||||
final isPortrait = false.obs;
|
||||
|
||||
@@ -70,27 +73,40 @@ class StateGlobal {
|
||||
if (_fullscreen.value != v) {
|
||||
_fullscreen.value = v;
|
||||
_showTabBar.value = !_fullscreen.value;
|
||||
refreshResizeEdgeSize();
|
||||
print(
|
||||
"fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
|
||||
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth;
|
||||
if (procWnd) {
|
||||
final wc = WindowController.fromWindowId(windowId);
|
||||
wc.setFullscreen(_fullscreen.isTrue).then((_) {
|
||||
// https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982
|
||||
if (isWindows && !v) {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
final frame = await wc.getFrame();
|
||||
final newRect = Rect.fromLTWH(
|
||||
frame.left, frame.top, frame.width + 1, frame.height + 1);
|
||||
await wc.setFrame(newRect);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (isWebDesktop) {
|
||||
procFullscreenWeb();
|
||||
} else {
|
||||
procFullscreenNative(procWnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
procFullscreenWeb() {
|
||||
final isFullscreen = ffiGetByName('fullscreen') == 'Y';
|
||||
String fullscreenValue = '';
|
||||
if (isFullscreen && _fullscreen.isFalse) {
|
||||
fullscreenValue = 'N';
|
||||
} else if (!isFullscreen && fullscreen.isTrue) {
|
||||
fullscreenValue = 'Y';
|
||||
}
|
||||
if (fullscreenValue.isNotEmpty) {
|
||||
ffiSetByName('fullscreen', fullscreenValue);
|
||||
}
|
||||
}
|
||||
|
||||
procFullscreenNative(bool procWnd) {
|
||||
refreshResizeEdgeSize();
|
||||
print("fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
|
||||
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth;
|
||||
if (procWnd) {
|
||||
final wc = WindowController.fromWindowId(windowId);
|
||||
wc.setFullscreen(_fullscreen.isTrue).then((_) {
|
||||
// We remove the redraw (width + 1, height + 1), because this issue cannot be reproduced.
|
||||
// https://github.com/rustdesk/rustdesk/issues/9675
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
refreshResizeEdgeSize() => _resizeEdgeSize.value = fullscreen.isTrue
|
||||
? kFullScreenEdgeSize
|
||||
: isMaximized.isTrue
|
||||
@@ -109,7 +125,13 @@ class StateGlobal {
|
||||
_inputSource = bind.mainGetInputSource();
|
||||
}
|
||||
|
||||
StateGlobal._();
|
||||
StateGlobal._() {
|
||||
if (isWebDesktop) {
|
||||
platformFFI.setFullscreenCallback((v) {
|
||||
_fullscreen.value = v;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static final StateGlobal instance = StateGlobal._();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:js';
|
||||
import 'dart:html';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
|
||||
import 'package:flutter_hbb/web/bridge.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
@@ -28,7 +30,15 @@ class PlatformFFI {
|
||||
context.callMethod('setByName', [name, value]);
|
||||
}
|
||||
|
||||
PlatformFFI._();
|
||||
PlatformFFI._() {
|
||||
window.document.addEventListener(
|
||||
'visibilitychange',
|
||||
(event) => {
|
||||
stateGlobal.isWebVisible =
|
||||
window.document.visibilityState == 'visible'
|
||||
});
|
||||
}
|
||||
|
||||
static final PlatformFFI instance = PlatformFFI._();
|
||||
|
||||
static get localeName => window.navigator.language;
|
||||
@@ -98,6 +108,10 @@ class PlatformFFI {
|
||||
sessionId: sessionId, display: display, ptr: ptr);
|
||||
|
||||
Future<void> init(String appType) async {
|
||||
Completer completer = Completer();
|
||||
context["onInitFinished"] = () {
|
||||
completer.complete();
|
||||
};
|
||||
context.callMethod('init');
|
||||
version = getByName('version');
|
||||
window.onContextMenu.listen((event) {
|
||||
@@ -112,6 +126,7 @@ class PlatformFFI {
|
||||
print('json.decode fail(): $e');
|
||||
}
|
||||
};
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void setEventCallback(void Function(Map<String, dynamic>) fun) {
|
||||
@@ -157,4 +172,10 @@ class PlatformFFI {
|
||||
|
||||
// just for compilation
|
||||
void syncAndroidServiceAppDirConfigPath() {}
|
||||
|
||||
void setFullscreenCallback(void Function(bool) fun) {
|
||||
context["onFullscreenChanged"] = (bool v) {
|
||||
fun(v);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,9 @@ class RustDeskMultiWindowManager {
|
||||
bool withScreenRect,
|
||||
) async {
|
||||
final windowController = await DesktopMultiWindow.createWindow(msg);
|
||||
if (isWindows) {
|
||||
windowController.setInitBackgroundColor(Colors.black);
|
||||
}
|
||||
final windowId = windowController.windowId;
|
||||
if (!withScreenRect) {
|
||||
windowController
|
||||
@@ -198,6 +201,7 @@ class RustDeskMultiWindowManager {
|
||||
String? switchUuid,
|
||||
bool? isRDP,
|
||||
bool? isSharedPassword,
|
||||
String? connToken,
|
||||
}) async {
|
||||
var params = {
|
||||
"type": type.index,
|
||||
@@ -214,6 +218,9 @@ class RustDeskMultiWindowManager {
|
||||
if (isSharedPassword != null) {
|
||||
params['isSharedPassword'] = isSharedPassword;
|
||||
}
|
||||
if (connToken != null) {
|
||||
params['connToken'] = connToken;
|
||||
}
|
||||
final msg = jsonEncode(params);
|
||||
|
||||
// separate window for file transfer is not supported
|
||||
@@ -251,8 +258,13 @@ class RustDeskMultiWindowManager {
|
||||
);
|
||||
}
|
||||
|
||||
Future<MultiWindowCallResult> newFileTransfer(String remoteId,
|
||||
{String? password, bool? isSharedPassword, bool? forceRelay}) async {
|
||||
Future<MultiWindowCallResult> newFileTransfer(
|
||||
String remoteId, {
|
||||
String? password,
|
||||
bool? isSharedPassword,
|
||||
bool? forceRelay,
|
||||
String? connToken,
|
||||
}) async {
|
||||
return await newSession(
|
||||
WindowType.FileTransfer,
|
||||
kWindowEventNewFileTransfer,
|
||||
@@ -261,11 +273,18 @@ class RustDeskMultiWindowManager {
|
||||
password: password,
|
||||
forceRelay: forceRelay,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
);
|
||||
}
|
||||
|
||||
Future<MultiWindowCallResult> newPortForward(String remoteId, bool isRDP,
|
||||
{String? password, bool? isSharedPassword, bool? forceRelay}) async {
|
||||
Future<MultiWindowCallResult> newPortForward(
|
||||
String remoteId,
|
||||
bool isRDP, {
|
||||
String? password,
|
||||
bool? isSharedPassword,
|
||||
bool? forceRelay,
|
||||
String? connToken,
|
||||
}) async {
|
||||
return await newSession(
|
||||
WindowType.PortForward,
|
||||
kWindowEventNewPortForward,
|
||||
@@ -275,6 +294,7 @@ class RustDeskMultiWindowManager {
|
||||
forceRelay: forceRelay,
|
||||
isRDP: isRDP,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
import 'dart:js' as js;
|
||||
import 'dart:html' as html;
|
||||
// cycle imports, maybe we can improve this
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
|
||||
final isAndroid_ = false;
|
||||
final isIOS_ = false;
|
||||
@@ -13,8 +15,7 @@ final isDesktop_ = false;
|
||||
|
||||
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);
|
||||
|
||||
final _userAgent = html.window.navigator.userAgent.toLowerCase();
|
||||
|
||||
final isWebOnWindows_ = _userAgent.contains('win');
|
||||
final isWebOnLinux_ = _userAgent.contains('linux');
|
||||
final isWebOnMacOS_ = _userAgent.contains('mac');
|
||||
final _localOs = js.context.callMethod('getByName', ['local_os', '']);
|
||||
final isWebOnWindows_ = _localOs == kPeerPlatformWindows;
|
||||
final isWebOnLinux_ = _localOs == kPeerPlatformLinux;
|
||||
final isWebOnMacOS_ = _localOs == kPeerPlatformMacOS;
|
||||
|
||||
14
flutter/lib/web/dummy.dart
Normal file
14
flutter/lib/web/dummy.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
Future<void> webselectFiles({required bool is_folder}) async {
|
||||
throw UnimplementedError("webselectFiles");
|
||||
}
|
||||
|
||||
Future<void> webSendLocalFiles(
|
||||
{required int handleIndex,
|
||||
required int actId,
|
||||
required String path,
|
||||
required String to,
|
||||
required int fileNum,
|
||||
required bool includeHidden,
|
||||
required bool isRemote}) {
|
||||
throw UnimplementedError("webSendLocalFiles");
|
||||
}
|
||||
@@ -1,23 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/mobile/pages/scan_page.dart';
|
||||
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/login.dart';
|
||||
import '../../models/model.dart';
|
||||
|
||||
class WebSettingsPage extends StatelessWidget {
|
||||
const WebSettingsPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isWebDesktop) {
|
||||
return _buildDesktopButton(context);
|
||||
} else {
|
||||
return _buildMobileMenu(context);
|
||||
}
|
||||
return _buildDesktopButton(context);
|
||||
}
|
||||
|
||||
Widget _buildDesktopButton(BuildContext context) {
|
||||
@@ -34,65 +23,4 @@ class WebSettingsPage extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileMenu(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
return PopupMenuButton<String>(
|
||||
tooltip: "",
|
||||
icon: const Icon(Icons.more_vert),
|
||||
itemBuilder: (context) {
|
||||
return (isIOS
|
||||
? [
|
||||
const PopupMenuItem(
|
||||
value: "scan",
|
||||
child: Icon(Icons.qr_code_scanner, color: Colors.black),
|
||||
)
|
||||
]
|
||||
: <PopupMenuItem<String>>[]) +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "server",
|
||||
child: Text(translate('ID/Relay Server')),
|
||||
)
|
||||
] +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "login",
|
||||
child: Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate("Login")
|
||||
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
|
||||
)
|
||||
] +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "about",
|
||||
child: Text(translate('About RustDesk')),
|
||||
)
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
if (value == 'server') {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
}
|
||||
if (value == 'about') {
|
||||
showAbout(gFFI.dialogManager);
|
||||
}
|
||||
if (value == 'login') {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
loginDialog();
|
||||
} else {
|
||||
logOutConfirmDialog();
|
||||
}
|
||||
}
|
||||
if (value == 'scan') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => ScanPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
30
flutter/lib/web/web_unique.dart
Normal file
30
flutter/lib/web/web_unique.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:js' as js;
|
||||
|
||||
Future<void> webselectFiles({required bool is_folder}) async {
|
||||
return Future(
|
||||
() => js.context.callMethod('setByName', ['select_files', is_folder]));
|
||||
}
|
||||
|
||||
Future<void> webSendLocalFiles(
|
||||
{required int handleIndex,
|
||||
required int actId,
|
||||
required String path,
|
||||
required String to,
|
||||
required int fileNum,
|
||||
required bool includeHidden,
|
||||
required bool isRemote}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'send_local_files',
|
||||
jsonEncode({
|
||||
'id': actId,
|
||||
'handle_index': handleIndex,
|
||||
'path': path,
|
||||
'to': to,
|
||||
'file_num': fileNum,
|
||||
'include_hidden': includeHidden,
|
||||
'is_remote': isRemote,
|
||||
})
|
||||
]));
|
||||
}
|
||||
@@ -335,7 +335,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "80b063b9d4e015f62e17f42a5aa0b3d20a365926"
|
||||
resolved-ref: "519350f1f40746798299e94786197d058353bac9"
|
||||
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# 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
|
||||
version: 1.3.1+47
|
||||
version: 1.3.2+51
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
|
||||
@@ -111,11 +111,17 @@ pub struct Enigo {
|
||||
double_click_interval: u32,
|
||||
last_click_time: Option<std::time::Instant>,
|
||||
multiple_click: i64,
|
||||
ignore_flags: bool,
|
||||
flags: CGEventFlags,
|
||||
char_to_vkey_map: Map<String, Map<char, CGKeyCode>>,
|
||||
}
|
||||
|
||||
impl Enigo {
|
||||
/// Set if ignore flags when posting events.
|
||||
pub fn set_ignore_flags(&mut self, ignore: bool) {
|
||||
self.ignore_flags = ignore;
|
||||
}
|
||||
|
||||
///
|
||||
pub fn reset_flag(&mut self) {
|
||||
self.flags = CGEventFlags::CGEventFlagNull;
|
||||
@@ -136,7 +142,9 @@ impl Enigo {
|
||||
}
|
||||
|
||||
fn post(&self, event: CGEvent) {
|
||||
event.set_flags(self.flags);
|
||||
if !self.ignore_flags {
|
||||
event.set_flags(self.flags);
|
||||
}
|
||||
event.set_integer_value_field(EventField::EVENT_SOURCE_USER_DATA, ENIGO_INPUT_EXTRA_VALUE);
|
||||
event.post(CGEventTapLocation::HID);
|
||||
}
|
||||
@@ -164,6 +172,7 @@ impl Default for Enigo {
|
||||
double_click_interval,
|
||||
multiple_click: 1,
|
||||
last_click_time: None,
|
||||
ignore_flags: false,
|
||||
flags: CGEventFlags::CGEventFlagNull,
|
||||
char_to_vkey_map: Default::default(),
|
||||
}
|
||||
|
||||
@@ -965,6 +965,10 @@ impl Config {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_bool_option(k: &str) -> bool {
|
||||
option2bool(k, &Self::get_option(k))
|
||||
}
|
||||
|
||||
pub fn set_option(k: String, v: String) {
|
||||
if !is_option_can_save(&OVERWRITE_SETTINGS, &k, &DEFAULT_SETTINGS, &v) {
|
||||
return;
|
||||
@@ -1558,6 +1562,21 @@ impl LocalConfig {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// Usually get_option should be used.
|
||||
pub fn get_option_from_file(k: &str) -> String {
|
||||
get_or(
|
||||
&OVERWRITE_LOCAL_SETTINGS,
|
||||
&Self::load().options,
|
||||
&DEFAULT_LOCAL_SETTINGS,
|
||||
k,
|
||||
)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_bool_option(k: &str) -> bool {
|
||||
option2bool(k, &Self::get_option(k))
|
||||
}
|
||||
|
||||
pub fn set_option(k: String, v: String) {
|
||||
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k, &DEFAULT_LOCAL_SETTINGS, &v) {
|
||||
return;
|
||||
@@ -2198,6 +2217,7 @@ pub mod keys {
|
||||
pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout";
|
||||
pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open";
|
||||
pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming";
|
||||
pub const OPTION_ALLOW_AUTO_RECORD_OUTGOING: &str = "allow-auto-record-outgoing";
|
||||
pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory";
|
||||
pub const OPTION_ENABLE_ABR: &str = "enable-abr";
|
||||
pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper";
|
||||
@@ -2321,6 +2341,8 @@ pub mod keys {
|
||||
OPTION_DISABLE_GROUP_PANEL,
|
||||
OPTION_PRE_ELEVATE_SERVICE,
|
||||
OPTION_ALLOW_REMOTE_CM_MODIFICATION,
|
||||
OPTION_ALLOW_AUTO_RECORD_OUTGOING,
|
||||
OPTION_VIDEO_SAVE_DIRECTORY,
|
||||
];
|
||||
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS
|
||||
pub const KEYS_SETTINGS: &[&str] = &[
|
||||
@@ -2342,7 +2364,6 @@ pub mod keys {
|
||||
OPTION_AUTO_DISCONNECT_TIMEOUT,
|
||||
OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN,
|
||||
OPTION_ALLOW_AUTO_RECORD_INCOMING,
|
||||
OPTION_VIDEO_SAVE_DIRECTORY,
|
||||
OPTION_ENABLE_ABR,
|
||||
OPTION_ALLOW_REMOVE_WALLPAPER,
|
||||
OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.3.1"
|
||||
version = "1.3.2"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
|
||||
@@ -62,4 +62,3 @@ gstreamer-video = { version = "0.16", optional = true }
|
||||
git = "https://github.com/rustdesk-org/hwcodec"
|
||||
optional = true
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::{
|
||||
aom::{self, AomDecoder, AomEncoder, AomEncoderConfig},
|
||||
common::GoogleImage,
|
||||
vpxcodec::{self, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId},
|
||||
CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb,
|
||||
CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb, ImageTexture,
|
||||
};
|
||||
|
||||
use hbb_common::{
|
||||
@@ -623,7 +623,7 @@ impl Decoder {
|
||||
&mut self,
|
||||
frame: &video_frame::Union,
|
||||
rgb: &mut ImageRgb,
|
||||
_texture: &mut *mut c_void,
|
||||
_texture: &mut ImageTexture,
|
||||
_pixelbuffer: &mut bool,
|
||||
chroma: &mut Option<Chroma>,
|
||||
) -> ResultType<bool> {
|
||||
@@ -777,12 +777,16 @@ impl Decoder {
|
||||
fn handle_vram_video_frame(
|
||||
decoder: &mut VRamDecoder,
|
||||
frames: &EncodedVideoFrames,
|
||||
texture: &mut *mut c_void,
|
||||
texture: &mut ImageTexture,
|
||||
) -> ResultType<bool> {
|
||||
let mut ret = false;
|
||||
for h26x in frames.frames.iter() {
|
||||
for image in decoder.decode(&h26x.data)? {
|
||||
*texture = image.frame.texture;
|
||||
*texture = ImageTexture {
|
||||
texture: image.frame.texture,
|
||||
w: image.frame.width as _,
|
||||
h: image.frame.height as _,
|
||||
};
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,22 @@ impl ImageRgb {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImageTexture {
|
||||
pub texture: *mut c_void,
|
||||
pub w: usize,
|
||||
pub h: usize,
|
||||
}
|
||||
|
||||
impl Default for ImageTexture {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
texture: std::ptr::null_mut(),
|
||||
w: 0,
|
||||
h: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn would_block_if_equal(old: &mut Vec<u8>, b: &[u8]) -> std::io::Result<()> {
|
||||
// does this really help?
|
||||
@@ -156,7 +172,7 @@ pub trait TraitPixelBuffer {
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
pub enum Frame<'a> {
|
||||
PixelBuffer(PixelBuffer<'a>),
|
||||
Texture(*mut c_void),
|
||||
Texture((*mut c_void, usize)),
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
@@ -164,7 +180,7 @@ impl Frame<'_> {
|
||||
pub fn valid<'a>(&'a self) -> bool {
|
||||
match self {
|
||||
Frame::PixelBuffer(pixelbuffer) => !pixelbuffer.data().is_empty(),
|
||||
Frame::Texture(texture) => !texture.is_null(),
|
||||
Frame::Texture((texture, _)) => !texture.is_null(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +202,7 @@ impl Frame<'_> {
|
||||
|
||||
pub enum EncodeInput<'a> {
|
||||
YUV(&'a [u8]),
|
||||
Texture(*mut c_void),
|
||||
Texture((*mut c_void, usize)),
|
||||
}
|
||||
|
||||
impl<'a> EncodeInput<'a> {
|
||||
@@ -197,7 +213,7 @@ impl<'a> EncodeInput<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn texture(&self) -> ResultType<*mut c_void> {
|
||||
pub fn texture(&self) -> ResultType<(*mut c_void, usize)> {
|
||||
match self {
|
||||
Self::Texture(f) => Ok(*f),
|
||||
_ => bail!("not texture frame"),
|
||||
@@ -296,6 +312,19 @@ impl From<&VideoFrame> for CodecFormat {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&video_frame::Union> for CodecFormat {
|
||||
fn from(it: &video_frame::Union) -> Self {
|
||||
match it {
|
||||
video_frame::Union::Vp8s(_) => CodecFormat::VP8,
|
||||
video_frame::Union::Vp9s(_) => CodecFormat::VP9,
|
||||
video_frame::Union::Av1s(_) => CodecFormat::AV1,
|
||||
video_frame::Union::H264s(_) => CodecFormat::H264,
|
||||
video_frame::Union::H265s(_) => CodecFormat::H265,
|
||||
_ => CodecFormat::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CodecName> for CodecFormat {
|
||||
fn from(value: &CodecName) -> Self {
|
||||
match value {
|
||||
|
||||
@@ -25,22 +25,28 @@ pub struct RecorderContext {
|
||||
pub server: bool,
|
||||
pub id: String,
|
||||
pub dir: String,
|
||||
pub display: usize,
|
||||
pub tx: Option<Sender<RecordState>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RecorderContext2 {
|
||||
pub filename: String,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub format: CodecFormat,
|
||||
pub tx: Option<Sender<RecordState>>,
|
||||
}
|
||||
|
||||
impl RecorderContext {
|
||||
pub fn set_filename(&mut self) -> ResultType<()> {
|
||||
if !PathBuf::from(&self.dir).exists() {
|
||||
std::fs::create_dir_all(&self.dir)?;
|
||||
impl RecorderContext2 {
|
||||
pub fn set_filename(&mut self, ctx: &RecorderContext) -> ResultType<()> {
|
||||
if !PathBuf::from(&ctx.dir).exists() {
|
||||
std::fs::create_dir_all(&ctx.dir)?;
|
||||
}
|
||||
let file = if self.server { "incoming" } else { "outgoing" }.to_string()
|
||||
let file = if ctx.server { "incoming" } else { "outgoing" }.to_string()
|
||||
+ "_"
|
||||
+ &self.id.clone()
|
||||
+ &ctx.id.clone()
|
||||
+ &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string()
|
||||
+ &format!("display{}_", ctx.display)
|
||||
+ &self.format.to_string().to_lowercase()
|
||||
+ if self.format == CodecFormat::VP9
|
||||
|| self.format == CodecFormat::VP8
|
||||
@@ -50,11 +56,10 @@ impl RecorderContext {
|
||||
} else {
|
||||
".mp4"
|
||||
};
|
||||
self.filename = PathBuf::from(&self.dir)
|
||||
self.filename = PathBuf::from(&ctx.dir)
|
||||
.join(file)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
log::info!("video will save to {}", self.filename);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -63,7 +68,7 @@ unsafe impl Send for Recorder {}
|
||||
unsafe impl Sync for Recorder {}
|
||||
|
||||
pub trait RecorderApi {
|
||||
fn new(ctx: RecorderContext) -> ResultType<Self>
|
||||
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool;
|
||||
@@ -78,13 +83,15 @@ pub enum RecordState {
|
||||
}
|
||||
|
||||
pub struct Recorder {
|
||||
pub inner: Box<dyn RecorderApi>,
|
||||
pub inner: Option<Box<dyn RecorderApi>>,
|
||||
ctx: RecorderContext,
|
||||
ctx2: Option<RecorderContext2>,
|
||||
pts: Option<i64>,
|
||||
check_failed: bool,
|
||||
}
|
||||
|
||||
impl Deref for Recorder {
|
||||
type Target = Box<dyn RecorderApi>;
|
||||
type Target = Option<Box<dyn RecorderApi>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
@@ -98,114 +105,123 @@ impl DerefMut for Recorder {
|
||||
}
|
||||
|
||||
impl Recorder {
|
||||
pub fn new(mut ctx: RecorderContext) -> ResultType<Self> {
|
||||
ctx.set_filename()?;
|
||||
let recorder = match ctx.format {
|
||||
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Recorder {
|
||||
inner: Box::new(WebmRecorder::new(ctx.clone())?),
|
||||
ctx,
|
||||
pts: None,
|
||||
},
|
||||
#[cfg(feature = "hwcodec")]
|
||||
_ => Recorder {
|
||||
inner: Box::new(HwRecorder::new(ctx.clone())?),
|
||||
ctx,
|
||||
pts: None,
|
||||
},
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
_ => bail!("unsupported codec type"),
|
||||
};
|
||||
recorder.send_state(RecordState::NewFile(recorder.ctx.filename.clone()));
|
||||
Ok(recorder)
|
||||
pub fn new(ctx: RecorderContext) -> ResultType<Self> {
|
||||
Ok(Self {
|
||||
inner: None,
|
||||
ctx,
|
||||
ctx2: None,
|
||||
pts: None,
|
||||
check_failed: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> {
|
||||
ctx.set_filename()?;
|
||||
self.inner = match ctx.format {
|
||||
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => {
|
||||
Box::new(WebmRecorder::new(ctx.clone())?)
|
||||
fn check(&mut self, w: usize, h: usize, format: CodecFormat) -> ResultType<()> {
|
||||
match self.ctx2 {
|
||||
Some(ref ctx2) => {
|
||||
if ctx2.width != w || ctx2.height != h || ctx2.format != format {
|
||||
let mut ctx2 = RecorderContext2 {
|
||||
width: w,
|
||||
height: h,
|
||||
format,
|
||||
filename: Default::default(),
|
||||
};
|
||||
ctx2.set_filename(&self.ctx)?;
|
||||
self.ctx2 = Some(ctx2);
|
||||
self.inner = None;
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
_ => Box::new(HwRecorder::new(ctx.clone())?),
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
_ => bail!("unsupported codec type"),
|
||||
None => {
|
||||
let mut ctx2 = RecorderContext2 {
|
||||
width: w,
|
||||
height: h,
|
||||
format,
|
||||
filename: Default::default(),
|
||||
};
|
||||
ctx2.set_filename(&self.ctx)?;
|
||||
self.ctx2 = Some(ctx2);
|
||||
self.inner = None;
|
||||
}
|
||||
}
|
||||
let Some(ctx2) = &self.ctx2 else {
|
||||
bail!("ctx2 is None");
|
||||
};
|
||||
self.ctx = ctx;
|
||||
self.pts = None;
|
||||
self.send_state(RecordState::NewFile(self.ctx.filename.clone()));
|
||||
if self.inner.is_none() {
|
||||
self.inner = match format {
|
||||
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Some(Box::new(
|
||||
WebmRecorder::new(self.ctx.clone(), (*ctx2).clone())?,
|
||||
)),
|
||||
#[cfg(feature = "hwcodec")]
|
||||
_ => Some(Box::new(HwRecorder::new(
|
||||
self.ctx.clone(),
|
||||
(*ctx2).clone(),
|
||||
)?)),
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
_ => bail!("unsupported codec type"),
|
||||
};
|
||||
self.pts = None;
|
||||
self.send_state(RecordState::NewFile(ctx2.filename.clone()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_message(&mut self, msg: &Message) {
|
||||
pub fn write_message(&mut self, msg: &Message, w: usize, h: usize) {
|
||||
if let Some(message::Union::VideoFrame(vf)) = &msg.union {
|
||||
if let Some(frame) = &vf.union {
|
||||
self.write_frame(frame).ok();
|
||||
self.write_frame(frame, w, h).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> {
|
||||
pub fn write_frame(
|
||||
&mut self,
|
||||
frame: &video_frame::Union,
|
||||
w: usize,
|
||||
h: usize,
|
||||
) -> ResultType<()> {
|
||||
if self.check_failed {
|
||||
bail!("check failed");
|
||||
}
|
||||
let format = CodecFormat::from(frame);
|
||||
if format == CodecFormat::Unknown {
|
||||
bail!("unsupported frame type");
|
||||
}
|
||||
let res = self.check(w, h, format);
|
||||
if res.is_err() {
|
||||
self.check_failed = true;
|
||||
log::error!("check failed: {:?}", res);
|
||||
res?;
|
||||
}
|
||||
match frame {
|
||||
video_frame::Union::Vp8s(vp8s) => {
|
||||
if self.ctx.format != CodecFormat::VP8 {
|
||||
self.change(RecorderContext {
|
||||
format: CodecFormat::VP8,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
for f in vp8s.frames.iter() {
|
||||
self.check_pts(f.pts)?;
|
||||
self.write_video(f);
|
||||
self.check_pts(f.pts, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
video_frame::Union::Vp9s(vp9s) => {
|
||||
if self.ctx.format != CodecFormat::VP9 {
|
||||
self.change(RecorderContext {
|
||||
format: CodecFormat::VP9,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
for f in vp9s.frames.iter() {
|
||||
self.check_pts(f.pts)?;
|
||||
self.write_video(f);
|
||||
self.check_pts(f.pts, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
video_frame::Union::Av1s(av1s) => {
|
||||
if self.ctx.format != CodecFormat::AV1 {
|
||||
self.change(RecorderContext {
|
||||
format: CodecFormat::AV1,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
for f in av1s.frames.iter() {
|
||||
self.check_pts(f.pts)?;
|
||||
self.write_video(f);
|
||||
self.check_pts(f.pts, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H264s(h264s) => {
|
||||
if self.ctx.format != CodecFormat::H264 {
|
||||
self.change(RecorderContext {
|
||||
format: CodecFormat::H264,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
for f in h264s.frames.iter() {
|
||||
self.check_pts(f.pts)?;
|
||||
self.write_video(f);
|
||||
self.check_pts(f.pts, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H265s(h265s) => {
|
||||
if self.ctx.format != CodecFormat::H265 {
|
||||
self.change(RecorderContext {
|
||||
format: CodecFormat::H265,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
for f in h265s.frames.iter() {
|
||||
self.check_pts(f.pts)?;
|
||||
self.write_video(f);
|
||||
self.check_pts(f.pts, w, h, format)?;
|
||||
self.as_mut().map(|x| x.write_video(f));
|
||||
}
|
||||
}
|
||||
_ => bail!("unsupported frame type"),
|
||||
@@ -214,13 +230,21 @@ impl Recorder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_pts(&mut self, pts: i64) -> ResultType<()> {
|
||||
fn check_pts(&mut self, pts: i64, w: usize, h: usize, format: CodecFormat) -> ResultType<()> {
|
||||
// https://stackoverflow.com/questions/76379101/how-to-create-one-playable-webm-file-from-two-different-video-tracks-with-same-c
|
||||
let old_pts = self.pts;
|
||||
self.pts = Some(pts);
|
||||
if old_pts.clone().unwrap_or_default() > pts {
|
||||
log::info!("pts {:?} -> {}, change record filename", old_pts, pts);
|
||||
self.change(self.ctx.clone())?;
|
||||
self.inner = None;
|
||||
self.ctx2 = None;
|
||||
let res = self.check(w, h, format);
|
||||
if res.is_err() {
|
||||
self.check_failed = true;
|
||||
log::error!("check failed: {:?}", res);
|
||||
res?;
|
||||
}
|
||||
self.pts = Some(pts);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -234,21 +258,22 @@ struct WebmRecorder {
|
||||
vt: VideoTrack,
|
||||
webm: Option<Segment<Writer<File>>>,
|
||||
ctx: RecorderContext,
|
||||
ctx2: RecorderContext2,
|
||||
key: bool,
|
||||
written: bool,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl RecorderApi for WebmRecorder {
|
||||
fn new(ctx: RecorderContext) -> ResultType<Self> {
|
||||
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self> {
|
||||
let out = match {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&ctx.filename)
|
||||
.open(&ctx2.filename)
|
||||
} {
|
||||
Ok(file) => file,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx.filename)?,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx2.filename)?,
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let mut webm = match mux::Segment::new(mux::Writer::new(out)) {
|
||||
@@ -256,18 +281,18 @@ impl RecorderApi for WebmRecorder {
|
||||
None => bail!("Failed to create webm mux"),
|
||||
};
|
||||
let vt = webm.add_video_track(
|
||||
ctx.width as _,
|
||||
ctx.height as _,
|
||||
ctx2.width as _,
|
||||
ctx2.height as _,
|
||||
None,
|
||||
if ctx.format == CodecFormat::VP9 {
|
||||
if ctx2.format == CodecFormat::VP9 {
|
||||
mux::VideoCodecId::VP9
|
||||
} else if ctx.format == CodecFormat::VP8 {
|
||||
} else if ctx2.format == CodecFormat::VP8 {
|
||||
mux::VideoCodecId::VP8
|
||||
} else {
|
||||
mux::VideoCodecId::AV1
|
||||
},
|
||||
);
|
||||
if ctx.format == CodecFormat::AV1 {
|
||||
if ctx2.format == CodecFormat::AV1 {
|
||||
// [129, 8, 12, 0] in 3.6.0, but zero works
|
||||
let codec_private = vec![0, 0, 0, 0];
|
||||
if !webm.set_codec_private(vt.track_number(), &codec_private) {
|
||||
@@ -278,6 +303,7 @@ impl RecorderApi for WebmRecorder {
|
||||
vt,
|
||||
webm: Some(webm),
|
||||
ctx,
|
||||
ctx2,
|
||||
key: false,
|
||||
written: false,
|
||||
start: Instant::now(),
|
||||
@@ -307,7 +333,7 @@ impl Drop for WebmRecorder {
|
||||
let _ = std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None));
|
||||
let mut state = RecordState::WriteTail;
|
||||
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
|
||||
std::fs::remove_file(&self.ctx.filename).ok();
|
||||
std::fs::remove_file(&self.ctx2.filename).ok();
|
||||
state = RecordState::RemoveFile;
|
||||
}
|
||||
self.ctx.tx.as_ref().map(|tx| tx.send(state));
|
||||
@@ -318,6 +344,7 @@ impl Drop for WebmRecorder {
|
||||
struct HwRecorder {
|
||||
muxer: Muxer,
|
||||
ctx: RecorderContext,
|
||||
ctx2: RecorderContext2,
|
||||
written: bool,
|
||||
key: bool,
|
||||
start: Instant,
|
||||
@@ -325,18 +352,19 @@ struct HwRecorder {
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
impl RecorderApi for HwRecorder {
|
||||
fn new(ctx: RecorderContext) -> ResultType<Self> {
|
||||
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self> {
|
||||
let muxer = Muxer::new(MuxContext {
|
||||
filename: ctx.filename.clone(),
|
||||
width: ctx.width,
|
||||
height: ctx.height,
|
||||
is265: ctx.format == CodecFormat::H265,
|
||||
filename: ctx2.filename.clone(),
|
||||
width: ctx2.width,
|
||||
height: ctx2.height,
|
||||
is265: ctx2.format == CodecFormat::H265,
|
||||
framerate: crate::hwcodec::DEFAULT_FPS as _,
|
||||
})
|
||||
.map_err(|_| anyhow!("Failed to create hardware muxer"))?;
|
||||
Ok(HwRecorder {
|
||||
muxer,
|
||||
ctx,
|
||||
ctx2,
|
||||
written: false,
|
||||
key: false,
|
||||
start: Instant::now(),
|
||||
@@ -365,7 +393,7 @@ impl Drop for HwRecorder {
|
||||
self.muxer.write_tail().ok();
|
||||
let mut state = RecordState::WriteTail;
|
||||
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
|
||||
std::fs::remove_file(&self.ctx.filename).ok();
|
||||
std::fs::remove_file(&self.ctx2.filename).ok();
|
||||
state = RecordState::RemoveFile;
|
||||
}
|
||||
self.ctx.tx.as_ref().map(|tx| tx.send(state));
|
||||
|
||||
@@ -101,7 +101,12 @@ impl EncoderApi for VRamEncoder {
|
||||
frame: EncodeInput,
|
||||
ms: i64,
|
||||
) -> ResultType<hbb_common::message_proto::VideoFrame> {
|
||||
let texture = frame.texture()?;
|
||||
let (texture, rotation) = frame.texture()?;
|
||||
if rotation != 0 {
|
||||
// to-do: support rotation
|
||||
// Both the encoder and display(w,h) information need to be changed.
|
||||
bail!("rotation not supported");
|
||||
}
|
||||
let mut vf = VideoFrame::new();
|
||||
let mut frames = Vec::new();
|
||||
for frame in self
|
||||
|
||||
@@ -253,7 +253,17 @@ impl Capturer {
|
||||
|
||||
pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result<Frame<'a>> {
|
||||
if self.output_texture {
|
||||
Ok(Frame::Texture(self.get_texture(timeout)?))
|
||||
let rotation = match self.display.rotation() {
|
||||
DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => 0,
|
||||
DXGI_MODE_ROTATION_ROTATE90 => 90,
|
||||
DXGI_MODE_ROTATION_ROTATE180 => 180,
|
||||
DXGI_MODE_ROTATION_ROTATE270 => 270,
|
||||
_ => {
|
||||
// Unsupported rotation, try anyway
|
||||
0
|
||||
}
|
||||
};
|
||||
Ok(Frame::Texture((self.get_texture(timeout)?, rotation)))
|
||||
} else {
|
||||
let width = self.width;
|
||||
let height = self.height;
|
||||
|
||||
@@ -14,15 +14,10 @@ case $1 in
|
||||
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service || true
|
||||
|
||||
# workaround temp dev build between 1.1.9 and 1.2.0
|
||||
ubuntuVersion=$(grep -oP 'VERSION_ID="\K[\d]+' /etc/os-release | bc -l)
|
||||
waylandSupportVersion=21
|
||||
if [ "$ubuntuVersion" != "" ] && [ "$ubuntuVersion" -ge "$waylandSupportVersion" ]
|
||||
serverUser=$(ps -ef | grep -E 'rustdesk +--server' | grep -v 'sudo ' | awk '{print $1}' | head -1)
|
||||
if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ]
|
||||
then
|
||||
serverUser=$(ps -ef | grep -E 'rustdesk +--server' | grep -v 'sudo ' | awk '{print $1}' | head -1)
|
||||
if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ]
|
||||
then
|
||||
systemctl --machine=${serverUser}@.host --user stop rustdesk >/dev/null 2>&1 || true
|
||||
fi
|
||||
systemctl --machine=${serverUser}@.host --user stop rustdesk >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pkgname=rustdesk
|
||||
pkgver=1.3.1
|
||||
pkgver=1.3.2
|
||||
pkgrel=0
|
||||
epoch=
|
||||
pkgdesc=""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.3.1
|
||||
Version: 1.3.2
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.3.1
|
||||
Version: 1.3.2
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.3.1
|
||||
Version: 1.3.2
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
113
src/client.rs
113
src/client.rs
@@ -11,6 +11,7 @@ use crossbeam_queue::ArrayQueue;
|
||||
use magnum_opus::{Channels::*, Decoder as AudioDecoder};
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
use ringbuf::{ring_buffer::RbBase, Rb};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -30,7 +31,6 @@ pub use file_trait::FileManager;
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use hbb_common::tokio::sync::mpsc::UnboundedSender;
|
||||
use hbb_common::tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
anyhow::{anyhow, Context},
|
||||
@@ -54,11 +54,15 @@ use hbb_common::{
|
||||
},
|
||||
AddrMangle, ResultType, Stream,
|
||||
};
|
||||
use hbb_common::{
|
||||
config::keys::OPTION_ALLOW_AUTO_RECORD_OUTGOING,
|
||||
tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver},
|
||||
};
|
||||
pub use helper::*;
|
||||
use scrap::{
|
||||
codec::Decoder,
|
||||
record::{Recorder, RecorderContext},
|
||||
CodecFormat, ImageFormat, ImageRgb,
|
||||
CodecFormat, ImageFormat, ImageRgb, ImageTexture,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -1146,7 +1150,7 @@ impl AudioHandler {
|
||||
pub struct VideoHandler {
|
||||
decoder: Decoder,
|
||||
pub rgb: ImageRgb,
|
||||
pub texture: *mut c_void,
|
||||
pub texture: ImageTexture,
|
||||
recorder: Arc<Mutex<Option<Recorder>>>,
|
||||
record: bool,
|
||||
_display: usize, // useful for debug
|
||||
@@ -1172,7 +1176,7 @@ impl VideoHandler {
|
||||
VideoHandler {
|
||||
decoder: Decoder::new(format, luid),
|
||||
rgb: ImageRgb::new(ImageFormat::ARGB, crate::get_dst_align_rgba()),
|
||||
texture: std::ptr::null_mut(),
|
||||
texture: Default::default(),
|
||||
recorder: Default::default(),
|
||||
record: false,
|
||||
_display,
|
||||
@@ -1220,11 +1224,14 @@ impl VideoHandler {
|
||||
}
|
||||
self.first_frame = false;
|
||||
if self.record {
|
||||
self.recorder
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_mut()
|
||||
.map(|r| r.write_frame(frame));
|
||||
self.recorder.lock().unwrap().as_mut().map(|r| {
|
||||
let (w, h) = if *pixelbuffer {
|
||||
(self.rgb.w, self.rgb.h)
|
||||
} else {
|
||||
(self.texture.w, self.texture.h)
|
||||
};
|
||||
r.write_frame(frame, w, h).ok();
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
@@ -1248,17 +1255,14 @@ impl VideoHandler {
|
||||
}
|
||||
|
||||
/// Start or stop screen record.
|
||||
pub fn record_screen(&mut self, start: bool, w: i32, h: i32, id: String) {
|
||||
pub fn record_screen(&mut self, start: bool, id: String, display: usize) {
|
||||
self.record = false;
|
||||
if start {
|
||||
self.recorder = Recorder::new(RecorderContext {
|
||||
server: false,
|
||||
id,
|
||||
dir: crate::ui_interface::video_save_directory(false),
|
||||
filename: "".to_owned(),
|
||||
width: w as _,
|
||||
height: h as _,
|
||||
format: scrap::CodecFormat::VP9,
|
||||
display,
|
||||
tx: None,
|
||||
})
|
||||
.map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r))));
|
||||
@@ -1271,7 +1275,7 @@ impl VideoHandler {
|
||||
}
|
||||
|
||||
// The source of sent password
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum PasswordSource {
|
||||
PersonalAb(Vec<u8>),
|
||||
SharedAb(String),
|
||||
@@ -1317,6 +1321,13 @@ impl PasswordSource {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct ConnToken {
|
||||
password: Vec<u8>,
|
||||
password_source: PasswordSource,
|
||||
session_id: u64,
|
||||
}
|
||||
|
||||
/// Login config handler for [`Client`].
|
||||
#[derive(Default)]
|
||||
pub struct LoginConfigHandler {
|
||||
@@ -1347,6 +1358,7 @@ pub struct LoginConfigHandler {
|
||||
password_source: PasswordSource, // where the sent password comes from
|
||||
shared_password: Option<String>, // Store the shared password
|
||||
pub enable_trusted_devices: bool,
|
||||
pub record: bool,
|
||||
}
|
||||
|
||||
impl Deref for LoginConfigHandler {
|
||||
@@ -1372,6 +1384,7 @@ impl LoginConfigHandler {
|
||||
mut force_relay: bool,
|
||||
adapter_luid: Option<i64>,
|
||||
shared_password: Option<String>,
|
||||
conn_token: Option<String>,
|
||||
) {
|
||||
let mut id = id;
|
||||
if id.contains("@") {
|
||||
@@ -1415,10 +1428,22 @@ impl LoginConfigHandler {
|
||||
let config = self.load_config();
|
||||
self.remember = !config.password.is_empty();
|
||||
self.config = config;
|
||||
let mut sid = rand::random();
|
||||
|
||||
let conn_token = conn_token
|
||||
.map(|x| serde_json::from_str::<ConnToken>(&x).ok())
|
||||
.flatten();
|
||||
let mut sid = 0;
|
||||
if let Some(token) = conn_token {
|
||||
sid = token.session_id;
|
||||
self.password = token.password; // use as last password
|
||||
self.password_source = token.password_source;
|
||||
}
|
||||
if sid == 0 {
|
||||
// you won the lottery
|
||||
sid = 1;
|
||||
sid = rand::random();
|
||||
if sid == 0 {
|
||||
// you won the lottery
|
||||
sid = 1;
|
||||
}
|
||||
}
|
||||
self.session_id = sid;
|
||||
self.supported_encoding = Default::default();
|
||||
@@ -1438,6 +1463,7 @@ impl LoginConfigHandler {
|
||||
self.adapter_luid = adapter_luid;
|
||||
self.selected_windows_session_id = None;
|
||||
self.shared_password = shared_password;
|
||||
self.record = LocalConfig::get_bool_option(OPTION_ALLOW_AUTO_RECORD_OUTGOING);
|
||||
}
|
||||
|
||||
/// Check if the client should auto login.
|
||||
@@ -2218,6 +2244,18 @@ impl LoginConfigHandler {
|
||||
msg_out.set_misc(misc);
|
||||
msg_out
|
||||
}
|
||||
|
||||
pub fn get_conn_token(&self) -> Option<String> {
|
||||
if self.password.is_empty() {
|
||||
return None;
|
||||
}
|
||||
serde_json::to_string(&ConnToken {
|
||||
password: self.password.clone(),
|
||||
password_source: self.password_source.clone(),
|
||||
session_id: self.session_id,
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Media data.
|
||||
@@ -2227,7 +2265,7 @@ pub enum MediaData {
|
||||
AudioFrame(Box<AudioFrame>),
|
||||
AudioFormat(AudioFormat),
|
||||
Reset(Option<usize>),
|
||||
RecordScreen(bool, usize, i32, i32, String),
|
||||
RecordScreen(bool),
|
||||
}
|
||||
|
||||
pub type MediaSender = mpsc::Sender<MediaData>;
|
||||
@@ -2303,10 +2341,16 @@ where
|
||||
let start = std::time::Instant::now();
|
||||
let format = CodecFormat::from(&vf);
|
||||
if !handler_controller_map.contains_key(&display) {
|
||||
let mut handler = VideoHandler::new(format, display);
|
||||
let record = session.lc.read().unwrap().record;
|
||||
let id = session.lc.read().unwrap().id.clone();
|
||||
if record {
|
||||
handler.record_screen(record, id, display);
|
||||
}
|
||||
handler_controller_map.insert(
|
||||
display,
|
||||
VideoHandlerController {
|
||||
handler: VideoHandler::new(format, display),
|
||||
handler,
|
||||
skip_beginning: 0,
|
||||
},
|
||||
);
|
||||
@@ -2325,7 +2369,7 @@ where
|
||||
video_callback(
|
||||
display,
|
||||
&mut handler_controller.handler.rgb,
|
||||
handler_controller.handler.texture,
|
||||
handler_controller.handler.texture.texture,
|
||||
pixelbuffer,
|
||||
);
|
||||
|
||||
@@ -2399,18 +2443,19 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
MediaData::RecordScreen(start, display, w, h, id) => {
|
||||
log::info!("record screen command: start: {start}, display: {display}");
|
||||
// Compatible with the sciter version(single ui session).
|
||||
// For the sciter version, there're no multi-ui-sessions for one connection.
|
||||
// The display is always 0, video_handler_controllers.len() is always 1. So we use the first video handler.
|
||||
if let Some(handler_controler) = handler_controller_map.get_mut(&display) {
|
||||
handler_controler.handler.record_screen(start, w, h, id);
|
||||
} else if handler_controller_map.len() == 1 {
|
||||
if let Some(handler_controler) =
|
||||
handler_controller_map.values_mut().next()
|
||||
{
|
||||
handler_controler.handler.record_screen(start, w, h, id);
|
||||
MediaData::RecordScreen(start) => {
|
||||
log::info!("record screen command: start: {start}");
|
||||
let record = session.lc.read().unwrap().record;
|
||||
session.update_record_status(start);
|
||||
if record != start {
|
||||
session.lc.write().unwrap().record = start;
|
||||
let id = session.lc.read().unwrap().id.clone();
|
||||
for (display, handler_controler) in handler_controller_map.iter_mut() {
|
||||
handler_controler.handler.record_screen(
|
||||
start,
|
||||
id.clone(),
|
||||
*display,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3169,7 +3214,7 @@ pub enum Data {
|
||||
SetConfirmOverrideFile((i32, i32, bool, bool, bool)),
|
||||
AddJob((i32, String, String, i32, bool, bool)),
|
||||
ResumeJob((i32, bool)),
|
||||
RecordScreen(bool, usize, i32, i32, String),
|
||||
RecordScreen(bool),
|
||||
ElevateDirect,
|
||||
ElevateWithLogon(String, String),
|
||||
NewVoiceCall,
|
||||
|
||||
@@ -837,10 +837,8 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
self.handle_job_status(id, -1, err);
|
||||
}
|
||||
}
|
||||
Data::RecordScreen(start, display, w, h, id) => {
|
||||
let _ = self
|
||||
.video_sender
|
||||
.send(MediaData::RecordScreen(start, display, w, h, id));
|
||||
Data::RecordScreen(start) => {
|
||||
let _ = self.video_sender.send(MediaData::RecordScreen(start));
|
||||
}
|
||||
Data::ElevateDirect => {
|
||||
let mut request = ElevationRequest::new();
|
||||
@@ -1218,7 +1216,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
crate::plugin::handle_listen_event(
|
||||
crate::plugin::EVENT_ON_CONN_CLIENT.to_owned(),
|
||||
self.handler.get_id(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if self.handler.is_file_transfer() {
|
||||
|
||||
@@ -828,7 +828,16 @@ async fn check_software_update_() -> hbb_common::ResultType<()> {
|
||||
let response_url = latest_release_response.url().to_string();
|
||||
|
||||
if get_version_number(&latest_release_version) > get_version_number(crate::VERSION) {
|
||||
*SOFTWARE_UPDATE_URL.lock().unwrap() = response_url;
|
||||
#[cfg(feature = "flutter")]
|
||||
{
|
||||
let mut m = HashMap::new();
|
||||
m.insert("name", "check_software_update_finish");
|
||||
m.insert("url", &response_url);
|
||||
if let Ok(data) = serde_json::to_string(&m) {
|
||||
let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data);
|
||||
}
|
||||
}
|
||||
*SOFTWARE_UPDATE_URL.lock().unwrap() = response_url;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::CString,
|
||||
os::raw::{c_char, c_int, c_void},
|
||||
str::FromStr,
|
||||
@@ -1010,6 +1010,10 @@ impl InvokeUiSession for FlutterHandler {
|
||||
rgba_data.valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_record_status(&self, start: bool) {
|
||||
self.push_event("record_status", &[("start", &start.to_string())], &[]);
|
||||
}
|
||||
}
|
||||
|
||||
impl FlutterHandler {
|
||||
@@ -1122,6 +1126,7 @@ pub fn session_add(
|
||||
force_relay: bool,
|
||||
password: String,
|
||||
is_shared_password: bool,
|
||||
conn_token: Option<String>,
|
||||
) -> ResultType<FlutterSession> {
|
||||
let conn_type = if is_file_transfer {
|
||||
ConnType::FILE_TRANSFER
|
||||
@@ -1176,6 +1181,7 @@ pub fn session_add(
|
||||
force_relay,
|
||||
get_adapter_luid(),
|
||||
shared_password,
|
||||
conn_token,
|
||||
);
|
||||
|
||||
let session = Arc::new(session.clone());
|
||||
@@ -1830,7 +1836,6 @@ pub(super) fn session_update_virtual_display(session: &FlutterSession, index: i3
|
||||
|
||||
// sessions mod is used to avoid the big lock of sessions' map.
|
||||
pub mod sessions {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ fn initialize(app_dir: &str, custom_client_config: &str) {
|
||||
scrap::mediacodec::check_mediacodec();
|
||||
crate::common::test_rendezvous_server();
|
||||
crate::common::test_nat_type();
|
||||
crate::common::check_software_update();
|
||||
}
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
@@ -122,6 +121,7 @@ pub fn session_add_sync(
|
||||
force_relay: bool,
|
||||
password: String,
|
||||
is_shared_password: bool,
|
||||
conn_token: Option<String>,
|
||||
) -> SyncReturn<String> {
|
||||
if let Err(e) = session_add(
|
||||
&session_id,
|
||||
@@ -133,6 +133,7 @@ pub fn session_add_sync(
|
||||
force_relay,
|
||||
password,
|
||||
is_shared_password,
|
||||
conn_token,
|
||||
) {
|
||||
SyncReturn(format!("Failed to add session with id {}, {}", &id, e))
|
||||
} else {
|
||||
@@ -223,6 +224,10 @@ pub fn session_get_enable_trusted_devices(session_id: SessionID) -> SyncReturn<b
|
||||
|
||||
pub fn session_close(session_id: SessionID) {
|
||||
if let Some(session) = sessions::remove_session_by_session_id(&session_id) {
|
||||
// `release_remote_keys` is not required for mobile platforms in common cases.
|
||||
// But we still call it to make the code more stable.
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
crate::keyboard::release_remote_keys("map");
|
||||
session.close_event_stream(session_id);
|
||||
session.close();
|
||||
}
|
||||
@@ -242,21 +247,17 @@ pub fn session_is_multi_ui_session(session_id: SessionID) -> SyncReturn<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_record_screen(
|
||||
session_id: SessionID,
|
||||
start: bool,
|
||||
display: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
) {
|
||||
pub fn session_record_screen(session_id: SessionID, start: bool) {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
session.record_screen(start, display as _, width as _, height as _);
|
||||
session.record_screen(start);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_record_status(session_id: SessionID, status: bool) {
|
||||
pub fn session_get_is_recording(session_id: SessionID) -> SyncReturn<bool> {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
session.record_status(status);
|
||||
SyncReturn(session.is_recording())
|
||||
} else {
|
||||
SyncReturn(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,6 +604,7 @@ pub fn session_send_files(
|
||||
file_num: i32,
|
||||
include_hidden: bool,
|
||||
is_remote: bool,
|
||||
_is_dir: bool,
|
||||
) {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
session.send_files(act_id, path, to, file_num, include_hidden, is_remote);
|
||||
@@ -634,7 +636,7 @@ pub fn session_remove_file(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_read_dir_recursive(
|
||||
pub fn session_read_dir_to_remove_recursive(
|
||||
session_id: SessionID,
|
||||
act_id: i32,
|
||||
path: String,
|
||||
@@ -1341,6 +1343,14 @@ pub fn session_close_voice_call(session_id: SessionID) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_get_conn_token(session_id: SessionID) -> SyncReturn<Option<String>> {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
SyncReturn(session.get_conn_token())
|
||||
} else {
|
||||
SyncReturn(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) {
|
||||
crate::ui_cm_interface::handle_incoming_voice_call(id, accept);
|
||||
}
|
||||
@@ -1376,11 +1386,10 @@ pub fn main_get_last_remote_id() -> String {
|
||||
LocalConfig::get_remote_id()
|
||||
}
|
||||
|
||||
pub fn main_get_software_update_url() -> String {
|
||||
pub fn main_get_software_update_url() {
|
||||
if get_local_option("enable-check-update".to_string()) != "N" {
|
||||
crate::common::check_software_update();
|
||||
}
|
||||
crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn main_get_home_dir() -> String {
|
||||
|
||||
@@ -121,6 +121,7 @@ pub struct ClipboardNonFile {
|
||||
pub height: i32,
|
||||
// message.proto: ClipboardFormat
|
||||
pub format: i32,
|
||||
pub special_name: String,
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -889,7 +890,7 @@ pub async fn set_data(data: &Data) -> ResultType<()> {
|
||||
set_data_async(data).await
|
||||
}
|
||||
|
||||
pub async fn set_data_async(data: &Data) -> ResultType<()> {
|
||||
async fn set_data_async(data: &Data) -> ResultType<()> {
|
||||
let mut c = connect(1000, "").await?;
|
||||
c.send(data).await?;
|
||||
Ok(())
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::flutter;
|
||||
use crate::platform::windows::{get_char_from_vk, get_unicode_from_vk};
|
||||
#[cfg(not(any(feature = "flutter", feature = "cli")))]
|
||||
use crate::ui::CUR_SESSION;
|
||||
use crate::ui_session_interface::{InvokeUiSession, Session};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::{client::get_key_state, common::GrabState};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -105,16 +106,31 @@ pub mod client {
|
||||
|
||||
pub fn process_event(keyboard_mode: &str, event: &Event, lock_modes: Option<i32>) {
|
||||
let keyboard_mode = get_keyboard_mode_enum(keyboard_mode);
|
||||
|
||||
if is_long_press(&event) {
|
||||
return;
|
||||
}
|
||||
|
||||
for key_event in event_to_key_events(&event, keyboard_mode, lock_modes) {
|
||||
let peer = get_peer_platform().to_lowercase();
|
||||
for key_event in event_to_key_events(peer, &event, keyboard_mode, lock_modes) {
|
||||
send_key_event(&key_event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_event_with_session<T: InvokeUiSession>(
|
||||
keyboard_mode: &str,
|
||||
event: &Event,
|
||||
lock_modes: Option<i32>,
|
||||
session: &Session<T>,
|
||||
) {
|
||||
let keyboard_mode = get_keyboard_mode_enum(keyboard_mode);
|
||||
if is_long_press(&event) {
|
||||
return;
|
||||
}
|
||||
let peer = session.peer_platform().to_lowercase();
|
||||
for key_event in event_to_key_events(peer, &event, keyboard_mode, lock_modes) {
|
||||
session.send_key_event(&key_event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_modifiers_state(
|
||||
alt: bool,
|
||||
ctrl: bool,
|
||||
@@ -171,6 +187,7 @@ pub mod client {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn map_key_to_control_key(key: &rdev::Key) -> Option<ControlKey> {
|
||||
match key {
|
||||
Key::Alt => Some(ControlKey::Alt),
|
||||
@@ -358,7 +375,6 @@ pub fn is_long_press(event: &Event) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn release_remote_keys(keyboard_mode: &str) {
|
||||
// todo!: client quit suddenly, how to release keys?
|
||||
let to_release = TO_RELEASE.lock().unwrap().clone();
|
||||
@@ -387,7 +403,6 @@ pub fn get_keyboard_mode_enum(keyboard_mode: &str) -> KeyboardMode {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
pub fn is_modifier(key: &rdev::Key) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
@@ -403,7 +418,6 @@ pub fn is_modifier(key: &rdev::Key) -> bool {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn is_numpad_rdev_key(key: &rdev::Key) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
@@ -426,7 +440,6 @@ pub fn is_numpad_rdev_key(key: &rdev::Key) -> bool {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn is_letter_rdev_key(key: &rdev::Key) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
@@ -462,7 +475,6 @@ pub fn is_letter_rdev_key(key: &rdev::Key) -> bool {
|
||||
// https://github.com/rustdesk/rustdesk/issues/8599
|
||||
// We just add these keys as letter keys.
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn is_letter_rdev_key_ex(key: &rdev::Key) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
@@ -471,7 +483,6 @@ pub fn is_letter_rdev_key_ex(key: &rdev::Key) -> bool {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn is_numpad_key(event: &Event) -> bool {
|
||||
matches!(event.event_type, EventType::KeyPress(key) | EventType::KeyRelease(key) if is_numpad_rdev_key(&key))
|
||||
}
|
||||
@@ -479,12 +490,10 @@ fn is_numpad_key(event: &Event) -> bool {
|
||||
// Check is letter key for lock modes.
|
||||
// Only letter keys need to check and send Lock key state.
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn is_letter_key_4_lock_modes(event: &Event) -> bool {
|
||||
matches!(event.event_type, EventType::KeyPress(key) | EventType::KeyRelease(key) if (is_letter_rdev_key(&key) || is_letter_rdev_key_ex(&key)))
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn parse_add_lock_modes_modifiers(
|
||||
key_event: &mut KeyEvent,
|
||||
lock_modes: i32,
|
||||
@@ -555,10 +564,13 @@ fn update_modifiers_state(event: &Event) {
|
||||
}
|
||||
|
||||
pub fn event_to_key_events(
|
||||
mut peer: String,
|
||||
event: &Event,
|
||||
keyboard_mode: KeyboardMode,
|
||||
_lock_modes: Option<i32>,
|
||||
) -> Vec<KeyEvent> {
|
||||
peer.retain(|c| !c.is_whitespace());
|
||||
|
||||
let mut key_event = KeyEvent::new();
|
||||
update_modifiers_state(event);
|
||||
|
||||
@@ -572,16 +584,9 @@ pub fn event_to_key_events(
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut peer = get_peer_platform().to_lowercase();
|
||||
peer.retain(|c| !c.is_whitespace());
|
||||
|
||||
key_event.mode = keyboard_mode.into();
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
let mut key_events;
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
let key_events;
|
||||
key_events = match keyboard_mode {
|
||||
let mut key_events = match keyboard_mode {
|
||||
KeyboardMode::Map => map_keyboard_mode(peer.as_str(), event, key_event),
|
||||
KeyboardMode::Translate => translate_keyboard_mode(peer.as_str(), event, key_event),
|
||||
_ => {
|
||||
@@ -596,15 +601,14 @@ pub fn event_to_key_events(
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
let is_numpad_key = is_numpad_key(&event);
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if keyboard_mode != KeyboardMode::Translate || is_numpad_key {
|
||||
let is_letter_key = is_letter_key_4_lock_modes(&event);
|
||||
for key_event in &mut key_events {
|
||||
if let Some(lock_modes) = _lock_modes {
|
||||
parse_add_lock_modes_modifiers(key_event, lock_modes, is_numpad_key, is_letter_key);
|
||||
} else {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
add_lock_modes_modifiers(key_event, is_numpad_key, is_letter_key);
|
||||
}
|
||||
}
|
||||
@@ -617,6 +621,7 @@ pub fn send_key_event(key_event: &KeyEvent) {
|
||||
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
|
||||
session.send_key_event(key_event);
|
||||
}
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
if let Some(session) = flutter::get_cur_session() {
|
||||
session.send_key_event(key_event);
|
||||
@@ -936,8 +941,19 @@ fn _map_keyboard_mode(_peer: &str, event: &Event, mut key_event: KeyEvent) -> Op
|
||||
_ => event.position_code as _,
|
||||
};
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
let keycode = 0;
|
||||
|
||||
let keycode = match _peer {
|
||||
OS_LOWER_WINDOWS => rdev::usb_hid_code_to_win_scancode(event.usb_hid as _)?,
|
||||
OS_LOWER_LINUX => rdev::usb_hid_code_to_linux_code(event.usb_hid as _)?,
|
||||
OS_LOWER_MACOS => {
|
||||
if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" {
|
||||
rdev::usb_hid_code_to_macos_iso_code(event.usb_hid as _)?
|
||||
} else {
|
||||
rdev::usb_hid_code_to_macos_code(event.usb_hid as _)?
|
||||
}
|
||||
}
|
||||
OS_LOWER_ANDROID => rdev::usb_hid_code_to_android_key_code(event.usb_hid as _)?,
|
||||
_ => event.usb_hid as _,
|
||||
};
|
||||
key_event.set_chr(keycode as _);
|
||||
Some(key_event)
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "عرض مراقب الجودة"),
|
||||
("Disable clipboard", "تعطيل الحافظة"),
|
||||
("Lock after session end", "القفل بعد نهاية هذه الجلسة"),
|
||||
("Insert", "ادخال"),
|
||||
("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del دخال"),
|
||||
("Insert Lock", "قفل الادخال"),
|
||||
("Refresh", "تحديث"),
|
||||
("ID does not exist", "المعرف غير موجود"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "التسجيل"),
|
||||
("Directory", "المسار"),
|
||||
("Automatically record incoming sessions", "تسجيل الجلسات القادمة تلقائيا"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "تغيير"),
|
||||
("Start session recording", "بدء تسجيل الجلسة"),
|
||||
("Stop session recording", "ايقاف تسجيل الجلسة"),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "Паказваць манітор якасці"),
|
||||
("Disable clipboard", "Адключыць буфер абмену"),
|
||||
("Lock after session end", "Заблакаваць уліковы запіс пасля сеансу"),
|
||||
("Insert", "Уставіць"),
|
||||
("Insert Ctrl + Alt + Del", "Уставіць Ctrl + Alt + Del"),
|
||||
("Insert Lock", "Заблакаваць уліковы запіс"),
|
||||
("Refresh", "Абнавіць"),
|
||||
("ID does not exist", "ID не існуе"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "Запіс"),
|
||||
("Directory", "Тэчка"),
|
||||
("Automatically record incoming sessions", "Аўтаматычна запісваць уваходныя сесіі"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "Змяніць"),
|
||||
("Start session recording", "Пачаць запіс сесіі"),
|
||||
("Stop session recording", "Спыніць запіс сесіі"),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "Показвай прозорец за качество"),
|
||||
("Disable clipboard", "Забрана за достъп до клипборд"),
|
||||
("Lock after session end", "Заключване след край на ползване"),
|
||||
("Insert", "Поставяне"),
|
||||
("Insert Ctrl + Alt + Del", "Поставяне Ctrl + Alt + Del"),
|
||||
("Insert Lock", "Заявка за заключване"),
|
||||
("Refresh", "Обновяване"),
|
||||
("ID does not exist", "Несъществуващ определител (ID)"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "Записване"),
|
||||
("Directory", "Директория"),
|
||||
("Automatically record incoming sessions", "Автоматичен запис на входящи сесии"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "Промяна"),
|
||||
("Start session recording", "Започванена запис"),
|
||||
("Stop session recording", "Край на запис"),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
932
src/lang/ca.rs
932
src/lang/ca.rs
File diff suppressed because it is too large
Load Diff
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "显示质量监测"),
|
||||
("Disable clipboard", "禁用剪贴板"),
|
||||
("Lock after session end", "会话结束后锁定远程电脑"),
|
||||
("Insert", "插入"),
|
||||
("Insert Ctrl + Alt + Del", "插入 Ctrl + Alt + Del"),
|
||||
("Insert Lock", "锁定远程电脑"),
|
||||
("Refresh", "刷新画面"),
|
||||
("ID does not exist", "ID 不存在"),
|
||||
@@ -310,7 +310,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Start the screen sharing service on boot, requires special permissions", "开机自动启动屏幕共享服务,此功能需要一些特殊权限。"),
|
||||
("Connection not allowed", "对方不允许连接"),
|
||||
("Legacy mode", "传统模式"),
|
||||
("Map mode", "1:1 传输"),
|
||||
("Map mode", "1:1 传输"),
|
||||
("Translate mode", "翻译模式"),
|
||||
("Use permanent password", "使用固定密码"),
|
||||
("Use both passwords", "同时使用两种密码"),
|
||||
@@ -363,7 +363,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Unpin Toolbar", "取消固定工具栏"),
|
||||
("Recording", "录屏"),
|
||||
("Directory", "目录"),
|
||||
("Automatically record incoming sessions", "自动录制来访会话"),
|
||||
("Automatically record incoming sessions", "自动录制传入会话"),
|
||||
("Automatically record outgoing sessions", "自动录制传出会话"),
|
||||
("Change", "更改"),
|
||||
("Start session recording", "开始录屏"),
|
||||
("Stop session recording", "结束录屏"),
|
||||
@@ -563,7 +564,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Plug out all", "拔出所有"),
|
||||
("True color (4:4:4)", "真彩模式(4:4:4)"),
|
||||
("Enable blocking user input", "允许阻止用户输入"),
|
||||
("id_input_tip", "可以输入 ID、直连 IP,或域名和端口号(<域名>:<端口号>)。\n要访问另一台服务器上的设备,请附加服务器地址(<ID>@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"<ID>@public\", 无需密钥。\n\n如果您想要在首次连接时,强制走中继连接,请在 ID 的后面添加 \"/r\",例如,\"9123456234/r\"。"),
|
||||
("id_input_tip", "可以输入 ID、直连 IP,或域名和端口号(<域名>:<端口号>)。\n要访问另一台服务器上的设备,请附加服务器地址(<ID>@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"<ID>@public\",无需密钥。\n\n如果您想要在首次连接时,强制走中继连接,请在 ID 的后面添加 \"/r\",例如,\"9123456234/r\"。"),
|
||||
("privacy_mode_impl_mag_tip", "模式 1"),
|
||||
("privacy_mode_impl_virtual_display_tip", "模式 2"),
|
||||
("Enter privacy mode", "进入隐私模式"),
|
||||
@@ -644,8 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Parent directory", "父目录"),
|
||||
("Resume", "继续"),
|
||||
("Invalid file name", "无效文件名"),
|
||||
("one-way-file-transfer-tip", "被控端启用了单项文件传输"),
|
||||
("one-way-file-transfer-tip", "被控端启用了单向文件传输"),
|
||||
("Authentication Required", "需要身份验证"),
|
||||
("Authenticate", "认证"),
|
||||
("web_id_input_tip", "可以输入同一个服务器内的 ID,web 客户端不支持直接 IP 访问。\n要访问另一台服务器上的设备,请附加服务器地址(<ID>@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"<ID>@public\",无需密钥。"),
|
||||
("Download", "下载"),
|
||||
("Upload folder", "上传文件夹"),
|
||||
("Upload files", "上传文件"),
|
||||
("Clipboard is synchronized", "剪贴板已同步"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "Zobrazit monitor kvality"),
|
||||
("Disable clipboard", "Vypnout schránku"),
|
||||
("Lock after session end", "Po ukončení relace zamknout plochu"),
|
||||
("Insert", "Vložit"),
|
||||
("Insert Ctrl + Alt + Del", "Vložit Ctrl + Alt + Del"),
|
||||
("Insert Lock", "Zamknout"),
|
||||
("Refresh", "Načíst znovu"),
|
||||
("ID does not exist", "Toto ID neexistuje"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "Nahrávání"),
|
||||
("Directory", "Adresář"),
|
||||
("Automatically record incoming sessions", "Automaticky nahrávat příchozí relace"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "Změnit"),
|
||||
("Start session recording", "Spustit záznam relace"),
|
||||
("Stop session recording", "Zastavit záznam relace"),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "Vis billedkvalitet"),
|
||||
("Disable clipboard", "Deaktiver udklipsholder"),
|
||||
("Lock after session end", "Lås efter afslutningen af fjernstyring"),
|
||||
("Insert", "Indsæt"),
|
||||
("Insert Ctrl + Alt + Del", "Indsæt Ctrl + Alt + Del"),
|
||||
("Insert Lock", "Indsæt lås"),
|
||||
("Refresh", "Genopfrisk"),
|
||||
("ID does not exist", "ID findes ikke"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "Optager"),
|
||||
("Directory", "Mappe"),
|
||||
("Automatically record incoming sessions", "Optag automatisk indgående sessioner"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "Ændr"),
|
||||
("Start session recording", "Start sessionsoptagelse"),
|
||||
("Stop session recording", "Stop sessionsoptagelse"),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "Qualitätsüberwachung anzeigen"),
|
||||
("Disable clipboard", "Zwischenablage deaktivieren"),
|
||||
("Lock after session end", "Nach Sitzungsende sperren"),
|
||||
("Insert", "Einfügen"),
|
||||
("Insert Ctrl + Alt + Del", "Strg + Alt + Entf senden"),
|
||||
("Insert Lock", "Win+L (Sperren) senden"),
|
||||
("Refresh", "Aktualisieren"),
|
||||
("ID does not exist", "Diese ID existiert nicht."),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "Aufnahme"),
|
||||
("Directory", "Verzeichnis"),
|
||||
("Automatically record incoming sessions", "Eingehende Sitzungen automatisch aufzeichnen"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "Ändern"),
|
||||
("Start session recording", "Sitzungsaufzeichnung starten"),
|
||||
("Stop session recording", "Sitzungsaufzeichnung beenden"),
|
||||
@@ -563,7 +564,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Plug out all", "Alle ausschalten"),
|
||||
("True color (4:4:4)", "True Color (4:4:4)"),
|
||||
("Enable blocking user input", "Blockieren von Benutzereingaben aktivieren"),
|
||||
("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (<domain>:<port>) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen möchten, fügen Sie bitte die Serveradresse (<id>@<server_address>?key=<key_value>) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"<id>@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."),
|
||||
("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (<domain>:<port>) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (<id>@<server_address>?key=<key_value>) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"<id>@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."),
|
||||
("privacy_mode_impl_mag_tip", "Modus 1"),
|
||||
("privacy_mode_impl_virtual_display_tip", "Modus 2"),
|
||||
("Enter privacy mode", "Datenschutzmodus aktivieren"),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", "Die einseitige Dateiübertragung ist auf der kontrollierten Seite aktiviert."),
|
||||
("Authentication Required", "Authentifizierung erforderlich"),
|
||||
("Authenticate", "Authentifizieren"),
|
||||
("web_id_input_tip", "Sie können eine ID auf demselben Server eingeben, direkter IP-Zugriff wird im Web-Client nicht unterstützt.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (<id>@<server_address>?key=<key_value>) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"<id>@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt."),
|
||||
("Download", "Herunterladen"),
|
||||
("Upload folder", "Ordner hochladen"),
|
||||
("Upload files", "Dateien hochladen"),
|
||||
("Clipboard is synchronized", "Zwischenablage ist synchronisiert"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "Εμφάνιση παρακολούθησης ποιότητας σύνδεσης"),
|
||||
("Disable clipboard", "Απενεργοποίηση προχείρου"),
|
||||
("Lock after session end", "Κλείδωμα μετά το τέλος της συνεδρίας"),
|
||||
("Insert", "Εισαγωγή"),
|
||||
("Insert Ctrl + Alt + Del", "Εισαγωγή Ctrl + Alt + Del"),
|
||||
("Insert Lock", "Κλείδωμα απομακρυσμένου σταθμού"),
|
||||
("Refresh", "Ανανέωση"),
|
||||
("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "Εγγραφή"),
|
||||
("Directory", "Φάκελος εγγραφών"),
|
||||
("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "Αλλαγή"),
|
||||
("Start session recording", "Έναρξη εγγραφής συνεδρίας"),
|
||||
("Stop session recording", "Διακοπή εγγραφής συνεδρίας"),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -235,5 +235,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("network_error_tip", "Please check your network connection, then click retry."),
|
||||
("enable-trusted-devices-tip", "Skip 2FA verification on trusted devices"),
|
||||
("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."),
|
||||
("web_id_input_tip", "You can input an ID in the same server, direct IP access is not supported in web client.\nIf you want to access a device on another server, please append the server address (<id>@<server_address>?key=<key_value>), for example,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nIf you want to access a device on a public server, please input \"<id>@public\", the key is not needed for public server."),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
248
src/lang/eo.rs
248
src/lang/eo.rs
@@ -6,11 +6,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("desk_tip", "Via aparato povas esti alirita kun tiu identigilo kaj pasvorto"),
|
||||
("Password", "Pasvorto"),
|
||||
("Ready", "Preta"),
|
||||
("Established", ""),
|
||||
("Established", "Establis"),
|
||||
("connecting_status", "Konektante al la reto RustDesk..."),
|
||||
("Enable service", "Ebligi servon"),
|
||||
("Start service", "Starti servon"),
|
||||
("Service is running", ""),
|
||||
("Service is running", "La servo funkcias"),
|
||||
("Service is not running", "La servo ne funkcias"),
|
||||
("not_ready_status", "Ne preta, bonvolu kontroli la retkonekto"),
|
||||
("Control Remote Desktop", "Kontroli foran aparaton"),
|
||||
@@ -29,33 +29,33 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Enable TCP tunneling", "Ebligi tunelado TCP"),
|
||||
("IP Whitelisting", "Listo de IP akceptataj"),
|
||||
("ID/Relay Server", "Identigila/Relajsa servilo"),
|
||||
("Import server config", "Enporti servilan agordon"),
|
||||
("Export Server Config", ""),
|
||||
("Import server config", "Importi servilan agordon"),
|
||||
("Export Server Config", "Eksporti servilan agordon"),
|
||||
("Import server configuration successfully", "Importi servilan agordon sukcese"),
|
||||
("Export server configuration successfully", ""),
|
||||
("Export server configuration successfully", "Eksporti servilan agordon sukcese"),
|
||||
("Invalid server configuration", "Nevalida servila agordo"),
|
||||
("Clipboard is empty", "La poŝo estas malplena"),
|
||||
("Stop service", "Haltu servon"),
|
||||
("Change ID", "Ŝanĝi identigilon"),
|
||||
("Your new ID", ""),
|
||||
("length %min% to %max%", ""),
|
||||
("starts with a letter", ""),
|
||||
("allowed characters", ""),
|
||||
("Your new ID", "Via nova identigilo"),
|
||||
("length %min% to %max%", "longeco %min% al %max%"),
|
||||
("starts with a letter", "komencas kun letero"),
|
||||
("allowed characters", "permesitaj signoj"),
|
||||
("id_change_tip", "Nur la signoj a-z, A-Z, 0-9, _ (substreko) povas esti uzataj. La unua litero povas esti inter a-z, A-Z. La longeco devas esti inter 6 kaj 16."),
|
||||
("Website", "Retejo"),
|
||||
("About", "Pri"),
|
||||
("Slogan_tip", ""),
|
||||
("Privacy Statement", ""),
|
||||
("Slogan_tip", "Farita kun koro en ĉi tiu ĥaosa mondo!"),
|
||||
("Privacy Statement", "Deklaro Pri Privateco"),
|
||||
("Mute", "Muta"),
|
||||
("Build Date", ""),
|
||||
("Version", ""),
|
||||
("Home", ""),
|
||||
("Audio Input", "Aŭdia enigo"),
|
||||
("Enhancements", ""),
|
||||
("Hardware Codec", ""),
|
||||
("Adaptive bitrate", ""),
|
||||
("Build Date", "konstruada dato"),
|
||||
("Version", "Versio"),
|
||||
("Home", "Hejmo"),
|
||||
("Audio Input", "Aŭdia Enigo"),
|
||||
("Enhancements", "Plibonigoj"),
|
||||
("Hardware Codec", "Aparataro Kodeko"),
|
||||
("Adaptive bitrate", "Adapta bitrapido"),
|
||||
("ID Server", "Servilo de identigiloj"),
|
||||
("Relay Server", "Relajsa servilo"),
|
||||
("Relay Server", "Relajsa Servilo"),
|
||||
("API Server", "Servilo de API"),
|
||||
("invalid_http", "Devas komenci kun http:// aŭ https://"),
|
||||
("Invalid IP", "IP nevalida"),
|
||||
@@ -83,35 +83,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Successful", "Sukceso"),
|
||||
("Connected, waiting for image...", "Konektita, atendante bildon..."),
|
||||
("Name", "Nomo"),
|
||||
("Type", ""),
|
||||
("Type", "Tipo"),
|
||||
("Modified", "Modifita"),
|
||||
("Size", "Grandeco"),
|
||||
("Show Hidden Files", "Montri kaŝitajn dosierojn"),
|
||||
("Receive", "Akcepti"),
|
||||
("Send", "Sendi"),
|
||||
("Refresh File", ""),
|
||||
("Local", ""),
|
||||
("Remote", ""),
|
||||
("Refresh File", "Aktualigu Dosieron"),
|
||||
("Local", "Loka"),
|
||||
("Remote", "Fora"),
|
||||
("Remote Computer", "Fora komputilo"),
|
||||
("Local Computer", "Loka komputilo"),
|
||||
("Confirm Delete", "Konfermi la forigo"),
|
||||
("Delete", ""),
|
||||
("Properties", ""),
|
||||
("Multi Select", ""),
|
||||
("Select All", ""),
|
||||
("Unselect All", ""),
|
||||
("Empty Directory", ""),
|
||||
("Not an empty directory", ""),
|
||||
("Are you sure you want to delete this file?", "Ĉu vi vere volas forigi tiun dosieron?"),
|
||||
("Are you sure you want to delete this empty directory?", ""),
|
||||
("Are you sure you want to delete the file of this directory?", ""),
|
||||
("Delete", "Forigi"),
|
||||
("Properties", "Propraĵoj"),
|
||||
("Multi Select", "Pluropa Elekto"),
|
||||
("Select All", "Elektu Ĉiujn"),
|
||||
("Unselect All", "Malelektu Ĉiujn"),
|
||||
("Empty Directory", "Malplena Dosierujo"),
|
||||
("Not an empty directory", "Ne Malplena Dosierujo"),
|
||||
("Are you sure you want to delete this file?", "Ĉu vi certas, ke vi volas forigi ĉi tiun dosieron?"),
|
||||
("Are you sure you want to delete this empty directory?", "Ĉu vi certas, ke vi volas forigi ĉi tiun malplenan dosierujon?"),
|
||||
("Are you sure you want to delete the file of this directory?", "Ĉu vi certa. ke vi volas forigi la dosieron de ĉi tiu dosierujo"),
|
||||
("Do this for all conflicts", "Same por ĉiuj konfliktoj"),
|
||||
("This is irreversible!", ""),
|
||||
("This is irreversible!", "Ĉi tio estas neinversigebla!"),
|
||||
("Deleting", "Forigado"),
|
||||
("files", "dosiero"),
|
||||
("Waiting", "Atendante..."),
|
||||
("Finished", "Finita"),
|
||||
("Speed", ""),
|
||||
("Speed", "Rapideco"),
|
||||
("Custom Image Quality", "Agordi bildan kvaliton"),
|
||||
("Privacy mode", "Modo privata"),
|
||||
("Block user input", "Bloki uzanta enigo"),
|
||||
@@ -127,10 +127,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Optimize reaction time", "Optimigi reakcia tempo"),
|
||||
("Custom", ""),
|
||||
("Show remote cursor", "Montri foran kursoron"),
|
||||
("Show quality monitor", ""),
|
||||
("Show quality monitor", "Montri kvalito monitoron"),
|
||||
("Disable clipboard", "Malebligi poŝon"),
|
||||
("Lock after session end", "Ŝlosi foran komputilon post malkonektado"),
|
||||
("Insert", "Enmeti"),
|
||||
("Insert Ctrl + Alt + Del", "Enmeti Ctrl + Alt + Del"),
|
||||
("Insert Lock", "Ŝlosi foran komputilon"),
|
||||
("Refresh", "Refreŝigi ekranon"),
|
||||
("ID does not exist", "La identigilo ne ekzistas"),
|
||||
@@ -170,8 +170,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Action", "Ago"),
|
||||
("Add", "Aldoni"),
|
||||
("Local Port", "Loka pordo"),
|
||||
("Local Address", ""),
|
||||
("Change Local Port", ""),
|
||||
("Local Address", "Loka Adreso"),
|
||||
("Change Local Port", "Ŝanĝi Loka Pordo"),
|
||||
("setup_server_tip", "Se vi bezonas pli rapida konekcio, vi povas krei vian propran servilon"),
|
||||
("Too short, at least 6 characters.", "Tro mallonga, almenaŭ 6 signoj."),
|
||||
("The confirmation is not identical.", "Ambaŭ enigoj ne kongruas"),
|
||||
@@ -203,23 +203,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Reboot required", "Restarto deviga"),
|
||||
("Unsupported display server", "La aktuala bilda servilo ne estas subtenita"),
|
||||
("x11 expected", "Bonvolu uzi x11"),
|
||||
("Port", ""),
|
||||
("Port", "Pordo"),
|
||||
("Settings", "Agordoj"),
|
||||
("Username", " Uzanta nomo"),
|
||||
("Invalid port", "Pordo nevalida"),
|
||||
("Closed manually by the peer", "Manuale fermita de la samtavolano"),
|
||||
("Enable remote configuration modification", "Permesi foran redaktadon de la konfiguracio"),
|
||||
("Run without install", "Plenumi sen instali"),
|
||||
("Connect via relay", ""),
|
||||
("Connect via relay", "Konekti per relajso"),
|
||||
("Always connect via relay", "Ĉiam konekti per relajso"),
|
||||
("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"),
|
||||
("Login", "Konekti"),
|
||||
("Verify", ""),
|
||||
("Remember me", ""),
|
||||
("Trust this device", ""),
|
||||
("Verification code", ""),
|
||||
("verification_tip", ""),
|
||||
("Logout", "Malkonekti"),
|
||||
("Login", "Ensaluti"),
|
||||
("Verify", "Kontrolis"),
|
||||
("Remember me", "Memori min"),
|
||||
("Trust this device", "Fidu ĉi tiun aparaton"),
|
||||
("Verification code", "Konfirmkodo"),
|
||||
("verification_tip", "Konfirmkodo estis sendita al la registrita retpoŝta adreso, enigu la konfirmkodon por daŭrigi ensaluti."),
|
||||
("Logout", "Elsaluti"),
|
||||
("Tags", "Etikedi"),
|
||||
("Search ID", "Serĉi ID"),
|
||||
("whitelist_sep", "Vi povas uzi komon, punktokomon, spacon aŭ linsalton kiel apartigilo"),
|
||||
@@ -241,86 +241,86 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Socks5 Proxy", "Socks5 prokura servilo"),
|
||||
("Socks5/Http(s) Proxy", "Socks5/Http(s) prokura servilo"),
|
||||
("Discovered", "Malkovritaj"),
|
||||
("install_daemon_tip", ""),
|
||||
("install_daemon_tip", "Por komenci ĉe ekŝargo, oni devas instali sisteman servon."),
|
||||
("Remote ID", "Fora identigilo"),
|
||||
("Paste", "Alglui"),
|
||||
("Paste here?", ""),
|
||||
("Paste here?", "Alglui ĉi tie?"),
|
||||
("Are you sure to close the connection?", "Ĉu vi vere volas fermi la konekton?"),
|
||||
("Download new version", "Elŝuti la novan version"),
|
||||
("Touch mode", "Tuŝa modo"),
|
||||
("Mouse mode", ""),
|
||||
("One-Finger Tap", ""),
|
||||
("Left Mouse", ""),
|
||||
("One-Long Tap", ""),
|
||||
("Two-Finger Tap", ""),
|
||||
("Right Mouse", ""),
|
||||
("One-Finger Move", ""),
|
||||
("Double Tap & Move", ""),
|
||||
("Mouse Drag", ""),
|
||||
("Three-Finger vertically", ""),
|
||||
("Mouse Wheel", ""),
|
||||
("Two-Finger Move", ""),
|
||||
("Canvas Move", ""),
|
||||
("Pinch to Zoom", ""),
|
||||
("Canvas Zoom", ""),
|
||||
("Mouse mode", "musa modo"),
|
||||
("One-Finger Tap", "Unufingra Frapeto"),
|
||||
("Left Mouse", "Maldekstra Muso"),
|
||||
("One-Long Tap", "Unulonga Frapeto"),
|
||||
("Two-Finger Tap", "Dufingra Frapeto"),
|
||||
("Right Mouse", "Deskra Muso"),
|
||||
("One-Finger Move", "Unufingra Movo"),
|
||||
("Double Tap & Move", "Duobla Frapeto & Movo"),
|
||||
("Mouse Drag", "Muso Trenadi"),
|
||||
("Three-Finger vertically", "Tri Figroj Vertikale"),
|
||||
("Mouse Wheel", "Musa Rado"),
|
||||
("Two-Finger Move", "Dufingra Movo"),
|
||||
("Canvas Move", "Kanvasa Movo"),
|
||||
("Pinch to Zoom", "Pinĉi al Zomo"),
|
||||
("Canvas Zoom", "Kanvasa Zomo"),
|
||||
("Reset canvas", "Restarigi kanvaso"),
|
||||
("No permission of file transfer", "Neniu permeso de dosiertransigo"),
|
||||
("Note", "Notu"),
|
||||
("Connection", ""),
|
||||
("Share Screen", ""),
|
||||
("Chat", ""),
|
||||
("Total", ""),
|
||||
("items", ""),
|
||||
("Selected", ""),
|
||||
("Screen Capture", ""),
|
||||
("Input Control", ""),
|
||||
("Audio Capture", ""),
|
||||
("File Connection", ""),
|
||||
("Screen Connection", ""),
|
||||
("Do you accept?", ""),
|
||||
("Open System Setting", ""),
|
||||
("How to get Android input permission?", ""),
|
||||
("android_input_permission_tip1", ""),
|
||||
("android_input_permission_tip2", ""),
|
||||
("android_new_connection_tip", ""),
|
||||
("android_service_will_start_tip", ""),
|
||||
("android_stop_service_tip", ""),
|
||||
("android_version_audio_tip", ""),
|
||||
("android_start_service_tip", ""),
|
||||
("android_permission_may_not_change_tip", ""),
|
||||
("Account", ""),
|
||||
("Overwrite", ""),
|
||||
("This file exists, skip or overwrite this file?", ""),
|
||||
("Quit", ""),
|
||||
("Help", ""),
|
||||
("Failed", ""),
|
||||
("Succeeded", ""),
|
||||
("Someone turns on privacy mode, exit", ""),
|
||||
("Unsupported", ""),
|
||||
("Peer denied", ""),
|
||||
("Please install plugins", ""),
|
||||
("Peer exit", ""),
|
||||
("Failed to turn off", ""),
|
||||
("Turned off", ""),
|
||||
("Language", ""),
|
||||
("Keep RustDesk background service", ""),
|
||||
("Ignore Battery Optimizations", ""),
|
||||
("android_open_battery_optimizations_tip", ""),
|
||||
("Start on boot", ""),
|
||||
("Start the screen sharing service on boot, requires special permissions", ""),
|
||||
("Connection not allowed", ""),
|
||||
("Connection", "Konekto"),
|
||||
("Share Screen", "Kunhavigi Ekranon"),
|
||||
("Chat", "Babilo"),
|
||||
("Total", "Sumo"),
|
||||
("items", "eroj"),
|
||||
("Selected", "Elektita"),
|
||||
("Screen Capture", "Ekrankapto"),
|
||||
("Input Control", "Eniga Kontrolo"),
|
||||
("Audio Capture", "Sonkontrolo"),
|
||||
("File Connection", "Dosiero Konekto"),
|
||||
("Screen Connection", "Ekrono konekto"),
|
||||
("Do you accept?", "Ĉu vi akceptas?"),
|
||||
("Open System Setting", "Malfermi Sistemajn Agordojn"),
|
||||
("How to get Android input permission?", "Kiel akiri Android enigajn permesojn"),
|
||||
("android_input_permission_tip1", "Por ke fora aparato regu vian Android-aparaton per muso aŭ tuŝo, vi devas permesi al RustDesk uzi la servon \"Alirebleco\"."),
|
||||
("android_input_permission_tip2", "Bonvolu iri al la sekva paĝo de sistemaj agordoj, trovi kaj eniri [Instatajn Servojn], ŝalti la servon [RustDesk Enigo]."),
|
||||
("android_new_connection_tip", "Nova kontrolpeto estis ricevita, kiu volas kontroli vian nunan aparaton."),
|
||||
("android_service_will_start_tip", "Ŝalti \"Ekrankapto\" aŭtomate startos la servon, permesante al aliaj aparatoj peti konekton al via aparato."),
|
||||
("android_stop_service_tip", "Fermante la servon aŭtomate fermos ĉiujn establitajn konektojn."),
|
||||
("android_version_audio_tip", "La nuna versio da Android ne subtenas sonkapton, bonvolu ĝisdatigi al Android 10 aŭ pli alta."),
|
||||
("android_start_service_tip", "Frapu [Komenci servo] aŭ ebligu la permeson de [Ekrankapto] por komenci la servon de kundivido de ekrano."),
|
||||
("android_permission_may_not_change_tip", "Permesoj por establitaj konektoj neble estas ŝanĝitaj tuj ĝis rekonektitaj."),
|
||||
("Account", "Konto"),
|
||||
("Overwrite", "anstataŭigi"),
|
||||
("This file exists, skip or overwrite this file?", "Ĉi tiu dosiero ekzistas, ĉu preterlasi aŭ anstataŭi ĉi tiun dosieron?"),
|
||||
("Quit", "Forlasi"),
|
||||
("Help", "Helpi"),
|
||||
("Failed", "Malsukcesa"),
|
||||
("Succeeded", "Sukcesa"),
|
||||
("Someone turns on privacy mode, exit", "Iu ŝaltas modon privata, Eliro"),
|
||||
("Unsupported", "Nesubtenata"),
|
||||
("Peer denied", "Samulo rifuzita"),
|
||||
("Please install plugins", "Bonvolu instali kromprogramojn"),
|
||||
("Peer exit", "Samulo eliras"),
|
||||
("Failed to turn off", "Malsukcesis malŝalti"),
|
||||
("Turned off", "Malŝaltita"),
|
||||
("Language", "Lingvo"),
|
||||
("Keep RustDesk background service", "Tenu RustDesk fonan servon"),
|
||||
("Ignore Battery Optimizations", "Ignoru Bateria Optimumigojn"),
|
||||
("android_open_battery_optimizations_tip", "Se vi volas malŝalti ĉi tiun funkcion, bonvolu iri al la sekva paĝo de agordoj de la aplikaĵo de RustDesk, trovi kaj eniri [Baterio], Malmarku [Senrestrikta]"),
|
||||
("Start on boot", "Komencu ĉe ekfunkciigo"),
|
||||
("Start the screen sharing service on boot, requires special permissions", "Komencu la servon de kundivido de ekrano ĉe lanĉo, postulas specialajn permesojn"),
|
||||
("Connection not allowed", "Konekto ne rajtas"),
|
||||
("Legacy mode", ""),
|
||||
("Map mode", ""),
|
||||
("Translate mode", ""),
|
||||
("Use permanent password", ""),
|
||||
("Use both passwords", ""),
|
||||
("Set permanent password", ""),
|
||||
("Enable remote restart", ""),
|
||||
("Restart remote device", ""),
|
||||
("Are you sure you want to restart", ""),
|
||||
("Restarting remote device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Map mode", "Mapa modo"),
|
||||
("Translate mode", "Traduki modo"),
|
||||
("Use permanent password", "Uzu permanenta pasvorto"),
|
||||
("Use both passwords", "Uzu ambaŭ pasvorto"),
|
||||
("Set permanent password", "Starigi permanenta pasvorto"),
|
||||
("Enable remote restart", "Permesi fora restartas"),
|
||||
("Restart remote device", "Restartu fora aparato"),
|
||||
("Are you sure you want to restart", "Ĉu vi certas, ke vi volas restarti"),
|
||||
("Restarting remote device", "Restartas fora aparato"),
|
||||
("remote_restarting_tip", "Fora aparato restartiĝas, bonvolu fermi ĉi tiun mesaĝkeston kaj rekonekti kun permanenta pasvorto post iom da tempo"),
|
||||
("Copied", "Kopiita"),
|
||||
("Exit Fullscreen", "Eliru Plenekranon"),
|
||||
("Fullscreen", "Plenekrane"),
|
||||
("Mobile Actions", "Poŝtelefonaj Agoj"),
|
||||
@@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Ratio", "Proporcio"),
|
||||
("Image Quality", "Bilda Kvalito"),
|
||||
("Scroll Style", "Ruluma Stilo"),
|
||||
("Show Toolbar", ""),
|
||||
("Hide Toolbar", ""),
|
||||
("Show Toolbar", "Montri Ilobreton"),
|
||||
("Hide Toolbar", "Kaŝi Ilobreton"),
|
||||
("Direct Connection", "Rekta Konekto"),
|
||||
("Relay Connection", "Relajsa Konekto"),
|
||||
("Secure Connection", "Sekura Konekto"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", ""),
|
||||
("Directory", ""),
|
||||
("Automatically record incoming sessions", ""),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", ""),
|
||||
("Start session recording", ""),
|
||||
("Stop session recording", ""),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "Mostrar calidad del monitor"),
|
||||
("Disable clipboard", "Deshabilitar portapapeles"),
|
||||
("Lock after session end", "Bloquear después del final de la sesión"),
|
||||
("Insert", "Insertar"),
|
||||
("Insert Ctrl + Alt + Del", "Insertar Ctrl + Alt + Del"),
|
||||
("Insert Lock", "Insertar bloqueo"),
|
||||
("Refresh", "Actualizar"),
|
||||
("ID does not exist", "La ID no existe"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "Grabando"),
|
||||
("Directory", "Directorio"),
|
||||
("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "Cambiar"),
|
||||
("Start session recording", "Comenzar grabación de sesión"),
|
||||
("Stop session recording", "Detener grabación de sesión"),
|
||||
@@ -644,8 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Parent directory", "Directorio superior"),
|
||||
("Resume", "Continuar"),
|
||||
("Invalid file name", "Nombre de archivo no válido"),
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("one-way-file-transfer-tip", "La transferencia en un sentido está habilitada en el lado controlado."),
|
||||
("Authentication Required", "Se requiere autenticación"),
|
||||
("Authenticate", "Autenticar"),
|
||||
("web_id_input_tip", "Puedes introducir una ID en el mismo servidor, el cliente web no soporta acceso vía IP.\nSi quieres acceder a un dispositivo en otro servidor, por favor, agrega la dirección del servidor (<id>@<direccion_servidor>?clave=<clave_valor>), por ejemplo,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi quieres accedder a un dispositivo en un servidor público, por favor, introduce \"<id>@public\", la clave no es necesaria para el servidor público."),
|
||||
("Download", "Descarga"),
|
||||
("Upload folder", "Subir carpeta"),
|
||||
("Upload files", "Subir archivos"),
|
||||
("Clipboard is synchronized", "Portapapeles sincronizado"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", ""),
|
||||
("Disable clipboard", ""),
|
||||
("Lock after session end", ""),
|
||||
("Insert", ""),
|
||||
("Insert Ctrl + Alt + Del", ""),
|
||||
("Insert Lock", "Sisesta lukk"),
|
||||
("Refresh", ""),
|
||||
("ID does not exist", ""),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", ""),
|
||||
("Directory", ""),
|
||||
("Automatically record incoming sessions", ""),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", ""),
|
||||
("Start session recording", ""),
|
||||
("Stop session recording", ""),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "Erakutsi kalitate monitorea"),
|
||||
("Disable clipboard", "Desgaitu arbela"),
|
||||
("Lock after session end", "Blokeatu sesioa amaitu ostean"),
|
||||
("Insert", "Sartu"),
|
||||
("Insert Ctrl + Alt + Del", "Sartu Ctrl + Alt + Del"),
|
||||
("Insert Lock", "Sarrera-blokeoa"),
|
||||
("Refresh", "Freskatu"),
|
||||
("ID does not exist", "IDa ez da existitzen"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "Grabatzen"),
|
||||
("Directory", "Direktorioa"),
|
||||
("Automatically record incoming sessions", "Automatikoki grabatu sarrerako saioak"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "Aldatu"),
|
||||
("Start session recording", "Hasi saioaren grabaketa"),
|
||||
("Stop session recording", "Gelditu saioaren grabaketa"),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "نمایش کیفیت مانیتور"),
|
||||
("Disable clipboard", " غیرفعالسازی کلیپبورد"),
|
||||
("Lock after session end", "قفل کردن حساب کاربری سیستم عامل پس از پایان جلسه"),
|
||||
("Insert", "افزودن"),
|
||||
("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del افزودن"),
|
||||
("Insert Lock", "قفل کردن سیستم"),
|
||||
("Refresh", "تازه سازی"),
|
||||
("ID does not exist", "شناسه وجود ندارد"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "در حال ضبط"),
|
||||
("Directory", "مسیر"),
|
||||
("Automatically record incoming sessions", "ضبط خودکار جلسات ورودی"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "تغییر"),
|
||||
("Start session recording", "شروع ضبط جلسه"),
|
||||
("Stop session recording", "توقف ضبط جلسه"),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "Afficher le moniteur de qualité"),
|
||||
("Disable clipboard", "Désactiver le presse-papier"),
|
||||
("Lock after session end", "Verrouiller l'appareil distant après la déconnexion"),
|
||||
("Insert", "Envoyer"),
|
||||
("Insert Ctrl + Alt + Del", "Envoyer Ctrl + Alt + Del"),
|
||||
("Insert Lock", "Verrouiller l'appareil distant"),
|
||||
("Refresh", "Rafraîchir l'écran"),
|
||||
("ID does not exist", "L'ID n'existe pas"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "Enregistrement"),
|
||||
("Directory", "Répertoire"),
|
||||
("Automatically record incoming sessions", "Enregistrement automatique des sessions entrantes"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "Modifier"),
|
||||
("Start session recording", "Commencer l'enregistrement"),
|
||||
("Stop session recording", "Stopper l'enregistrement"),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", ""),
|
||||
("Disable clipboard", ""),
|
||||
("Lock after session end", ""),
|
||||
("Insert", ""),
|
||||
("Insert Ctrl + Alt + Del", ""),
|
||||
("Insert Lock", "הוסף נעילה"),
|
||||
("Refresh", ""),
|
||||
("ID does not exist", ""),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", ""),
|
||||
("Directory", ""),
|
||||
("Automatically record incoming sessions", ""),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", ""),
|
||||
("Start session recording", ""),
|
||||
("Stop session recording", ""),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "Prikaži kvalitetu monitora"),
|
||||
("Disable clipboard", "Zabrani međuspremnik"),
|
||||
("Lock after session end", "Zaključaj po završetku sesije"),
|
||||
("Insert", "Umetni"),
|
||||
("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del umetanje"),
|
||||
("Insert Lock", "Zaključaj umetanje"),
|
||||
("Refresh", "Osvježi"),
|
||||
("ID does not exist", "ID ne postoji"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "Snimanje"),
|
||||
("Directory", "Mapa"),
|
||||
("Automatically record incoming sessions", "Automatski snimi dolazne sesije"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "Promijeni"),
|
||||
("Start session recording", "Započni snimanje sesije"),
|
||||
("Stop session recording", "Zaustavi snimanje sesije"),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", ""),
|
||||
("Disable clipboard", "Közös vágólap kikapcsolása"),
|
||||
("Lock after session end", "Távoli fiók zárolása a munkamenet végén"),
|
||||
("Insert", ""),
|
||||
("Insert Ctrl + Alt + Del", "Illessze be a Ctrl + Alt + Del"),
|
||||
("Insert Lock", "Távoli fiók zárolása"),
|
||||
("Refresh", "Frissítés"),
|
||||
("ID does not exist", "Az azonosító nem létezik"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "Felvétel"),
|
||||
("Directory", "Könyvtár"),
|
||||
("Automatically record incoming sessions", "A bejövő munkamenetek automatikus rögzítése"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "Változtatás"),
|
||||
("Start session recording", "Munkamenet rögzítés indítása"),
|
||||
("Stop session recording", "Munkamenet rögzítés leállítása"),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
202
src/lang/id.rs
202
src/lang/id.rs
@@ -2,8 +2,8 @@ lazy_static::lazy_static! {
|
||||
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
[
|
||||
("Status", "Status"),
|
||||
("Your Desktop", "Desktop Anda"),
|
||||
("desk_tip", "Desktop Anda dapat diakses dengan ID dan kata sandi ini."),
|
||||
("Your Desktop", "Layar Utama"),
|
||||
("desk_tip", "Layar kamu dapat diakses dengan ID dan kata sandi ini."),
|
||||
("Password", "Kata sandi"),
|
||||
("Ready", "Sudah siap"),
|
||||
("Established", "Didirikan"),
|
||||
@@ -12,17 +12,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Start service", "Mulai Layanan"),
|
||||
("Service is running", "Layanan berjalan"),
|
||||
("Service is not running", "Layanan tidak berjalan"),
|
||||
("not_ready_status", "Belum siap. Silakan periksa koneksi Anda"),
|
||||
("Control Remote Desktop", "Kontrol Remote Desktop"),
|
||||
("Transfer file", "File Transfer"),
|
||||
("Connect", "Hubungkan"),
|
||||
("not_ready_status", "Belum siap digunakan. Silakan periksa koneksi"),
|
||||
("Control Remote Desktop", "Kontrol PC dari jarak jauh"),
|
||||
("Transfer file", "Transfer File"),
|
||||
("Connect", "Sambungkan"),
|
||||
("Recent sessions", "Sesi Terkini"),
|
||||
("Address book", "Buku Alamat"),
|
||||
("Confirmation", "Konfirmasi"),
|
||||
("TCP tunneling", "Tunneling TCP"),
|
||||
("Remove", "Hapus"),
|
||||
("Refresh random password", "Perbarui kata sandi acak"),
|
||||
("Set your own password", "Tetapkan kata sandi Anda"),
|
||||
("Set your own password", "Tetapkan kata sandi"),
|
||||
("Enable keyboard/mouse", "Aktifkan Keyboard/Mouse"),
|
||||
("Enable clipboard", "Aktifkan Papan Klip"),
|
||||
("Enable file transfer", "Aktifkan Transfer file"),
|
||||
@@ -37,7 +37,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Clipboard is empty", "Papan klip kosong"),
|
||||
("Stop service", "Hentikan Layanan"),
|
||||
("Change ID", "Ubah ID"),
|
||||
("Your new ID", "ID baru anda"),
|
||||
("Your new ID", "ID baru"),
|
||||
("length %min% to %max%", "panjang %min% s/d %max%"),
|
||||
("starts with a letter", "Dimulai dengan huruf"),
|
||||
("allowed characters", "Karakter yang dapat digunakan"),
|
||||
@@ -69,10 +69,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Retry", "Coba lagi"),
|
||||
("OK", "Oke"),
|
||||
("Password Required", "Kata sandi tidak boleh kosong"),
|
||||
("Please enter your password", "Silahkan masukkan kata sandi anda"),
|
||||
("Please enter your password", "Silahkan masukkan kata sandi"),
|
||||
("Remember password", "Ingat kata sandi"),
|
||||
("Wrong Password", "Kata sandi Salah"),
|
||||
("Do you want to enter again?", "Apakah anda ingin masuk lagi?"),
|
||||
("Do you want to enter again?", "Apakah kamu ingin mencoba lagi?"),
|
||||
("Connection Error", "Kesalahan koneksi"),
|
||||
("Error", "Kesalahan"),
|
||||
("Reset by the peer", "Direset oleh rekan"),
|
||||
@@ -102,9 +102,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Unselect All", "Batalkan Pilihan Semua"),
|
||||
("Empty Directory", "Folder Kosong"),
|
||||
("Not an empty directory", "Folder tidak kosong"),
|
||||
("Are you sure you want to delete this file?", "Apakah anda yakin untuk menghapus file ini?"),
|
||||
("Are you sure you want to delete this empty directory?", "Apakah anda yakin untuk menghapus folder ini?"),
|
||||
("Are you sure you want to delete the file of this directory?", "Apakah anda yakin untuk menghapus file dan folder ini?"),
|
||||
("Are you sure you want to delete this file?", "Apakah kamu yakin untuk menghapus file ini?"),
|
||||
("Are you sure you want to delete this empty directory?", "Apakah yakin yakin untuk menghapus folder ini?"),
|
||||
("Are you sure you want to delete the file of this directory?", "Apakah yakin yakin untuk menghapus file dan folder ini?"),
|
||||
("Do this for all conflicts", "Lakukan untuk semua konflik"),
|
||||
("This is irreversible!", "Ini tidak dapat diubah!"),
|
||||
("Deleting", "Menghapus"),
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "Tampilkan kualitas monitor"),
|
||||
("Disable clipboard", "Matikan papan klip"),
|
||||
("Lock after session end", "Kunci setelah sesi berakhir"),
|
||||
("Insert", "Menyisipkan"),
|
||||
("Insert Ctrl + Alt + Del", "Menyisipkan Ctrl + Alt + Del"),
|
||||
("Insert Lock", "Masukkan Kunci"),
|
||||
("Refresh", "Segarkan"),
|
||||
("ID does not exist", "ID tidak ada"),
|
||||
@@ -150,20 +150,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Click to download", "Klik untuk unduh"),
|
||||
("Click to update", "Klik untuk memperbarui"),
|
||||
("Configure", "Konfigurasi"),
|
||||
("config_acc", "Untuk mengontrol Desktop Anda dari jarak jauh, Anda perlu memberikan izin \"Aksesibilitas\" RustDesk."),
|
||||
("config_screen", "Untuk mengakses Desktop Anda dari jarak jauh, Anda perlu memberikan izin \"Perekaman Layar\" RustDesk."),
|
||||
("config_acc", "Agar bisa mengontrol Desktopmu dari jarak jauh, Kamu harus memberikan izin \"Aksesibilitas\" untuk RustDesk."),
|
||||
("config_screen", "Agar bisa mengakses Desktopmu dari jarak jauh, kamu harus memberikan izin \"Perekaman Layar\" untuk RustDesk."),
|
||||
("Installing ...", "Menginstall"),
|
||||
("Install", "Instal"),
|
||||
("Installation", "Instalasi"),
|
||||
("Installation Path", "Direktori Instalasi"),
|
||||
("Create start menu shortcuts", "Buat pintasan start menu"),
|
||||
("Create desktop icon", "Buat icon desktop"),
|
||||
("agreement_tip", "Dengan memulai instalasi, Anda menerima perjanjian lisensi."),
|
||||
("agreement_tip", "Dengan memulai proses instalasi, Kamu menerima perjanjian lisensi."),
|
||||
("Accept and Install", "Terima dan Install"),
|
||||
("End-user license agreement", "Perjanjian lisensi pengguna"),
|
||||
("Generating ...", "Memproses..."),
|
||||
("Your installation is lower version.", "Instalasi Anda adalah versi yang lebih rendah."),
|
||||
("not_close_tcp_tip", "Jangan tutup jendela ini saat menggunakan tunnel"),
|
||||
("Your installation is lower version.", "Kamu menggunakan versi instalasi yang lebih rendah."),
|
||||
("not_close_tcp_tip", "Pastikan jendela ini tetap terbuka saat menggunakan tunnel."),
|
||||
("Listening ...", "Menghubungkan..."),
|
||||
("Remote Host", "Host Remote"),
|
||||
("Remote Port", "Port Remote"),
|
||||
@@ -172,24 +172,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Local Port", "Port Lokal"),
|
||||
("Local Address", "Alamat lokal"),
|
||||
("Change Local Port", "Ubah Port Lokal"),
|
||||
("setup_server_tip", "Untuk mendapatkan koneksi yang lebih baik, disarankan untuk menginstal di server anda sendiri"),
|
||||
("setup_server_tip", "Untuk koneksi yang lebih baik, silakan konfigurasi di server pribadi"),
|
||||
("Too short, at least 6 characters.", "Terlalu pendek, setidaknya 6 karekter."),
|
||||
("The confirmation is not identical.", "Konfirmasi tidak identik."),
|
||||
("Permissions", "Perizinan"),
|
||||
("Accept", "Terima"),
|
||||
("Dismiss", "Hentikan"),
|
||||
("Disconnect", "Terputus"),
|
||||
("Enable file copy and paste", "Izinkan salin dan tempel file"),
|
||||
("Enable file copy and paste", "Izinkan copy dan paste"),
|
||||
("Connected", "Terhubung"),
|
||||
("Direct and encrypted connection", "Koneksi langsung dan terenkripsi"),
|
||||
("Relayed and encrypted connection", "Koneksi relay dan terenkripsi"),
|
||||
("Direct and unencrypted connection", "Koneksi langsung dan tanpa enkripsi"),
|
||||
("Relayed and unencrypted connection", "Koneksi relay dan tanpa enkripsi"),
|
||||
("Enter Remote ID", "Masukkan ID Remote"),
|
||||
("Enter your password", "Masukkan kata sandi anda"),
|
||||
("Enter your password", "Masukkan kata sandi"),
|
||||
("Logging in...", "Masuk..."),
|
||||
("Enable RDP session sharing", "Aktifkan berbagi sesi RDP"),
|
||||
("Auto Login", "Login Otomatis (Hanya berlaku jika Anda mengatur \"Kunci setelah sesi berakhir\")"),
|
||||
("Auto Login", "Login Otomatis (Hanya berlaku jika sudah mengatur \"Kunci setelah sesi berakhir\")"),
|
||||
("Enable direct IP access", "Aktifkan Akses IP Langsung"),
|
||||
("Rename", "Ubah nama"),
|
||||
("Space", "Spasi"),
|
||||
@@ -199,7 +199,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please enter the folder name", "Silahkan masukkan nama folder"),
|
||||
("Fix it", "Perbaiki"),
|
||||
("Warning", "Peringatan"),
|
||||
("Login screen using Wayland is not supported", "Layar masuk menggunakan Wayland tidak didukung"),
|
||||
("Login screen using Wayland is not supported", "Login screen dengan Wayland tidak didukung"),
|
||||
("Reboot required", "Diperlukan boot ulang"),
|
||||
("Unsupported display server", "Server tampilan tidak didukung "),
|
||||
("x11 expected", "Diperlukan x11"),
|
||||
@@ -241,11 +241,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Socks5 Proxy", "Proksi Socks5"),
|
||||
("Socks5/Http(s) Proxy", "Proksi Socks5/Http(s)"),
|
||||
("Discovered", "Telah ditemukan"),
|
||||
("install_daemon_tip", "Untuk memulai saat boot, Anda perlu menginstal system service."),
|
||||
("install_daemon_tip", "Untuk dapat berjalan saat sistem menyala, kamu perlu menginstal layanan sistem (system service/daemon)."),
|
||||
("Remote ID", "ID Remote"),
|
||||
("Paste", "Tempel"),
|
||||
("Paste here?", "Tempel disini?"),
|
||||
("Are you sure to close the connection?", "Apakah anda yakin akan menutup koneksi?"),
|
||||
("Are you sure to close the connection?", "Apakah kamu yakin akan menutup koneksi?"),
|
||||
("Download new version", "Unduh versi baru"),
|
||||
("Touch mode", "Mode Layar Sentuh"),
|
||||
("Mouse mode", "Mode Mouse"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "Perekaman"),
|
||||
("Directory", "Direktori"),
|
||||
("Automatically record incoming sessions", "Otomatis merekam sesi masuk"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "Ubah"),
|
||||
("Start session recording", "Mulai sesi perekaman"),
|
||||
("Stop session recording", "Hentikan sesi perekaman"),
|
||||
@@ -373,7 +374,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Write a message", "Tulis pesan"),
|
||||
("Prompt", ""),
|
||||
("Please wait for confirmation of UAC...", "Harap tunggu konfirmasi UAC"),
|
||||
("elevated_foreground_window_tip", "Jendela remote desktop ini memerlukan hak akses khusus, jadi anda tidak bisa menggunakan mouse dan keyboard untuk sementara. Anda bisa meminta pihak pengguna yang diremote untuk menyembunyikan jendela ini atau klik tombol elevasi di jendela pengaturan koneksi. Untuk menghindari masalah ini, direkomendasikan untuk menginstall aplikasi secara permanen"),
|
||||
("elevated_foreground_window_tip", "Jendela yang sedang aktif di remote desktop memerlukan hak istimewa yang lebih tinggi untuk beroperasi, sehingga mouse dan keyboard tidak dapat digunakan sementara waktu. Kamu bisa meminta pengguna jarak jauh untuk meminimalkan jendela saat ini, atau klik tombol elevasi di jendela manajemen koneksi. Untuk menghindari masalah ini, disarankan untuk menginstal software di perangkat remote secara permanen."),
|
||||
("Disconnected", "Terputus"),
|
||||
("Other", "Lainnya"),
|
||||
("Confirm before closing multiple tabs", "Konfirmasi sebelum menutup banyak tab"),
|
||||
@@ -394,9 +395,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Accept sessions via click", "Izinkan sesi dengan klik"),
|
||||
("Accept sessions via both", "Izinkan sesi dengan keduanya"),
|
||||
("Please wait for the remote side to accept your session request...", "Harap tunggu pihak pengguna remote untuk menerima permintaan sesi..."),
|
||||
("One-time Password", "Kata sandi sekali pakai"),
|
||||
("Use one-time password", "Gunakan kata sandi sekali pakai"),
|
||||
("One-time password length", "Panjang kata sandi sekali pakai"),
|
||||
("One-time Password", "Kata sandi sementara"),
|
||||
("Use one-time password", "Gunakan kata sandi sementara"),
|
||||
("One-time password length", "Panjang kata sandi sementara"),
|
||||
("Request access to your device", "Permintaan akses ke perangkat ini"),
|
||||
("Hide connection management window", "Sembunyikan jendela pengaturan koneksi"),
|
||||
("hide_cm_tip", "Izinkan untuk menyembunyikan hanya jika menerima sesi melalui kata sandi dan menggunakan kata sandi permanen"),
|
||||
@@ -569,83 +570,88 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Enter privacy mode", "Masuk mode privasi"),
|
||||
("Exit privacy mode", "Keluar mode privasi"),
|
||||
("idd_not_support_under_win10_2004_tip", "Driver grafis yang Anda gunakan tidak kompatibel dengan versi Windows Anda dan memerlukan Windows 10 versi 2004 atau yang lebih baru"),
|
||||
("input_source_1_tip", ""),
|
||||
("input_source_2_tip", ""),
|
||||
("Swap control-command key", ""),
|
||||
("swap-left-right-mouse", ""),
|
||||
("2FA code", ""),
|
||||
("More", ""),
|
||||
("enable-2fa-title", ""),
|
||||
("enable-2fa-desc", ""),
|
||||
("wrong-2fa-code", ""),
|
||||
("enter-2fa-title", ""),
|
||||
("Email verification code must be 6 characters.", ""),
|
||||
("2FA code must be 6 digits.", ""),
|
||||
("Multiple Windows sessions found", ""),
|
||||
("Please select the session you want to connect to", ""),
|
||||
("powered_by_me", ""),
|
||||
("outgoing_only_desk_tip", ""),
|
||||
("preset_password_warning", ""),
|
||||
("Security Alert", ""),
|
||||
("My address book", ""),
|
||||
("Personal", ""),
|
||||
("Owner", ""),
|
||||
("Set shared password", ""),
|
||||
("Exist in", ""),
|
||||
("input_source_1_tip", "Sumber input 1"),
|
||||
("input_source_2_tip", "Sumber input 2"),
|
||||
("Swap control-command key", "Menukar tombol control-command"),
|
||||
("swap-left-right-mouse", "Tukar fungsi tombol kiri dan kanan pada mouse"),
|
||||
("2FA code", "Kode 2FA"),
|
||||
("More", "Lainnya"),
|
||||
("enable-2fa-title", "Aktifkan autentikasi 2FA"),
|
||||
("enable-2fa-desc", "Silakan atur autentikator Anda sekarang. Anda dapat menggunakan aplikasi autentikator seperti Authy, Microsoft Authenticator, atau Google Authenticator di ponsel atau desktop Anda\n\nPindai kode QR dengan aplikasi Anda dan masukkan kode yang ditampilkan oleh aplikasi untuk mengaktifkan autentikasi 2FA."),
|
||||
("wrong-2fa-code", "Tidak dapat memverifikasi kode. Pastikan bahwa kode dan pengaturan waktu lokal sudah sesuai"),
|
||||
("enter-2fa-title", "Autentikasi dua faktor"),
|
||||
("Email verification code must be 6 characters.", "Kode verifikasi email harus terdiri dari 6 karakter."),
|
||||
("2FA code must be 6 digits.", "Kode 2FA harus terdiri dari 6 digit."),
|
||||
("Multiple Windows sessions found", "Terdapat beberapa sesi Windows"),
|
||||
("Please select the session you want to connect to", "Silakan pilih sesi yang ingin Anda sambungkan."),
|
||||
("powered_by_me", "Didukung oleh RustDesk"),
|
||||
("outgoing_only_desk_tip", "Ini adalah edisi yang sudah kustomisasi.\nAnda dapat terhubung ke perangkat lain, tetapi perangkat lain tidak dapat terhubung ke perangkat Anda."),
|
||||
("preset_password_warning", "Edisi yang dikustomisasi ini dilengkapi dengan kata sandi bawaan. Siapa pun yang mengetahui kata sandi ini dapat memperoleh kontrol penuh atas perangkat Anda. Jika Anda tidak mengharapkan ini, segera hapus pemasangan aplikasi tersebut."),
|
||||
("Security Alert", "Peringatan Keamanan"),
|
||||
("My address book", "Daftar Kontak"),
|
||||
("Personal", "Personal"),
|
||||
("Owner", "Pemilik"),
|
||||
("Set shared password", "Atus kata sandi kolaboratif"),
|
||||
("Exist in", "Ada di"),
|
||||
("Read-only", ""),
|
||||
("Read/Write", ""),
|
||||
("Full Control", ""),
|
||||
("share_warning_tip", ""),
|
||||
("share_warning_tip", "Informasi di atas bersifat publik dan dapat dilihat oleh orang lain."),
|
||||
("Everyone", ""),
|
||||
("ab_web_console_tip", ""),
|
||||
("allow-only-conn-window-open-tip", ""),
|
||||
("no_need_privacy_mode_no_physical_displays_tip", ""),
|
||||
("Follow remote cursor", ""),
|
||||
("Follow remote window focus", ""),
|
||||
("default_proxy_tip", ""),
|
||||
("no_audio_input_device_tip", ""),
|
||||
("ab_web_console_tip", "Detail Lain di Konsol Web"),
|
||||
("allow-only-conn-window-open-tip", "Koneksi hanya diperbolehkan jika jendela RustDesk sedang terbuka."),
|
||||
("no_need_privacy_mode_no_physical_displays_tip", "Karena tidak ada layar fisik, mode privasi tidak perlu diaktifkan."),
|
||||
("Follow remote cursor", "Ikuti kursor yang terhubung"),
|
||||
("Follow remote window focus", "Ikuti jendela remote yang sedang aktif"),
|
||||
("default_proxy_tip", "Pengaturan standar untuk protokol dan port adalah Socks5 dan 1080."),
|
||||
("no_audio_input_device_tip", "Perangkat input audio tidak terdeteksi."),
|
||||
("Incoming", ""),
|
||||
("Outgoing", ""),
|
||||
("Clear Wayland screen selection", ""),
|
||||
("clear_Wayland_screen_selection_tip", ""),
|
||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
||||
("android_new_voice_call_tip", ""),
|
||||
("texture_render_tip", ""),
|
||||
("Use texture rendering", ""),
|
||||
("Clear Wayland screen selection", "Kosongkan pilihan layar Wayland"),
|
||||
("clear_Wayland_screen_selection_tip", "Setelah mengosongkan pilihan layar, Kamu bisa memilih kembali layar untuk dibagi"),
|
||||
("confirm_clear_Wayland_screen_selection_tip", "Kamu yakin ingin membersihkan pemilihan layar Wayland?"),
|
||||
("android_new_voice_call_tip", "Kamu mendapatkan permintaan panggilan suara baru. Jika diterima, audio akan berubah menjadi komunikasi suara."),
|
||||
("texture_render_tip", "Aktifkan rendering tekstur untuk membuat tampilan gambar lebih mulus. Kamu dapat menonaktifkan opsi ini jika terjadi masalah saat merender."),
|
||||
("Use texture rendering", "Aktifkan rendering tekstur"),
|
||||
("Floating window", ""),
|
||||
("floating_window_tip", ""),
|
||||
("Keep screen on", ""),
|
||||
("Never", ""),
|
||||
("During controlled", ""),
|
||||
("floating_window_tip", "Untuk menjaga layanan/service RustDesk agar tetap aktif"),
|
||||
("Keep screen on", "Biarkan layar tetap menyala"),
|
||||
("Never", "Tidak pernah"),
|
||||
("During controlled", "Dalam proses pengendalian"),
|
||||
("During service is on", ""),
|
||||
("Capture screen using DirectX", ""),
|
||||
("Back", ""),
|
||||
("Apps", ""),
|
||||
("Volume up", ""),
|
||||
("Volume down", ""),
|
||||
("Capture screen using DirectX", "Rekam layar dengan DirectX"),
|
||||
("Back", "Kembali"),
|
||||
("Apps", "App"),
|
||||
("Volume up", "Naikkan volume"),
|
||||
("Volume down", "Turunkan volume"),
|
||||
("Power", ""),
|
||||
("Telegram bot", ""),
|
||||
("enable-bot-tip", ""),
|
||||
("enable-bot-desc", ""),
|
||||
("cancel-2fa-confirm-tip", ""),
|
||||
("cancel-bot-confirm-tip", ""),
|
||||
("About RustDesk", ""),
|
||||
("Send clipboard keystrokes", ""),
|
||||
("network_error_tip", ""),
|
||||
("Unlock with PIN", ""),
|
||||
("Requires at least {} characters", ""),
|
||||
("Wrong PIN", ""),
|
||||
("Set PIN", ""),
|
||||
("Enable trusted devices", ""),
|
||||
("Manage trusted devices", ""),
|
||||
("Platform", ""),
|
||||
("Days remaining", ""),
|
||||
("enable-trusted-devices-tip", ""),
|
||||
("Parent directory", ""),
|
||||
("Resume", ""),
|
||||
("Invalid file name", ""),
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("enable-bot-tip", "Jika fitur ini diaktifkan, Kamu dapat menerima kode 2FA dari bot, serta mendapatkan notifikasi tentang koneksi."),
|
||||
("enable-bot-desc", "1. Buka chat dengan @BotFather.\n2. Kirim perintah \"/newbot\". Setelah menyelesaikan langkah ini, Kamu akan mendapatkan token\n3. Mulai percakapan dengan bot yang baru dibuat. Kirim pesan yang dimulai dengan garis miring (\"/\") seperti \"/hello\" untuk mengaktifkannya."),
|
||||
("cancel-2fa-confirm-tip", "Apakah Kamu yakin ingin membatalkan 2FA?"),
|
||||
("cancel-bot-confirm-tip", "Apakah Kamu yakin ingin membatalkan bot Telegram?"),
|
||||
("About RustDesk", "Tentang RustDesk"),
|
||||
("Send clipboard keystrokes", "Kirim keystrokes clipboard"),
|
||||
("network_error_tip", "Periksa koneksi internet, lalu klik \"Coba lagi\"."),
|
||||
("Unlock with PIN", "Buka menggunakan PIN"),
|
||||
("Requires at least {} characters", "Memerlukan setidaknya {} karakter."),
|
||||
("Wrong PIN", "PIN salah"),
|
||||
("Set PIN", "Atur PIN"),
|
||||
("Enable trusted devices", "Izinkan perangkat tepercaya"),
|
||||
("Manage trusted devices", "Kelola perangkat tepercaya"),
|
||||
("Platform", "Platform"),
|
||||
("Days remaining", "Sisa hari"),
|
||||
("enable-trusted-devices-tip", "Tidak memerlukan verifikasi 2FA pada perangkat tepercaya."),
|
||||
("Parent directory", "Direktori utama"),
|
||||
("Resume", "Lanjutkan"),
|
||||
("Invalid file name", "Nama file tidak valid"),
|
||||
("one-way-file-transfer-tip", "Transfer file satu arah (One-way) telah diaktifkan pada sisi yang dikendalikan."),
|
||||
("Authentication Required", "Diperlukan autentikasi"),
|
||||
("Authenticate", "Autentikasi"),
|
||||
("web_id_input_tip", "Kamu bisa memasukkan ID pada server yang sama, akses IP langsung tidak didukung di klien web.\nJika Anda ingin mengakses perangkat di server lain, silakan tambahkan alamat server (<id>@<server_address>?key=<key_value>), contohnya:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nUntuk mengakses perangkat di server publik, cukup masukkan \"<id>@public\", tanpa kunci/key."),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "Visualizza qualità video"),
|
||||
("Disable clipboard", "Disabilita appunti"),
|
||||
("Lock after session end", "Blocca al termine della sessione"),
|
||||
("Insert", "Inserisci"),
|
||||
("Insert Ctrl + Alt + Del", "Inserisci Ctrl + Alt + Del"),
|
||||
("Insert Lock", "Blocco inserimento"),
|
||||
("Refresh", "Aggiorna"),
|
||||
("ID does not exist", "L'ID non esiste"),
|
||||
@@ -363,13 +363,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Unpin Toolbar", "Sblocca barra strumenti"),
|
||||
("Recording", "Registrazione"),
|
||||
("Directory", "Cartella"),
|
||||
("Automatically record incoming sessions", "Registra automaticamente le sessioni in entrata"),
|
||||
("Automatically record incoming sessions", "Registra automaticamente sessioni in entrata"),
|
||||
("Automatically record outgoing sessions", "Registra automaticamente sessioni in uscita"),
|
||||
("Change", "Modifica"),
|
||||
("Start session recording", "Inizia registrazione sessione"),
|
||||
("Stop session recording", "Ferma registrazione sessione"),
|
||||
("Enable recording session", "Abilita registrazione sessione"),
|
||||
("Enable LAN discovery", "Abilita rilevamento LAN"),
|
||||
("Deny LAN discovery", "Nega rilevamento LAN"),
|
||||
("Deny LAN discovery", "Non effettuare rilevamento LAN"),
|
||||
("Write a message", "Scrivi un messaggio"),
|
||||
("Prompt", "Richiedi"),
|
||||
("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."),
|
||||
@@ -532,7 +533,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("HSV Color", "Colore HSV"),
|
||||
("Installation Successful!", "Installazione completata"),
|
||||
("Installation failed!", "Installazione fallita"),
|
||||
("Reverse mouse wheel", "Rotella mouse inversa"),
|
||||
("Reverse mouse wheel", "Funzione rotellina mouse inversa"),
|
||||
("{} sessions", "{} sessioni"),
|
||||
("scam_title", "Potresti essere stato TRUFFATO!"),
|
||||
("scam_text1", "Se sei al telefono con qualcuno che NON conosci NON DI TUA FIDUCIA che ti ha chiesto di usare RustDesk e di avviare il servizio, non procedere e riattacca subito."),
|
||||
@@ -559,7 +560,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Big tiles", "Icone grandi"),
|
||||
("Small tiles", "Icone piccole"),
|
||||
("List", "Elenco"),
|
||||
("Virtual display", "Scehrmo virtuale"),
|
||||
("Virtual display", "Schermo virtuale"),
|
||||
("Plug out all", "Scollega tutto"),
|
||||
("True color (4:4:4)", "Colore reale (4:4:4)"),
|
||||
("Enable blocking user input", "Abilita blocco input utente"),
|
||||
@@ -632,7 +633,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("About RustDesk", "Info su RustDesk"),
|
||||
("Send clipboard keystrokes", "Invia sequenze tasti appunti"),
|
||||
("network_error_tip", "Controlla la connessione di rete, quindi seleziona 'Riprova'."),
|
||||
("Unlock with PIN", "Sblocca con PIN"),
|
||||
("Unlock with PIN", "Abilita sblocco con PIN"),
|
||||
("Requires at least {} characters", "Richiede almeno {} caratteri"),
|
||||
("Wrong PIN", "PIN errato"),
|
||||
("Set PIN", "Imposta PIN"),
|
||||
@@ -644,8 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Parent directory", "Cartella principale"),
|
||||
("Resume", "Riprendi"),
|
||||
("Invalid file name", "Nome file non valido"),
|
||||
("one-way-file-transfer-tip", "Il trasferimento file unidirezionale è abilitato sul lato controllato."),
|
||||
("one-way-file-transfer-tip", "Sul lato controllato è abilitato il trasferimento file unidirezionale."),
|
||||
("Authentication Required", "Richiesta autenticazione"),
|
||||
("Authenticate", "Autentica"),
|
||||
("web_id_input_tip", "È possibile inserire un ID nello stesso server, nel client web non è supportato l'accesso con IP diretto.\nSe vuoi accedere ad un dispositivo in un altro server, aggiungi l'indirizzo del server (<id>@<indirizzo_server>?key=<valore_chiave >), ad esempio,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere ad un dispositivo in un server pubblico, inserisci \"<id>@public\", la chiave non è necessaria per il server pubblico."),
|
||||
("Download", "Download"),
|
||||
("Upload folder", "Cartella upload"),
|
||||
("Upload files", "File upload"),
|
||||
("Clipboard is synchronized", "Gli appunti sono sincronizzati"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show quality monitor", "品質モニターを表示"),
|
||||
("Disable clipboard", "クリップボードを無効化"),
|
||||
("Lock after session end", "セッション終了後にロックする"),
|
||||
("Insert", "送信"),
|
||||
("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del 送信"),
|
||||
("Insert Lock", "ロック命令を送信"),
|
||||
("Refresh", "更新"),
|
||||
("ID does not exist", "IDが存在しません"),
|
||||
@@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "録画"),
|
||||
("Directory", "ディレクトリ"),
|
||||
("Automatically record incoming sessions", "受信したセッションを自動で記録する"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Change", "変更"),
|
||||
("Start session recording", "セッションの録画を開始"),
|
||||
("Stop session recording", "セッションの録画を停止"),
|
||||
@@ -647,5 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("Authentication Required", ""),
|
||||
("Authenticate", ""),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
].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