Compare commits

...

89 Commits
1.3.2 ... 1.3.3

Author SHA1 Message Date
21pages
b99c540210 add "Untagged" to filter addressbook peers without tags (#10063)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-26 20:35:17 +08:00
fufesou
84dab0e96f Fix/android keyboard map mode workaround (#10064)
* fix: Android, keyboard, map mode, workaround

The `KeyEvent.physicalKey.usbHidUsage` are wrong if using Microsoft
SwiftKey keyboard.

`window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)` is a workaround for this issue.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: clear callback on first image

Signed-off-by: fufesou <linlong1266@gmail.com>

* Android disable soft keyboard in remote page if not editing.

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-26 20:33:54 +08:00
fufesou
458a88fb89 fix: mobile autocomplete options (#10060)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-26 16:24:51 +08:00
Yevhen Popok
8a70932cd6 Update Ukrainian UI translation (#10056) 2024-11-26 09:51:27 +08:00
flusheDData
edfae98a01 Update es.rs (#10055)
New term added
2024-11-25 23:46:28 +08:00
fufesou
d61c99b105 fix: Android 29, crash on restart and reconn (#10054)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-25 21:49:09 +08:00
21pages
30a11bfe0a android wait 3s for isStart flag (#10053)
The normal process is that `startCapture` and `VideoService::run` run in parallel,  the `run` function waits for startCapture to complete, then sets the scale, and subsequently calls `stopCapture` and `startCapture`. If the `run` function does not wait long enough, `startCapture` initializes the surface with the original width and height, but the `start` flag is still false, meaning it can't call `stopCapture` and `startCapture`. This results in only capturing the upper-left portion of the virtual display.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-25 21:45:58 +08:00
fufesou
9d2bdfefb1 feat: update build_fdroid.sh (#10040)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-25 11:12:43 +08:00
21pages
34b93c6f83 fix aom pts (#10042)
the old pts/duration is wrong, use timebase 1/1000 like other codecs, not found any differences.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-25 11:09:17 +08:00
21pages
152d0ce74b install vaapi dependencies before vcpkg installs ffmpeg (#10035)
linux vaapi encoding/decoding lost since installing ffmpeg with
vcpkg

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-24 21:07:03 +08:00
rustdesk
32a3bcdc4f no custom engine 2024-11-24 10:05:00 +09:00
zuiyu
314c93b210 Create empty dir on send files in local (#9993)
* feat: Add empty dirs on sendfiles

* Update connection.rs

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-11-23 23:09:11 +08:00
rustdesk
b64f6271e2 typo 2024-11-24 00:08:39 +09:00
RustDesk
ac044c4049 Update config.toml (#10029) 2024-11-23 22:54:43 +08:00
fufesou
0973f51df9 feat: macos, audio, loopback (#10025)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-23 19:41:27 +09:00
21pages
02b046bdbf fix hwcodec ram quality change not work (#10024)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-23 16:36:13 +08:00
Andrzej Rudnik
02c274aeb6 Updated Polish translation (#10019)
* Update pl.rs

* Update README-PL.md
2024-11-23 08:41:25 +08:00
fufesou
ab6a6ca17d fix: ci macos (#10020)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-23 08:38:30 +08:00
21pages
28d38cd71d avoid invalid recording files and fix removing little recording file (#10012)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-22 17:19:22 +08:00
rustdesk
b487f297b8 flutter 3.24.5 2024-11-22 16:58:08 +09:00
21pages
64654ee7cf seperate video decoding thread for each display (#9968)
* seperate video decoding thread for each display

1. Separate Video Decoding Thread for Each Display
2. Fix Decode Errors When Clearing the Queue
Previously, on-flight frames after clearing the queue could not be decoded successfully. This issue can be resolved by setting a discard_queue flag when sending a refresh message. The flag will be reset upon receiving a keyframe.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* update video format along with fps to flutter

Signed-off-by: 21pages <sunboeasy@gmail.com>

* Fix keyframe interval when auto record outgoing sessions

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-22 00:02:25 +08:00
Alex Rijckaert
1c99eb5500 Update nl.rs (#9997) 2024-11-21 19:12:24 +08:00
zyl
74dd0c8fa0 fix mis-align problem when converting &[u8] to &[f32] (#9986)
* fix: windows, improve audio buffer (#9770)

* .

* fix statics does not record

and avoid channel changing when drio audio when audio is stero

* add some commence

* fix mis-align problem when converting &[u8] to &[f32]

* add safety commence

* revert client.rs

* avoid tmp lifetime extends

* avoid move in loop

* avoid use after drop

* another use after free

* another use after free

* make code more reasonable

---------

Co-authored-by: zylthinking <zhaoyulong@qianxin.com>
2024-11-21 13:36:11 +08:00
rustdesk
d26fea41ee upgrade The-Fat-Controller 2024-11-21 02:26:51 +09:00
21pages
bc211c8031 A=b, A case insensitive (#9976)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-20 19:44:24 +08:00
Mr-Update
d4cb7d68c5 Update de.rs (#9975) 2024-11-20 19:41:43 +08:00
KAYUII
608d7d55d5 add env VCPKG_INSTALLED_ROOT (#9985) 2024-11-20 19:41:30 +08:00
fufesou
4a49fbe4a6 fix: privacy mode 2 (#9972)
Do not change the resolutions when setting the new primary display.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-19 17:29:28 +08:00
fufesou
f760e21ff8 fix: android w&h, refresh when no connection (#9966)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-19 15:03:00 +08:00
solokot
251e1a3487 Update ru.rs (#9962) 2024-11-19 08:15:50 +08:00
bovirus
b990ff3782 Update Italian language (#9961) 2024-11-19 08:15:37 +08:00
21pages
c5426b0fbc Fix hevc decode error "Could not find ref with POC" (#9960)
1. Dropping frames can cause this error, reset encoder when this
   happens.
2. There are some logic error for clear video queue, because video queue
   message is not cleared. This need to be fixed.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-18 23:03:27 +08:00
fufesou
8b710f62c8 feat: android clipboard, multi-formats (#9950)
* feat: android clipboard, multi-formats

Signed-off-by: fufesou <linlong1266@gmail.com>

* Chore

Signed-off-by: fufesou <linlong1266@gmail.com>

* Remove unused code

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-18 15:43:41 +08:00
21pages
0707e791e8 opt av1 test data (#9954)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-18 15:05:23 +08:00
XLion
a07392e6b8 Add URL and Vendor to RPM spec (#9947) 2024-11-17 21:07:00 +08:00
21pages
9125a68f81 remove flutter install cache flag (#9944)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-17 11:35:53 +08:00
21pages
9ee77a9b92 fix last commit (#9939)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-17 09:21:34 +08:00
21pages
304e0e465d if av1 is not slow in test, av1 takes precedence over vp9 (#9938)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-16 22:31:28 +08:00
RustDesk
5d2bb9c995 Update bug_report.yaml (#9935) 2024-11-16 16:27:51 +08:00
RustDesk
ddc172bdfa Update bug_report.yaml (#9934) 2024-11-16 16:26:26 +08:00
21pages
06c7bc137f linux android use cpal (#9914)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-14 21:01:41 +08:00
rustdesk
9e4cc91a14 use linux for bridge because macos runner network problem 2024-11-13 17:00:58 +08:00
zyl
0a28d09ff8 fix: windows, improve audio buffer (#9770) (#9893)
* fix: windows, improve audio buffer (#9770)

* .

* fix statics does not record

and avoid channel changing when drio audio when audio is stero

* add some commence

---------

Co-authored-by: zylthinking <zhaoyulong@qianxin.com>
2024-11-13 15:35:23 +08:00
fufesou
ab89d84a8f refact: ci, bridge (#9899)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-12 22:10:56 +08:00
HanaKuru
0aa98eac6d Use base64Url encoding for server configuration to ensure compatibility with the command line --config option. (#9897) 2024-11-12 20:15:34 +08:00
fufesou
d4aa2b7ce4 fix: virtual display, headless, wait plug in done (#9895)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-12 20:12:05 +08:00
21pages
35b4535ebc fix aarch64 nightly build (#9881)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-11 16:28:25 +08:00
fufesou
f0be80c253 fix: macos, workaround app close (#9880)
* fix: macos, workaround app close

Signed-off-by: fufesou <linlong1266@gmail.com>

* Update common.dart

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-11-11 13:06:23 +08:00
21pages
a79a9f697b fix "Add to addresssBook" dropdown menu (#9878)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-11 08:39:54 +08:00
fufesou
6082bb2754 fix: save load window rect, Windows, ignore dpi (#9875)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-11 00:35:41 +08:00
rustdesk
4e6a43288e https://github.com/rustdesk/rustdesk/issues/9877 https://developer.apple.com/documentation/security/ksecusedataprotectionkeychain 2024-11-10 23:57:25 +08:00
fufesou
72a1f1161e refact: flutter 3.24.4 (#9874)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-10 11:18:08 +08:00
RustDesk
912f5265f1 Revert "Refact/flutter 3.24.4 (#9870)" (#9871)
This reverts commit 5eb2c31207.
2024-11-09 23:32:18 +08:00
fufesou
5eb2c31207 Refact/flutter 3.24.4 (#9870)
* Update pubspec.lock

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact: flutter 3.24.3

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: workaround Autocomplete options

Signed-off-by: fufesou <linlong1266@gmail.com>

* Replace engine with rustdesk custom flutter engine

* Update flutter-build.yml to use RustDesk flutter engine

* Fix the problem of missing extraction file directory windows-x64-release

* Update pubspec.lock.3.22.3

Signed-off-by: fufesou <linlong1266@gmail.com>

* remove pubspec.lock.3.22.3

Signed-off-by: fufesou <linlong1266@gmail.com>

* upgrade flutter android to 3.24.4

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: vitoway <vitoway@email.com>
Co-authored-by: vitoway <167743630+vitoway@users.noreply.github.com>
2024-11-09 20:51:47 +08:00
Lee Jong Mun
062c8d582c kor translation update (#9866) 2024-11-09 11:37:54 +08:00
XLion
68b07505ab Update tw.rs (#9863) 2024-11-08 22:18:30 +08:00
fufesou
d3efcd4223 fix: mobile, soft keyboard (#9860)
Switching the input method, don't affect the canvas.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-08 15:01:36 +08:00
rustdesk
a277b022ff bump to 1.3.3 2024-11-08 15:00:49 +08:00
21pages
740c5358ab rotate ID3D11Texture2D (#9772)
* Rotate ID3D11Texture2D after duplication with d3d11 video processor.
* If display is not rotated, nothing will be created; If the rotation
  fails, it will use the old fallback logic

TODO:
* If changing from Landscape to Landscape(flipped) during capture, the resolution is
  not changed, video service fallback to gdi directly.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-08 12:12:10 +08:00
fufesou
7978e0301d fix: input mobile -> Android (#9767)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-08 12:11:56 +08:00
fufesou
0f070b0108 revert: 9644, iOS, Korean input (#9857)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-07 22:54:14 +08:00
fufesou
6f0cb3b8c2 fix: mobile, two fingers cale, no tapdown (#9856)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-07 22:36:56 +08:00
fufesou
d0ef52e418 fix: touch input, ensure message orders (#9855)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-07 21:23:41 +08:00
fufesou
69277dd16b fix: mobile, don't adjust canvas on gesture help show up (#9846)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-07 14:58:10 +08:00
fufesou
faf97c770c fix: mobile, cursor mode, don't reset canvas (#9843)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-06 21:59:23 +08:00
notlin4
78088360ca Fix traditional Chinese localization (#9833) 2024-11-06 07:59:12 +08:00
fufesou
5cfd1701fb fix: mobile input, touch mode, in display (#9827)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-05 17:55:38 +08:00
21pages
a4bd23c9de fix missing window focus service on windows/macos (#9824)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-11-05 11:28:27 +08:00
fufesou
12c1337b7b fix: mobile mouse mode, cursor range (#9811)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-04 22:37:21 +08:00
fufesou
040253b319 fix: mobile cursor focus (#9803)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-01 15:40:57 +08:00
fufesou
44fa83d080 fix: android input, soft keyboard, mouse mode (#9797)
Cursor movement in the remote screen.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-11-01 11:25:38 +08:00
21pages
4f7e10bac6 Revert "Change the minimum value of the bitrate slider to 5" (#9795)
* Revert "Change the minimum value of the bitrate slider to 5 (#9480)"

This reverts commit beb1084e87.

* Revert "Change the value of kMinQuality to 5  (#9508)"

This reverts commit d563372a91.
2024-10-31 22:57:39 +08:00
jkh0kr
d1fdcf1b16 Update ko.rs (#9792) 2024-10-31 12:01:37 +08:00
fufesou
4c12b83068 fix: android input do not handle composing (#9790)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-31 10:20:57 +08:00
21pages
f86c88b3d8 refresh icon not visible when not using one-time password (#9791)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-31 10:11:42 +08:00
fufesou
697dd87383 Refact/mobile remove adjust 4 soft keyabord (#9787)
* refact: remove adjust for soft keyboard

Signed-off-by: fufesou <linlong1266@gmail.com>

* mobile, do not set the view style after scale end

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-31 10:11:00 +08:00
fufesou
0b8cccd8be fix: mobile view style, on conn (#9786)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-30 19:00:07 +08:00
Kleofass
32dbc0c8fb Update lv.rs (#9785) 2024-10-30 17:08:32 +08:00
fufesou
bae4a2c710 Fix/android check normal usbhid usage (#9784)
* fix: android check normal usbhid usage

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: android input, ignore composing if is deleting

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-30 15:29:52 +08:00
RustDesk
711ed28846 Revert "revert: #9769 (#9780)" (#9783)
This reverts commit 0f5f9f6524.
2024-10-30 13:12:18 +08:00
fufesou
e6c5064ce5 fix: android input, Backspace and Enter (#9782)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-30 13:05:25 +08:00
fufesou
0f5f9f6524 revert: #9769 (#9780)
Not sure TextEditingController.addListener() can handle all composing
changes.

https://github.com/rustdesk/rustdesk/issues/7727#issuecomment-2445721499

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-30 12:17:13 +08:00
21pages
ce7867c1c0 fix wrong display of custom clients when approval mode is not set (#9779)
when approve-mode is not set, the approve mode option shows as
password, it's `both` approve mode in rust, so only ui is wrong.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-30 11:29:39 +08:00
rustdesk
415d2c5c60 OPTION_VERIFICATION_METHOD 2024-10-29 22:52:46 +08:00
fufesou
a289eae07c fix: mobile -> mobile, long press (#9775)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-29 19:57:29 +08:00
RustDesk
1c9b456456 Update bug_report.yaml (#9771) 2024-10-29 15:33:12 +08:00
fufesou
eba19e67ff fix: mobile input (#9769)
1. Map mode. Check if the KeyEvent's usbHidUsage is correct.
2. Korean input, use listener to handle composing state.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-10-29 14:19:16 +08:00
John Fowler
adc5a7be51 Update hu.rs (#9762)
Making the Hungarian language file complete. Adding tips in Hungarian.
2024-10-29 13:55:13 +08:00
pppanghu77
26d23d588a fix: [translations] Add the translation in cn.rs (#9768)
Add the translation in cn.rs

Log: Add the translation in cn.rs
2024-10-29 13:54:16 +08:00
138 changed files with 3477 additions and 1650 deletions

View File

@@ -12,3 +12,5 @@ rustflags = [
#rustflags = [
# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic"
#]
[net]
git-fetch-with-cli = true

View File

@@ -26,8 +26,8 @@ body:
- type: input
id: os
attributes:
label: Operating system(s) on local side and remote side
description: What operating system(s) do you see this bug on? local side -> remote side.
label: Operating system(s) on local (controlling) side and remote (controlled) side
description: What operating system(s) do you see this bug on? local (controlling) side -> remote (controlled) side.
placeholder: |
Windows 10 -> osx
validations:
@@ -35,8 +35,8 @@ body:
- type: input
id: version
attributes:
label: RustDesk Version(s) on local side and remote side
description: What RustDesk version(s) do you see this bug on? local side -> remote side.
label: RustDesk Version(s) on local (controlling) side and remote (controlled) side
description: What RustDesk version(s) do you see this bug on? local (controlling) side -> remote (controlled) side.
placeholder: |
1.1.9 -> 1.1.8
validations:

View File

@@ -0,0 +1,42 @@
diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart
index 7e634cd2aa..c1e9acc295 100644
--- a/packages/flutter/lib/src/material/dropdown_menu.dart
+++ b/packages/flutter/lib/src/material/dropdown_menu.dart
@@ -475,7 +475,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
final GlobalKey _leadingKey = GlobalKey();
late List<GlobalKey> buttonItemKeys;
final MenuController _controller = MenuController();
- late bool _enableFilter;
+ bool _enableFilter = false;
late List<DropdownMenuEntry<T>> filteredEntries;
List<Widget>? _initialMenu;
int? currentHighlight;
@@ -524,6 +524,11 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
}
_localTextEditingController = widget.controller ?? TextEditingController();
}
+ if (oldWidget.enableFilter != widget.enableFilter) {
+ if (!widget.enableFilter) {
+ _enableFilter = false;
+ }
+ }
if (oldWidget.enableSearch != widget.enableSearch) {
if (!widget.enableSearch) {
currentHighlight = null;
@@ -663,6 +668,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
);
currentHighlight = widget.enableSearch ? i : null;
widget.onSelected?.call(entry.value);
+ _enableFilter = false;
}
: null,
requestFocusOnHover: false,
@@ -735,6 +741,8 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
if (_enableFilter) {
filteredEntries = widget.filterCallback?.call(filteredEntries, _localTextEditingController!.text)
?? filter(widget.dropdownMenuEntries, _localTextEditingController!);
+ } else {
+ filteredEntries = widget.dropdownMenuEntries;
}
if (widget.enableSearch) {

View File

@@ -6,7 +6,7 @@ on:
workflow_call:
env:
FLUTTER_VERSION: "3.19.6"
FLUTTER_VERSION: "3.22.3"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
@@ -74,11 +74,12 @@ jobs:
shell: bash
run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
pushd flutter && flutter pub get && popd
pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd
- name: Run flutter rust bridge
run: |
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h
- name: Upload Artifact
uses: actions/upload-artifact@master
@@ -89,3 +90,5 @@ jobs:
./src/bridge_generated.io.rs
./flutter/lib/generated_bridge.dart
./flutter/lib/generated_bridge.freezed.dart
./flutter/macos/Runner/bridge_generated.h
./flutter/ios/Runner/bridge_generated.h

View File

@@ -20,20 +20,20 @@ on:
env:
SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
MAC_RUST_VERSION: "1.81" # 1.81 is requred for macos, because of https://github.com/yury/cidre requires 1.81
CARGO_NDK_VERSION: "3.1.2"
SCITER_ARMV7_CMAKE_VERSION: "3.29.7"
SCITER_NASM_DEBVERSION: "2.14-1"
LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.19.6"
ANDROID_FLUTTER_VERSION: "3.13.9" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem).
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
FLUTTER_VERSION: "3.24.5"
ANDROID_FLUTTER_VERSION: "3.24.5"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.07.12
VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1"
VERSION: "1.3.2"
VERSION: "1.3.3"
NDK_VERSION: "r27b"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -46,6 +46,9 @@ env:
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs:
generate-bridge:
uses: ./.github/workflows/bridge.yml
build-RustDeskTempTopMostWindow:
uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml
with:
@@ -59,7 +62,7 @@ jobs:
build-for-windows-flutter:
name: ${{ matrix.job.target }}
needs: [build-RustDeskTempTopMostWindow]
needs: [build-RustDeskTempTopMostWindow, generate-bridge]
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
@@ -85,6 +88,12 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
- name: Restore bridge files
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v1
with:
@@ -95,7 +104,23 @@ jobs:
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
# https://github.com/flutter/flutter/issues/155685
- name: Replace engine with rustdesk custom flutter engine
if: false
run: |
flutter doctor -v
flutter precache --windows
Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip
Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release
mv -Force windows-x64-release/*  C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/
- name: Patch flutter
shell: bash
run: |
cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter)))
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
@@ -108,13 +133,6 @@ jobs:
with:
prefix-key: ${{ matrix.job.os }}
- name: Install flutter rust bridge deps
run: |
git config --global core.longpaths true
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
Push-Location flutter ; flutter pub get ; Pop-Location
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
@@ -374,6 +392,7 @@ jobs:
# use build-for-macOS instead
if: false
runs-on: [self-hosted, macOS, ARM64]
needs: [generate-bridge]
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
@@ -385,12 +404,11 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
- name: Install flutter rust bridge deps
shell: bash
run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
- name: Restore bridge files
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Build rustdesk
run: |
@@ -446,6 +464,7 @@ jobs:
if: ${{ inputs.upload-artifact }}
name: build rustdesk ios ipa
runs-on: ${{ matrix.job.os }}
needs: [generate-bridge]
strategy:
fail-fast: false
matrix:
@@ -475,6 +494,11 @@ jobs:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Patch flutter
run: |
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
@@ -510,12 +534,11 @@ jobs:
prefix-key: rustdesk-lib-cache-ios
key: ${{ matrix.job.target }}
- name: Install flutter rust bridge deps
shell: bash
run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
- name: Restore bridge files
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Build rustdesk lib
run: |
@@ -550,6 +573,7 @@ jobs:
#if: ${{ inputs.upload-artifact }}
if: false
runs-on: [self-hosted, macOS, ARM64]
needs: [generate-bridge]
strategy:
fail-fast: false
steps:
@@ -565,12 +589,11 @@ jobs:
# $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
- name: Install flutter rust bridge deps
shell: bash
run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
- name: Restore bridge files
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Build rustdesk lib
run: |
@@ -605,6 +628,7 @@ jobs:
build-for-macOS:
name: ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
needs: [generate-bridge]
strategy:
fail-fast: false
matrix:
@@ -619,7 +643,7 @@ jobs:
target: aarch64-apple-darwin,
os: macos-latest,
# extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296
extra-build-args: "",
extra-build-args: "--screencapturekit",
arch: aarch64,
}
steps:
@@ -668,7 +692,13 @@ jobs:
- name: Install build runtime
run: |
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config
brew install llvm create-dmg nasm cmake gcc wget ninja
# pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner
if command -v pkg-config &>/dev/null; then
echo "pkg-config is already installed"
else
brew install pkg-config
fi
- name: Install flutter
uses: subosito/flutter-action@v2
@@ -676,6 +706,11 @@ jobs:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Patch flutter
run: |
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Workaround for flutter issue
shell: bash
run: |
@@ -687,7 +722,7 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
toolchain: ${{ env.MAC_RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
@@ -695,12 +730,11 @@ jobs:
with:
prefix-key: ${{ matrix.job.os }}
- name: Install flutter rust bridge deps
shell: bash
run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
- name: Restore bridge files
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
@@ -735,6 +769,13 @@ jobs:
- name: Build rustdesk
run: |
if [ "${{ matrix.job.target }}" = "aarch64-apple-darwin" ]; then
MIN_MACOS_VERSION="12.3"
sed -i -e "s/MACOSX_DEPLOYMENT_TARGET\=[0-9]*.[0-9]*/MACOSX_DEPLOYMENT_TARGET=${MIN_MACOS_VERSION}/" build.py
sed -i -e "s/platform :osx, '.*'/platform :osx, '${MIN_MACOS_VERSION}'/" flutter/macos/Podfile
sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml
sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj
fi
./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }}
- name: create unsigned dmg
@@ -822,11 +863,8 @@ jobs:
tag_name: ${{ env.TAG_NAME }}
files: rustdesk-${{ env.VERSION }}-unsigned.tar.gz
generate-bridge-linux:
uses: ./.github/workflows/bridge.yml
build-rustdesk-android:
needs: [generate-bridge-linux]
needs: [generate-bridge]
name: build rustdesk android apk ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
strategy:
@@ -903,7 +941,7 @@ jobs:
llvm-10-dev \
nasm \
ninja-build \
openjdk-11-jdk-headless \
openjdk-17-jdk-headless \
pkg-config \
tree \
wget
@@ -915,6 +953,12 @@ jobs:
with:
channel: "stable"
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
- name: Patch flutter
run: |
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
@@ -974,7 +1018,7 @@ jobs:
key: ${{ matrix.job.target }}
- name: fix android for flutter 3.13
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
run: |
cd flutter
sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml
@@ -1022,9 +1066,9 @@ jobs:
- name: Build rustdesk
shell: bash
env:
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
run: |
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
# temporary use debug sign config
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
case ${{ matrix.job.target }} in
@@ -1069,6 +1113,14 @@ jobs:
mkdir -p signed-apk; pushd signed-apk
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk .
# https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075
- name: Setup sign tool version variable
shell: bash
run: |
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
@@ -1080,8 +1132,8 @@ jobs:
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "30.0.2"
# env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable"
BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }}
- name: Upload Artifacts
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
@@ -1166,7 +1218,7 @@ jobs:
llvm-10-dev \
nasm \
ninja-build \
openjdk-11-jdk-headless \
openjdk-17-jdk-headless \
pkg-config \
tree \
wget
@@ -1179,6 +1231,11 @@ jobs:
channel: "stable"
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
- name: Patch flutter
run: |
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Restore bridge files
uses: actions/download-artifact@master
with:
@@ -1211,7 +1268,7 @@ jobs:
path: ./flutter/android/app/src/main/jniLibs/x86
- name: fix android for flutter 3.13
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
run: |
cd flutter
sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml
@@ -1223,9 +1280,9 @@ jobs:
- name: Build rustdesk
shell: bash
env:
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
run: |
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
# temporary use debug sign config
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
@@ -1245,6 +1302,14 @@ jobs:
mkdir -p signed-apk
mv ./flutter/build/app/outputs/flutter-apk/app-${{ env.reltype }}.apk signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk
# https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075
- name: Setup sign tool version variable
shell: bash
run: |
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
@@ -1256,8 +1321,8 @@ jobs:
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "30.0.2"
# env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable"
BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }}
- name: Upload Artifacts
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
@@ -1285,7 +1350,7 @@ jobs:
signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk
build-rustdesk-linux:
needs: [generate-bridge-linux]
needs: [generate-bridge]
name: build rustdesk linux ${{ matrix.job.target }}
runs-on: ${{ matrix.job.on }}
strategy:
@@ -1376,6 +1441,7 @@ jobs:
- name: Install vcpkg dependencies
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
run: |
sudo apt install -y libva-dev libvdpau-dev
if ! $VCPKG_ROOT/vcpkg \
install \
--triplet ${{ matrix.job.vcpkg-triplet }} \
@@ -1520,6 +1586,19 @@ jobs:
;;
esac
if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then
case ${{ matrix.job.arch }} in
aarch64)
pushd /opt/flutter-elinux/flutter
;;
x86_64)
pushd /opt/flutter
;;
esac
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
popd
fi
# build flutter
pushd /workspace
export CARGO_INCREMENTAL=0
@@ -1989,7 +2068,12 @@ jobs:
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Patch flutter
shell: bash
run: |
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
# https://rustdesk.com/docs/en/dev/build/web/
- name: Build web

View File

@@ -18,7 +18,7 @@ env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.06.15
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
VERSION: "1.3.2"
VERSION: "1.3.3"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"

32
Cargo.lock generated
View File

@@ -893,6 +893,20 @@ dependencies = [
"regex",
]
[[package]]
name = "cidre"
version = "0.4.0"
source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf"
dependencies = [
"cidre-macros",
"parking_lot",
]
[[package]]
name = "cidre-macros"
version = "0.1.0"
source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf"
[[package]]
name = "cipher"
version = "0.4.4"
@@ -1276,10 +1290,10 @@ dependencies = [
[[package]]
name = "cpal"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#4d318ff778063ce14669fd4bd67a1673653fc6e5"
dependencies = [
"alsa",
"cidre",
"core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)",
"coreaudio-rs",
"dasp_sample",
@@ -1683,7 +1697,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.8.4",
"libloading 0.7.4",
]
[[package]]
@@ -3051,7 +3065,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hwcodec"
version = "0.7.0"
source = "git+https://github.com/rustdesk-org/hwcodec#8bbd05bb300ad07cc345356ad85570f9ea99fbfa"
source = "git+https://github.com/rustdesk-org/hwcodec#da7dab48df19edb5a7138ff9e01bf9f148b523da"
dependencies = [
"bindgen 0.59.2",
"cc",
@@ -3505,7 +3519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
dependencies = [
"cfg-if 1.0.0",
"windows-targets 0.52.5",
"windows-targets 0.48.5",
]
[[package]]
@@ -4149,7 +4163,7 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
"proc-macro-crate 2.0.2",
"proc-macro-crate 1.3.1",
"proc-macro2 1.0.86",
"quote 1.0.36",
"syn 2.0.68",
@@ -5480,7 +5494,7 @@ dependencies = [
[[package]]
name = "rustdesk"
version = "1.3.2"
version = "1.3.3"
dependencies = [
"android-wakelock",
"android_logger",
@@ -5580,7 +5594,7 @@ dependencies = [
[[package]]
name = "rustdesk-portable-packer"
version = "1.3.2"
version = "1.3.3"
dependencies = [
"brotli",
"dirs 5.0.1",
@@ -6445,7 +6459,7 @@ dependencies = [
[[package]]
name = "tfc"
version = "0.7.0"
source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#de9c8ba480f166a9fc90aaa47bb0e84b443ea9c6"
source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#78bb80a8e596e4c14ae57c8448f5fca75f91f2b0"
dependencies = [
"anyhow",
"core-graphics 0.23.2",

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk"
version = "1.3.2"
version = "1.3.3"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"
@@ -36,6 +36,7 @@ unix-file-copy-paste = [
"dep:once_cell",
"clipboard/unix-file-copy-paste",
]
screencapturekit = ["cpal/screencapturekit"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -77,9 +78,7 @@ fon = "0.6"
zip = "0.6"
shutdown_hooks = "0.1"
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies]
cpal = "0.15"
cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" }
ringbuf = "0.3"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.3.2
version: 1.3.3
exec: usr/lib/rustdesk/rustdesk
exec_args: $@
apt:

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.3.2
version: 1.3.3
exec: usr/lib/rustdesk/rustdesk
exec_args: $@
apt:

View File

@@ -143,6 +143,12 @@ def make_parser():
"--package",
type=str
)
if osx:
parser.add_argument(
'--screencapturekit',
action='store_true',
help='Enable feature screencapturekit'
)
return parser
@@ -274,6 +280,9 @@ def get_features(args):
features.append('flutter')
if args.unix_file_copy_paste:
features.append('unix-file-copy-paste')
if osx:
if args.screencapturekit:
features.append('screencapturekit')
print("features:", features)
return features

View File

@@ -1,7 +1,7 @@
#[cfg(windows)]
fn build_windows() {
let file = "src/platform/windows.cc";
let file2 = "src/platform/windows_delete_test_cert.cc";
let file2 = "src/platform/windows_delete_test_cert.cc";
cc::Build::new().file(file).file(file2).compile("windows");
println!("cargo:rustc-link-lib=WtsApi32");
println!("cargo:rerun-if-changed={}", file);
@@ -61,7 +61,11 @@ fn install_android_deps() {
let target = format!("{}-android", target_arch);
let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap();
let mut path: std::path::PathBuf = vcpkg_root.into();
path.push("installed");
if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") {
path = vcpkg_root.into();
} else {
path.push("installed");
}
path.push(target);
println!(
"{}",
@@ -72,7 +76,6 @@ fn install_android_deps() {
);
println!("cargo:rustc-link-lib=ndk_compat");
println!("cargo:rustc-link-lib=oboe");
println!("cargo:rustc-link-lib=oboe_wrapper");
println!("cargo:rustc-link-lib=c++");
println!("cargo:rustc-link-lib=OpenSLES");
}

View File

@@ -164,3 +164,7 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)
## [Serwery publiczne](#public-servers)
RustDesk jest obsługiwany przez bezpłatne serwer w Unii Europejskiej, uprzejmie dostarczony przez [Codext GmbH](https://codext.link/rustdesk?utm_source=github)

View File

@@ -1,6 +1,9 @@
import com.google.protobuf.gradle.*
plugins {
id "com.google.protobuf" version "0.9.4"
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def keystoreProperties = new Properties()
@@ -17,11 +20,6 @@ if (localPropertiesFile.exists()) {
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
@@ -32,10 +30,6 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
dependencies {
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
}
@@ -57,7 +51,7 @@ protobuf {
}
android {
compileSdkVersion 33
compileSdkVersion 34
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@@ -105,7 +99,6 @@ flutter {
dependencies {
implementation "androidx.media:media:1.6.0"
implementation 'com.github.getActivity:XXPermissions:18.5'
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("1.9.10") } }
implementation 'com.caverock:androidsvg-aar:1.4'
}

View File

@@ -304,7 +304,13 @@ class FloatingWindowService : Service(), View.OnTouchListener {
val popupMenu = PopupMenu(this, floatingView)
val idShowRustDesk = 0
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
val idStopService = 1
// For host side, clipboard sync
val idSyncClipboard = 1
val isClipboardListenerEnabled = MainActivity.rdClipboardManager?.isListening ?: false
if (isClipboardListenerEnabled) {
popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard"))
}
val idStopService = 2
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
@@ -312,6 +318,10 @@ class FloatingWindowService : Service(), View.OnTouchListener {
openMainActivity()
true
}
idSyncClipboard -> {
syncClipboard()
true
}
idStopService -> {
stopMainService()
true
@@ -340,6 +350,10 @@ class FloatingWindowService : Service(), View.OnTouchListener {
}
}
private fun syncClipboard() {
MainActivity.rdClipboardManager?.syncClipboard(false)
}
private fun stopMainService() {
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null)
}

View File

@@ -280,20 +280,20 @@ class InputService : AccessibilityService() {
var textToCommit: String? = null
if (keyboardMode == KeyboardMode.Legacy) {
if (keyEvent.hasChr() && keyEvent.getDown()) {
// [down] indicates the key's state(down or up).
// [press] indicates a click event(down and up).
// https://github.com/rustdesk/rustdesk/blob/3a7594755341f023f56fa4b6a43b60d6b47df88d/flutter/lib/models/input_model.dart#L688
if (keyEvent.hasSeq()) {
textToCommit = keyEvent.getSeq()
} else if (keyboardMode == KeyboardMode.Legacy) {
if (keyEvent.hasChr() && (keyEvent.getDown() || keyEvent.getPress())) {
val chr = keyEvent.getChr()
if (chr != null) {
textToCommit = String(Character.toChars(chr))
}
}
} else if (keyboardMode == KeyboardMode.Translate) {
if (keyEvent.hasSeq() && keyEvent.getDown()) {
val seq = keyEvent.getSeq()
if (seq != null) {
textToCommit = seq
}
}
} else {
}
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
@@ -320,6 +320,10 @@ class InputService : AccessibilityService() {
} else {
ke?.let { event ->
inputConnection.sendKeyEvent(event)
if (keyEvent.getPress()) {
val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode)
inputConnection.sendKeyEvent(actionUpEvent)
}
}
}
}
@@ -333,6 +337,10 @@ class InputService : AccessibilityService() {
for (item in possibleNodes) {
val success = trySendKeyEvent(event, item, textToCommit)
if (success) {
if (keyEvent.getPress()) {
val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode)
trySendKeyEvent(actionUpEvent, item, textToCommit)
}
break
}
}

View File

@@ -31,14 +31,12 @@ object KeyEventConverter {
}
var action = 0
if (keyEventProto.getDown()) {
if (keyEventProto.getDown() || keyEventProto.getPress()) {
action = KeyEvent.ACTION_DOWN
} else {
action = KeyEvent.ACTION_UP
}
// FIXME: The last parameter is the repeat count, not modifiers ?
// https://developer.android.com/reference/android/view/KeyEvent#KeyEvent(long,%20long,%20int,%20int,%20int)
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
}

View File

@@ -13,6 +13,8 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.ClipboardManager
import android.os.Bundle
import android.os.Build
import android.os.IBinder
import android.util.Log
@@ -36,6 +38,9 @@ import kotlin.concurrent.thread
class MainActivity : FlutterActivity() {
companion object {
var flutterMethodChannel: MethodChannel? = null
private var _rdClipboardManager: RdClipboardManager? = null
val rdClipboardManager: RdClipboardManager?
get() = _rdClipboardManager;
}
private val channelTag = "mChannel"
@@ -85,11 +90,20 @@ class MainActivity : FlutterActivity() {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (_rdClipboardManager == null) {
_rdClipboardManager = RdClipboardManager(getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
FFI.setClipboardManager(_rdClipboardManager!!)
}
}
override fun onDestroy() {
Log.e(logTag, "onDestroy")
mainService?.let {
unbindService(serviceConnection)
}
rdClipboardManager?.rustEnableServiceClipboard(false)
super.onDestroy()
}
@@ -388,4 +402,13 @@ class MainActivity : FlutterActivity() {
super.onStart()
stopService(Intent(this, FloatingWindowService::class.java))
}
// For client side
// When swithing from other app to this app, try to sync clipboard.
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
rdClipboardManager?.syncClipboard(true)
}
}
}

View File

@@ -302,6 +302,8 @@ class MainService : Service() {
stopCapture()
FFI.refreshScreen()
startCapture()
} else {
FFI.refreshScreen()
}
}

View File

@@ -0,0 +1,224 @@
package com.carriez.flutter_hbb
import java.nio.ByteBuffer
import java.util.Timer
import java.util.TimerTask
import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
import android.util.Log
import androidx.annotation.Keep
import hbb.MessageOuterClass.ClipboardFormat
import hbb.MessageOuterClass.Clipboard
import hbb.MessageOuterClass.MultiClipboards
import ffi.FFI
class RdClipboardManager(private val clipboardManager: ClipboardManager) {
private val logTag = "RdClipboardManager"
private val supportedMimeTypes = arrayOf(
ClipDescription.MIMETYPE_TEXT_PLAIN,
ClipDescription.MIMETYPE_TEXT_HTML
)
// 1. Avoid listening to the same clipboard data updated by `rustUpdateClipboard`.
// 2. Avoid sending the clipboard data before enabling client clipboard.
// 1) Disable clipboard
// 2) Copy text "a"
// 3) Enable clipboard
// 4) Switch to another app
// 5) Switch back to the app
// 6) "a" should not be sent to the client, because it's copied before enabling clipboard
//
// It's okay to that `rustEnableClientClipboard(false)` is called after `rustUpdateClipboard`,
// though the `lastUpdatedClipData` will be set to null once.
private var lastUpdatedClipData: ClipData? = null
private var isClientEnabled = true;
private var _isListening = false;
val isListening: Boolean
get() = _isListening
fun checkPrimaryClip(isClient: Boolean, isSync: Boolean) {
val clipData = clipboardManager.primaryClip
if (clipData != null && clipData.itemCount > 0) {
// Only handle the first item in the clipboard for now.
val clip = clipData.getItemAt(0)
val isHostSync = !isClient && isSync
// Ignore the `isClipboardDataEqual()` check if it's a host sync operation.
// Because it's a action manually triggered by the user.
if (!isHostSync) {
if (lastUpdatedClipData != null && isClipboardDataEqual(clipData, lastUpdatedClipData!!)) {
Log.d(logTag, "Clipboard data is the same as last update, ignore")
return
}
}
val mimeTypeCount = clipData.description.getMimeTypeCount()
val mimeTypes = mutableListOf<String>()
for (i in 0 until mimeTypeCount) {
mimeTypes.add(clipData.description.getMimeType(i))
}
var text: CharSequence? = null;
var html: String? = null;
if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
text = clip?.text
}
if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) {
text = clip?.text
html = clip?.htmlText
}
var count = 0
val clips = MultiClipboards.newBuilder()
if (text != null) {
val content = com.google.protobuf.ByteString.copyFromUtf8(text.toString())
clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Text).setContent(content).build())
count++
}
if (html != null) {
val content = com.google.protobuf.ByteString.copyFromUtf8(html)
clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Html).setContent(content).build())
count++
}
if (count > 0) {
val clipsBytes = clips.build().toByteArray()
val isClientFlag = if (isClient) 1 else 0
val clipsBuf = ByteBuffer.allocateDirect(clipsBytes.size + 1).apply {
put(isClientFlag.toByte())
put(clipsBytes)
}
clipsBuf.flip()
lastUpdatedClipData = clipData
Log.d(logTag, "${if (isClient) "client" else "host"}, send clipboard data to the remote")
FFI.onClipboardUpdate(clipsBuf)
}
}
}
private val clipboardListener = object : ClipboardManager.OnPrimaryClipChangedListener {
override fun onPrimaryClipChanged() {
Log.d(logTag, "onPrimaryClipChanged")
checkPrimaryClip(true, false)
}
}
private fun isSupportedMimeType(mimeType: String): Boolean {
return supportedMimeTypes.contains(mimeType)
}
private fun isClipboardDataEqual(left: ClipData, right: ClipData): Boolean {
if (left.description.getMimeTypeCount() != right.description.getMimeTypeCount()) {
return false
}
val mimeTypeCount = left.description.getMimeTypeCount()
for (i in 0 until mimeTypeCount) {
if (left.description.getMimeType(i) != right.description.getMimeType(i)) {
return false
}
}
if (left.itemCount != right.itemCount) {
return false
}
for (i in 0 until left.itemCount) {
val mimeType = left.description.getMimeType(i)
if (!isSupportedMimeType(mimeType)) {
continue
}
val leftItem = left.getItemAt(i)
val rightItem = right.getItemAt(i)
if (mimeType == ClipDescription.MIMETYPE_TEXT_PLAIN || mimeType == ClipDescription.MIMETYPE_TEXT_HTML) {
if (leftItem.text != rightItem.text || leftItem.htmlText != rightItem.htmlText) {
return false
}
}
}
return true
}
@Keep
fun rustEnableServiceClipboard(enable: Boolean) {
Log.d(logTag, "rustEnableServiceClipboard: enable: $enable, _isListening: $_isListening")
if (enable) {
if (!_isListening) {
clipboardManager.addPrimaryClipChangedListener(clipboardListener)
_isListening = true
}
} else {
if (_isListening) {
clipboardManager.removePrimaryClipChangedListener(clipboardListener)
_isListening = false
lastUpdatedClipData = null
}
}
}
@Keep
fun rustEnableClientClipboard(enable: Boolean) {
Log.d(logTag, "rustEnableClientClipboard: enable: $enable")
isClientEnabled = enable
if (enable) {
lastUpdatedClipData = clipboardManager.primaryClip
} else {
lastUpdatedClipData = null
}
}
fun syncClipboard(isClient: Boolean) {
Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled, _isListening: $_isListening")
if (isClient && !isClientEnabled) {
return
}
if (!isClient && !_isListening) {
return
}
checkPrimaryClip(isClient, true)
}
@Keep
fun rustUpdateClipboard(clips: ByteArray) {
val clips = MultiClipboards.parseFrom(clips)
var mimeTypes = mutableListOf<String>()
var text: String? = null
var html: String? = null
for (clip in clips.getClipboardsList()) {
when (clip.format) {
ClipboardFormat.Text -> {
mimeTypes.add(ClipDescription.MIMETYPE_TEXT_PLAIN)
text = String(clip.content.toByteArray(), Charsets.UTF_8)
}
ClipboardFormat.Html -> {
mimeTypes.add(ClipDescription.MIMETYPE_TEXT_HTML)
html = String(clip.content.toByteArray(), Charsets.UTF_8)
}
ClipboardFormat.ImageRgba -> {
}
ClipboardFormat.ImagePng -> {
}
else -> {
Log.e(logTag, "Unsupported clipboard format: ${clip.format}")
}
}
}
val clipDescription = ClipDescription("clipboard", mimeTypes.toTypedArray())
var item: ClipData.Item? = null
if (text == null) {
Log.e(logTag, "No text content in clipboard")
return
} else {
if (html == null) {
item = ClipData.Item(text)
} else {
item = ClipData.Item(text, html)
}
}
if (item == null) {
Log.e(logTag, "No item in clipboard")
return
}
val clipData = ClipData(clipDescription, item)
lastUpdatedClipData = clipData
clipboardManager.setPrimaryClip(clipData)
}
}

View File

@@ -5,12 +5,15 @@ package ffi
import android.content.Context
import java.nio.ByteBuffer
import com.carriez.flutter_hbb.RdClipboardManager
object FFI {
init {
System.loadLibrary("rustdesk")
}
external fun init(ctx: Context)
external fun setClipboardManager(clipboardManager: RdClipboardManager)
external fun startServer(app_dir: String, custom_client_config: String)
external fun startService()
external fun onVideoFrameUpdate(buf: ByteBuffer)
@@ -20,4 +23,5 @@ object FFI {
external fun setFrameRawEnable(name: String, value: Boolean)
external fun setCodecInfo(info: String)
external fun getLocalOption(key: String): String
}
external fun onClipboardUpdate(clips: ByteBuffer)
}

View File

@@ -1,18 +1,3 @@
buildscript {
ext.kotlin_version = '1.9.10'
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.14'
}
}
allprojects {
repositories {
google()
@@ -24,6 +9,8 @@ allprojects {
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip

View File

@@ -1,11 +1,25 @@
include ':app'
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.10" apply false
}
include ":app"

View File

@@ -82,6 +82,21 @@ export PATH="${PATH}:${HOME}/flutter/bin:${HOME}/depot_tools"
export VCPKG_ROOT="${HOME}/vcpkg"
prepare_Flutter() {
version="${1}"
pushd "${HOME}"
if [ ! -f "${HOME}/flutter/bin/flutter" ]; then
git clone https://github.com/flutter/flutter
fi
pushd flutter
git restore .
git checkout "${version}"
flutter config --no-analytics
popd # flutter
popd # ${HOME}
}
# Now act depending on build step
# NOTE: F-Droid maintainers require explicit declaration of dependencies
@@ -110,9 +125,13 @@ prebuild)
.github/workflows/flutter-build.yml)"
fi
FLUTTER_BRIDGE_VERSION="$(yq -r \
.env.FLUTTER_VERSION \
.github/workflows/bridge.yml)"
FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \
.env.FLUTTER_RUST_BRIDGE_VERSION \
.github/workflows/flutter-build.yml)"
.github/workflows/bridge.yml)"
NDK_VERSION="$(yq -r \
.env.NDK_VERSION \
@@ -127,6 +146,7 @@ prebuild)
.github/workflows/flutter-build.yml)"
if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] ||
[ -z "${FLUTTER_BRIDGE_VERSION}" ] ||
[ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] ||
[ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] ||
[ -z "${VCPKG_COMMIT_ID}" ]; then
@@ -163,24 +183,6 @@ prebuild)
sdkmanager --install "ndk;${NDK_VERSION}"
fi
# Install Flutter
if [ ! -f "${HOME}/flutter/bin/flutter" ]; then
pushd "${HOME}"
git clone https://github.com/flutter/flutter
pushd flutter
git reset --hard "${FLUTTER_VERSION}"
flutter config --no-analytics
popd # flutter
popd # ${HOME}
fi
# Install Rust
if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then
@@ -275,12 +277,46 @@ prebuild)
git apply res/fdroid/patches/*.patch
# Backup .gclient file, for later restore
cp flutter-sdk/.gclient flutter-sdk/.gclient.bak
# For FLUTTER_BRIDGE_VERSION
sed \
-i \
-e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' \
flutter/pubspec.yaml
# Install Flutter bridge version
prepare_Flutter "${FLUTTER_BRIDGE_VERSION}"
cp flutter-sdk/.gclient.bak flutter-sdk/.gclient
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_BRIDGE_VERSION}/" flutter-sdk/.gclient
# Download Flutter dependencies
pushd flutter
flutter clean && flutter packages pub get
popd # flutter
# Generate FFI bindings
flutter_rust_bridge_codegen \
--rust-input ./src/flutter_ffi.rs \
--dart-output ./flutter/lib/generated_bridge.dart
git restore flutter/pubspec.*
# Install Flutter
prepare_Flutter "${FLUTTER_VERSION}"
cp flutter-sdk/.gclient.bak flutter-sdk/.gclient
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient
# gms is not in thoes files now, but we still keep the following line for future reference(maybe).
sed \
-i \
-e '/gms/d' \
flutter/android/build.gradle \
flutter/android/app/build.gradle
# `firebase_analytics`` is not in thoes files now, but we still keep the following lines.
sed \
-i \
-e '/firebase_analytics/d' \
@@ -296,34 +332,6 @@ prebuild)
-e '/firebase/Id' \
flutter/lib/main.dart
if [ "${FLUTTER_VERSION}" = "3.13.9" ]; then
# Fix for android 3.13.9
# https://github.com/rustdesk/rustdesk/blob/285e974d1a52c891d5fcc28e963d724e085558bc/.github/workflows/flutter-build.yml#L862
sed \
-i \
-e 's/extended_text: .*/extended_text: 11.1.0/' \
-e 's/uni_links_desktop/#uni_links_desktop/g' \
flutter/pubspec.yaml
set --
while read -r _1; do
set -- "$@" "${_1}"
done 0<<.a
$(find flutter/lib/ -type f -name "*dart*")
.a
sed \
-i \
-e 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' \
"$@"
set --
fi
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient
;;
build)
# build: perform actual build of APK file
@@ -373,16 +381,10 @@ build)
pushd flutter
flutter packages pub get
flutter clean && flutter packages pub get
popd # flutter
# Generate FFI bindings
flutter_rust_bridge_codegen \
--rust-input ./src/flutter_ffi.rs \
--dart-output ./flutter/lib/generated_bridge.dart
# Build host android deps
bash flutter/build_android_deps.sh "${ANDROID_ABI}"

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>13.0</string>
</dict>
</plist>

View File

@@ -1,10 +1,7 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
platform :ios, '12.0'
platform :ios, '13.0'
project 'Runner', {
'Debug' => :debug,

View File

@@ -137,6 +137,6 @@ SPEC CHECKSUMS:
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
PODFILE CHECKSUM: 83d1b0fb6fc8613d8312a03b8e1540d37cfc5d2c
COCOAPODS: 1.15.2

View File

@@ -347,7 +347,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -491,7 +491,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -541,7 +541,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;

View File

@@ -64,6 +64,9 @@ int androidVersion = 0;
// So we need to use this flag to enable/disable resizable.
bool _linuxWindowResizable = true;
// Only used on Windows(window manager).
bool _ignoreDevicePixelRatio = true;
/// only available for Windows target
int windowsBuildNumber = 0;
DesktopType? desktopType;
@@ -1611,12 +1614,6 @@ Widget getPlatformImage(String platform, {double size = 50}) {
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
}
class OffsetDevicePixelRatio {
Offset offset;
final double devicePixelRatio;
OffsetDevicePixelRatio(this.offset, this.devicePixelRatio);
}
class LastWindowPosition {
double? width;
double? height;
@@ -1699,8 +1696,10 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
if (isFullscreen || isMaximized) {
setPreFrame();
} else {
position = await windowManager.getPosition();
sz = await windowManager.getSize();
position = await windowManager.getPosition(
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
sz = await windowManager.getSize(
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
}
break;
default:
@@ -1818,7 +1817,7 @@ bool isPointInRect(Offset point, Rect rect) {
}
/// return null means center
Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
Future<Offset?> _adjustRestoreMainWindowOffset(
double? left,
double? top,
double? width,
@@ -1832,13 +1831,9 @@ Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
double? frameTop;
double? frameRight;
double? frameBottom;
double devicePixelRatio = 1.0;
if (isDesktop || isWebDesktop) {
for (final screen in await window_size.getScreenList()) {
if (isPointInRect(Offset(left, top), screen.visibleFrame)) {
devicePixelRatio = screen.scaleFactor;
}
frameLeft = frameLeft == null
? screen.visibleFrame.left
: min(screen.visibleFrame.left, frameLeft);
@@ -1872,7 +1867,7 @@ Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
top < frameTop!) {
return null;
} else {
return OffsetDevicePixelRatio(Offset(left, top), devicePixelRatio);
return Offset(left, top);
}
}
@@ -1932,47 +1927,23 @@ Future<bool> restoreWindowPosition(WindowType type,
}
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
final offsetDevicePixelRatio = await _adjustRestoreMainWindowOffset(
final offsetLeftTop = await _adjustRestoreMainWindowOffset(
lpos.offsetWidth,
lpos.offsetHeight,
size.width,
size.height,
);
debugPrint(
"restore lpos: ${size.width}/${size.height}, offset:${offsetDevicePixelRatio?.offset.dx}/${offsetDevicePixelRatio?.offset.dy}, devicePixelRatio:${offsetDevicePixelRatio?.devicePixelRatio}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
"restore lpos: ${size.width}/${size.height}, offset:${offsetLeftTop?.dx}/${offsetLeftTop?.dy}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
switch (type) {
case WindowType.Main:
// https://github.com/rustdesk/rustdesk/issues/8038
// `setBounds()` in `window_manager` will use the current devicePixelRatio.
// So we need to adjust the offset by the scale factor.
// https://github.com/rustdesk-org/window_manager/blob/f19acdb008645366339444a359a45c3257c8b32e/windows/window_manager.cpp#L701
if (isWindows) {
double? curDevicePixelRatio;
Offset curPos = await windowManager.getPosition();
for (final screen in await window_size.getScreenList()) {
if (isPointInRect(curPos, screen.visibleFrame)) {
curDevicePixelRatio = screen.scaleFactor;
}
}
if (curDevicePixelRatio != null &&
curDevicePixelRatio != 0 &&
offsetDevicePixelRatio != null) {
if (offsetDevicePixelRatio.devicePixelRatio != 0) {
final scale =
offsetDevicePixelRatio.devicePixelRatio / curDevicePixelRatio;
offsetDevicePixelRatio.offset =
offsetDevicePixelRatio.offset.scale(scale, scale);
debugPrint(
"restore new offset: ${offsetDevicePixelRatio.offset.dx}/${offsetDevicePixelRatio.offset.dy}, scale:$scale");
}
}
}
restorePos() async {
if (offsetDevicePixelRatio == null) {
if (offsetLeftTop == null) {
await windowManager.center();
} else {
await windowManager.setPosition(offsetDevicePixelRatio.offset);
await windowManager.setPosition(offsetLeftTop,
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
}
}
if (lpos.isMaximized == true) {
@@ -1981,20 +1952,39 @@ Future<bool> restoreWindowPosition(WindowType type,
await windowManager.maximize();
}
} else {
if (!bind.isIncomingOnly() || bind.isOutgoingOnly()) {
await windowManager.setSize(size);
final storeSize = !bind.isIncomingOnly() || bind.isOutgoingOnly();
if (isWindows) {
if (storeSize) {
// We need to set the window size first to avoid the incorrect size in some special cases.
// E.g. There are two monitors, the left one is 100% DPI and the right one is 175% DPI.
// The window belongs to the left monitor, but if it is moved a little to the right, it will belong to the right monitor.
// After restoring, the size will be incorrect.
// See known issue in https://github.com/rustdesk/rustdesk/pull/9840
await windowManager.setSize(size,
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
}
await restorePos();
if (storeSize) {
await windowManager.setSize(size,
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
}
} else {
if (storeSize) {
await windowManager.setSize(size,
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
}
await restorePos();
}
await restorePos();
}
return true;
default:
final wc = WindowController.fromWindowId(windowId!);
restoreFrame() async {
if (offsetDevicePixelRatio == null) {
if (offsetLeftTop == null) {
await wc.center();
} else {
final frame = Rect.fromLTWH(offsetDevicePixelRatio.offset.dx,
offsetDevicePixelRatio.offset.dy, size.width, size.height);
final frame = Rect.fromLTWH(
offsetLeftTop.dx, offsetLeftTop.dy, size.width, size.height);
await wc.setFrame(frame);
}
}
@@ -2255,7 +2245,10 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
}
}
var key = uri.queryParameters["key"];
var queryParameters =
uri.queryParameters.map((k, v) => MapEntry(k.toLowerCase(), v));
var key = queryParameters["key"];
if (id != null) {
if (key != null) {
id = "$id?key=$key";
@@ -2264,7 +2257,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
if (isMobile) {
if (id != null) {
final forceRelay = uri.queryParameters["relay"] != null;
final forceRelay = queryParameters["relay"] != null;
connect(Get.context!, id, forceRelay: forceRelay);
return null;
}
@@ -2274,7 +2267,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
if (command != null && id != null) {
args.add(command);
args.add(id);
var param = uri.queryParameters;
var param = queryParameters;
String? password = param["password"];
if (password != null) args.addAll(['--password', password]);
String? switch_uuid = param["switch_uuid"];
@@ -2508,9 +2501,20 @@ Future<void> onActiveWindowChanged() async {
// But the app will not close.
//
// No idea why we need to delay here, `terminate()` itself is also an async function.
Future.delayed(Duration.zero, () {
RdPlatformChannel.instance.terminate();
});
//
// A quick workaround, use `Timer.periodic` to avoid the app not closing.
// Because `await windowManager.close()` and `RdPlatformChannel.instance.terminate()`
// may not work since `Flutter 3.24.4`, see the following logs.
// A delay will allow the app to close.
//
//```
// embedder.cc (2725): 'FlutterPlatformMessageCreateResponseHandle' returned 'kInvalidArguments'. Engine handle was invalid.
// 2024-11-11 11:41:11.546 RustDesk[90272:2567686] Failed to create a FlutterPlatformMessageResponseHandle (2)
// embedder.cc (2672): 'FlutterEngineSendPlatformMessage' returned 'kInvalidArguments'. Invalid engine handle.
// 2024-11-11 11:41:11.565 RustDesk[90272:2567686] Failed to send message to Flutter engine on channel 'flutter/lifecycle' (2).
// ```
periodic_immediate(
Duration(milliseconds: 30), RdPlatformChannel.instance.terminate);
}
}
}
@@ -2603,7 +2607,7 @@ class ServerConfig {
config['relay'] = relayServer.trim();
config['api'] = apiServer.trim();
config['key'] = key.trim();
return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits))
return base64UrlEncode(Uint8List.fromList(jsonEncode(config).codeUnits))
.split('')
.reversed
.join();

View File

@@ -1,5 +1,6 @@
import 'dart:math';
import 'package:bot_toast/bot_toast.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/material.dart';
@@ -316,13 +317,14 @@ class _AddressBookState extends State<AddressBook> {
Widget _buildTags() {
return Obx(() {
final List tags;
List tags;
if (gFFI.abModel.sortTags.value) {
tags = gFFI.abModel.currentAbTags.toList();
tags.sort();
} else {
tags = gFFI.abModel.currentAbTags;
tags = gFFI.abModel.currentAbTags.toList();
}
tags = [kUntagged, ...tags].toList();
final editPermission = gFFI.abModel.current.canWrite();
tagBuilder(String e) {
return AddressBookTag(
@@ -669,6 +671,14 @@ class _AddressBookState extends State<AddressBook> {
} else {
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
field = tags.join(',');
for (var t in [kUntagged, translate(kUntagged)]) {
if (tags.contains(t)) {
BotToast.showText(
contentColor: Colors.red, text: 'Tag name cannot be "$t"');
isInProgress = false;
return;
}
}
gFFI.abModel.addTags(tags);
// final currentPeers
}
@@ -741,12 +751,14 @@ class AddressBookTag extends StatelessWidget {
}
const double radius = 8;
final isUnTagged = name == kUntagged;
final showAction = showActionMenu && !isUnTagged;
return GestureDetector(
onTap: onTap,
onTapDown: showActionMenu ? setPosition : null,
onSecondaryTapDown: showActionMenu ? setPosition : null,
onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null,
onLongPress: showActionMenu ? () => _showMenu(context, pos) : null,
onTapDown: showAction ? setPosition : null,
onSecondaryTapDown: showAction ? setPosition : null,
onSecondaryTap: showAction ? () => _showMenu(context, pos) : null,
onLongPress: showAction ? () => _showMenu(context, pos) : null,
child: Obx(() => Container(
decoration: BoxDecoration(
color: tags.contains(name)
@@ -758,17 +770,18 @@ class AddressBookTag extends StatelessWidget {
child: IntrinsicWidth(
child: Row(
children: [
Container(
width: radius,
height: radius,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: tags.contains(name)
? Colors.white
: gFFI.abModel.getCurrentAbTagColor(name)),
).marginOnly(right: radius / 2),
if (!isUnTagged)
Container(
width: radius,
height: radius,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: tags.contains(name)
? Colors.white
: gFFI.abModel.getCurrentAbTagColor(name)),
).marginOnly(right: radius / 2),
Expanded(
child: Text(name,
child: Text(isUnTagged ? translate(name) : name,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: tags.contains(name) ? Colors.white : null)),

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
const _kWindowsSystemSound = 'System Sound';
const _kSystemSound = 'System Sound';
typedef AudioINputSetDevice = void Function(String device);
typedef AudioInputBuilder = Widget Function(
@@ -21,7 +21,7 @@ class AudioInput extends StatelessWidget {
: super(key: key);
static String getDefault() {
if (isWindows) return translate('System Sound');
if (bind.mainAudioSupportLoopback()) return translate(_kSystemSound);
return '';
}
@@ -55,8 +55,8 @@ class AudioInput extends StatelessWidget {
static Future<Map<String, Object>> getDevicesInfo(
bool isCm, bool isVoiceCall) async {
List<String> devices = (await bind.mainGetSoundInputs()).toList();
if (isWindows) {
devices.insert(0, translate(_kWindowsSystemSound));
if (bind.mainAudioSupportLoopback()) {
devices.insert(0, translate(_kSystemSound));
}
String current = await getValue(isCm, isVoiceCall);
return {'devices': devices, 'current': current};

View File

@@ -86,7 +86,7 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
// end
switch (_currentState) {
case GestureState.oneFingerPan:
debugPrint("TwoFingerState.pan onEnd");
debugPrint("OneFingerState.pan onEnd");
if (onOneFingerPanEnd != null) {
onOneFingerPanEnd!(_getDragEndDetails(d));
}

View File

@@ -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/ab_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
@@ -532,15 +533,22 @@ class AddressBookPeersView extends BasePeersView {
if (selectedTags.isEmpty) {
return true;
}
// The result of a no-tag union with normal tags, still allows normal tags to perform union or intersection operations.
final selectedNormalTags =
selectedTags.where((tag) => tag != kUntagged).toList();
if (selectedTags.contains(kUntagged)) {
if (idents.isEmpty) return true;
if (selectedNormalTags.isEmpty) return false;
}
if (gFFI.abModel.filterByIntersection.value) {
for (final tag in selectedTags) {
for (final tag in selectedNormalTags) {
if (!idents.contains(tag)) {
return false;
}
}
return true;
} else {
for (final tag in selectedTags) {
for (final tag in selectedNormalTags) {
if (idents.contains(tag)) {
return true;
}

View File

@@ -84,8 +84,17 @@ class _RawTouchGestureDetectorRegionState
double _mouseScrollIntegral = 0; // mouse scroll speed controller
double _scale = 1;
// Workaround tap down event when two fingers are used to scale(mobile)
TapDownDetails? _lastTapDownDetails;
PointerDeviceKind? lastDeviceKind;
// For touch mode, onDoubleTap
// `onDoubleTap()` does not provide the position of the tap event.
Offset _lastPosOfDoubleTapDown = Offset.zero;
bool _touchModePanStarted = false;
Offset _doubleFinerTapPosition = Offset.zero;
FFI get ffi => widget.ffi;
FfiModel get ffiModel => widget.ffiModel;
InputModel get inputModel => widget.inputModel;
@@ -100,152 +109,190 @@ class _RawTouchGestureDetectorRegionState
);
}
onTapDown(TapDownDetails d) {
onTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
_lastPosOfDoubleTapDown = d.localPosition;
// Desktop or mobile "Touch mode"
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
inputModel.tapDown(MouseButtons.left);
}
_lastTapDownDetails = d;
}
}
onTapUp(TapUpDetails d) {
onTapUp(TapUpDetails d) async {
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
_lastTapDownDetails = null;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
inputModel.tapUp(MouseButtons.left);
final isMoved =
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
if (isMoved) {
if (lastTapDownDetails != null) {
await inputModel.tapDown(MouseButtons.left);
}
await inputModel.tapUp(MouseButtons.left);
}
}
}
onTap() {
onTap() async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (!handleTouch) {
// Mobile, "Mouse mode"
inputModel.tap(MouseButtons.left);
await inputModel.tap(MouseButtons.left);
}
}
onDoubleTapDown(TapDownDetails d) {
onDoubleTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
_lastPosOfDoubleTapDown = d.localPosition;
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
}
}
onDoubleTap() {
onDoubleTap() async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
return;
}
inputModel.tap(MouseButtons.left);
inputModel.tap(MouseButtons.left);
if (handleTouch &&
!ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) {
return;
}
await inputModel.tap(MouseButtons.left);
await inputModel.tap(MouseButtons.left);
}
onLongPressDown(LongPressDownDetails d) {
onLongPressDown(LongPressDownDetails d) async {
lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
_lastPosOfDoubleTapDown = d.localPosition;
_cacheLongPressPosition = d.localPosition;
if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
return;
}
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
}
}
onLongPressUp() {
onLongPressUp() async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
inputModel.tapUp(MouseButtons.left);
await inputModel.tapUp(MouseButtons.left);
}
}
// for mobiles
onLongPress() {
onLongPress() async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
ffi.cursorModel
final isMoved = await ffi.cursorModel
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
if (!isMoved) {
return;
}
}
if (!ffi.ffiModel.isPeerMobile) {
await inputModel.tap(MouseButtons.right);
}
inputModel.tap(MouseButtons.right);
}
onDoubleFinerTapDown(TapDownDetails d) {
onDoubleFinerTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
_doubleFinerTapPosition = d.localPosition;
// ignore for desktop and mobile
}
onDoubleFinerTap(TapDownDetails d) {
onDoubleFinerTap(TapDownDetails d) async {
lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if ((isDesktop || isWebDesktop) || !ffiModel.touchMode) {
inputModel.tap(MouseButtons.right);
// mobile mouse mode or desktop touch screen
final isMobileMouseMode = isMobile && !ffiModel.touchMode;
// We can't use `d.localPosition` here because it's always (0, 0) on desktop.
final isDesktopInRemoteRect = (isDesktop || isWebDesktop) &&
ffi.cursorModel.isInRemoteRect(_doubleFinerTapPosition);
if (isMobileMouseMode || isDesktopInRemoteRect) {
await inputModel.tap(MouseButtons.right);
}
}
onHoldDragStart(DragStartDetails d) {
onHoldDragStart(DragStartDetails d) async {
lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (!handleTouch) {
inputModel.sendMouse('down', MouseButtons.left);
await inputModel.sendMouse('down', MouseButtons.left);
}
}
onHoldDragUpdate(DragUpdateDetails d) {
onHoldDragUpdate(DragUpdateDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (!handleTouch) {
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
}
}
onHoldDragEnd(DragEndDetails d) {
onHoldDragEnd(DragEndDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (!handleTouch) {
inputModel.sendMouse('up', MouseButtons.left);
await inputModel.sendMouse('up', MouseButtons.left);
}
}
onOneFingerPanStart(BuildContext context, DragStartDetails d) {
onOneFingerPanStart(BuildContext context, DragStartDetails d) async {
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
_lastTapDownDetails = null;
lastDeviceKind = d.kind ?? lastDeviceKind;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (handleTouch) {
if (lastTapDownDetails != null) {
await ffi.cursorModel.move(lastTapDownDetails.localPosition.dx,
lastTapDownDetails.localPosition.dy);
}
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return;
}
if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
return;
}
_touchModePanStarted = true;
if (isDesktop || isWebDesktop) {
ffi.cursorModel.trySetRemoteWindowCoords();
}
// Workaround for the issue that the first pan event is sent a long time after the start event.
// If the time interval between the start event and the first pan event is less than 500ms,
// we consider to use the long press position as the start position.
@@ -253,11 +300,11 @@ class _RawTouchGestureDetectorRegionState
// TODO: We should find a better way to send the first pan event as soon as possible.
if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs <
500) {
ffi.cursorModel
await ffi.cursorModel
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
}
inputModel.sendMouse('down', MouseButtons.left);
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
await inputModel.sendMouse('down', MouseButtons.left);
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
} else {
final offset = ffi.cursorModel.offset;
final cursorX = offset.dx;
@@ -266,39 +313,44 @@ class _RawTouchGestureDetectorRegionState
ffi.cursorModel.getVisibleRect().inflate(1); // extend edges
final size = MediaQueryData.fromView(View.of(context)).size;
if (!visible.contains(Offset(cursorX, cursorY))) {
ffi.cursorModel.move(size.width / 2, size.height / 2);
await ffi.cursorModel.move(size.width / 2, size.height / 2);
}
}
}
onOneFingerPanUpdate(DragUpdateDetails d) {
onOneFingerPanUpdate(DragUpdateDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return;
}
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
if (handleTouch && !_touchModePanStarted) {
return;
}
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
}
onOneFingerPanEnd(DragEndDetails d) {
onOneFingerPanEnd(DragEndDetails d) async {
_touchModePanStarted = false;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (isDesktop || isWebDesktop) {
ffi.cursorModel.clearRemoteWindowCoords();
}
inputModel.sendMouse('up', MouseButtons.left);
await inputModel.sendMouse('up', MouseButtons.left);
}
// scale + pan event
onTwoFingerScaleStart(ScaleStartDetails d) {
_lastTapDownDetails = null;
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
}
onTwoFingerScaleUpdate(ScaleUpdateDetails d) {
onTwoFingerScaleUpdate(ScaleUpdateDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
@@ -307,7 +359,7 @@ class _RawTouchGestureDetectorRegionState
_scale = d.scale;
if (scale != 0) {
bind.sessionSendPointer(
await bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode(
PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
@@ -322,21 +374,22 @@ class _RawTouchGestureDetectorRegionState
}
}
onTwoFingerScaleEnd(ScaleEndDetails d) {
onTwoFingerScaleEnd(ScaleEndDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if ((isDesktop || isWebDesktop)) {
bind.sessionSendPointer(
await bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode(
PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
} else {
// mobile
_scale = 1;
bind.sessionSetViewStyle(sessionId: sessionId, value: "");
// No idea why we need to set the view style to "" here.
// bind.sessionSetViewStyle(sessionId: sessionId, value: "");
}
inputModel.sendMouse('up', MouseButtons.left);
await inputModel.sendMouse('up', MouseButtons.left);
}
get onHoldDragCancel => null;

View File

@@ -169,6 +169,13 @@ const int kWindowMainId = 0;
const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse";
const String kMouseEventTypeDefault = "";
const String kMouseEventTypePanStart = "pan_start";
const String kMouseEventTypePanUpdate = "pan_update";
const String kMouseEventTypePanEnd = "pan_end";
const String kMouseEventTypeDown = "down";
const String kMouseEventTypeUp = "up";
const String kKeyFlutterKey = "flutter_key";
const String kKeyShowDisplaysAsIndividualWindows =
@@ -202,7 +209,7 @@ const double kMinFps = 5;
const double kDefaultFps = 30;
const double kMaxFps = 120;
const double kMinQuality = 5;
const double kMinQuality = 10;
const double kDefaultQuality = 50;
const double kMaxQuality = 100;
const double kMaxMoreQuality = 2000;
@@ -572,4 +579,4 @@ extension WindowsTargetExt on int {
WindowsTarget get windowsVersion => getWindowsTarget(this);
}
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';

View File

@@ -203,6 +203,8 @@ class _ConnectionPageState extends State<ConnectionPage>
bool isPeersLoading = false;
bool isPeersLoaded = false;
// https://github.com/flutter/flutter/issues/157244
Iterable<Peer> _autocompleteOpts = [];
@override
void initState() {
@@ -330,7 +332,7 @@ class _ConnectionPageState extends State<ConnectionPage>
child: Autocomplete<Peer>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return const Iterable<Peer>.empty();
_autocompleteOpts = const Iterable<Peer>.empty();
} else if (peers.isEmpty && !isPeersLoaded) {
Peer emptyPeer = Peer(
id: '',
@@ -346,7 +348,7 @@ class _ConnectionPageState extends State<ConnectionPage>
rdpUsername: '',
loginName: '',
);
return [emptyPeer];
_autocompleteOpts = [emptyPeer];
} else {
String textWithoutSpaces =
textEditingValue.text.replaceAll(" ", "");
@@ -357,8 +359,7 @@ class _ConnectionPageState extends State<ConnectionPage>
);
}
String textToFind = textEditingValue.text.toLowerCase();
return peers
_autocompleteOpts = peers
.where((peer) =>
peer.id.toLowerCase().contains(textToFind) ||
peer.username
@@ -370,6 +371,7 @@ class _ConnectionPageState extends State<ConnectionPage>
peer.alias.toLowerCase().contains(textToFind))
.toList();
}
return _autocompleteOpts;
},
fieldViewBuilder: (
BuildContext context,
@@ -430,6 +432,7 @@ class _ConnectionPageState extends State<ConnectionPage>
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<Peer> onSelected,
Iterable<Peer> options) {
options = _autocompleteOpts;
double maxHeight = options.length * 50;
if (options.length == 1) {
maxHeight = 52;

View File

@@ -272,10 +272,21 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}
buildPasswordBoard(BuildContext context) {
final model = gFFI.serverModel;
return ChangeNotifierProvider.value(
value: gFFI.serverModel,
child: Consumer<ServerModel>(
builder: (context, model, child) {
return buildPasswordBoard2(context, model);
},
));
}
buildPasswordBoard2(BuildContext context, ServerModel model) {
RxBool refreshHover = false.obs;
RxBool editHover = false.obs;
final textColor = Theme.of(context).textTheme.titleLarge?.color;
final showOneTime = model.approveMode != 'click' &&
model.verificationMethod != kUsePermanentPassword;
return Container(
margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13),
child: Row(
@@ -304,8 +315,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Expanded(
child: GestureDetector(
onDoubleTap: () {
if (model.verificationMethod !=
kUsePermanentPassword) {
if (showOneTime) {
Clipboard.setData(
ClipboardData(text: model.serverPasswd.text));
showToast(translate("Copied"));
@@ -323,22 +333,23 @@ class _DesktopHomePageState extends State<DesktopHomePage>
),
),
),
AnimatedRotationWidget(
onPressed: () => bind.mainUpdateTemporaryPassword(),
child: Tooltip(
message: translate('Refresh Password'),
child: Obx(() => RotatedBox(
quarterTurns: 2,
child: Icon(
Icons.refresh,
color: refreshHover.value
? textColor
: Color(0xFFDDDDDD),
size: 22,
))),
),
onHover: (value) => refreshHover.value = value,
).marginOnly(right: 8, top: 4),
if (showOneTime)
AnimatedRotationWidget(
onPressed: () => bind.mainUpdateTemporaryPassword(),
child: Tooltip(
message: translate('Refresh Password'),
child: Obx(() => RotatedBox(
quarterTurns: 2,
child: Icon(
Icons.refresh,
color: refreshHover.value
? textColor
: Color(0xFFDDDDDD),
size: 22,
))),
),
onHover: (value) => refreshHover.value = value,
).marginOnly(right: 8, top: 4),
if (!bind.isDisableSettings())
InkWell(
child: Tooltip(

View File

@@ -1029,7 +1029,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
translate('Accept sessions via both'),
];
var modeInitialKey = model.approveMode;
if (!modeKeys.contains(modeInitialKey)) modeInitialKey = '';
if (!modeKeys.contains(modeInitialKey)) {
modeInitialKey = defaultOptionApproveMode;
}
final usePassword = model.approveMode != 'click';
final isApproveModeFixed = isOptionFixed(kOptionApproveMode);

View File

@@ -48,6 +48,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
bool isPeersLoaded = false;
StreamSubscription? _uniLinksSubscription;
// https://github.com/flutter/flutter/issues/157244
Iterable<Peer> _autocompleteOpts = [];
_ConnectionPageState() {
if (!isWeb) _uniLinksSubscription = listenUniLinks();
_idController.addListener(() {
@@ -166,7 +169,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
child: Autocomplete<Peer>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return const Iterable<Peer>.empty();
_autocompleteOpts = const Iterable<Peer>.empty();
} else if (peers.isEmpty && !isPeersLoaded) {
Peer emptyPeer = Peer(
id: '',
@@ -182,7 +185,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
rdpUsername: '',
loginName: '',
);
return [emptyPeer];
_autocompleteOpts = [emptyPeer];
} else {
String textWithoutSpaces =
textEditingValue.text.replaceAll(" ", "");
@@ -194,7 +197,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
}
String textToFind = textEditingValue.text.toLowerCase();
return peers
_autocompleteOpts = peers
.where((peer) =>
peer.id.toLowerCase().contains(textToFind) ||
peer.username
@@ -206,6 +209,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
peer.alias.toLowerCase().contains(textToFind))
.toList();
}
return _autocompleteOpts;
},
fieldViewBuilder: (BuildContext context,
TextEditingController fieldTextEditingController,
@@ -274,6 +278,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<Peer> onSelected,
Iterable<Peer> options) {
options = _autocompleteOpts;
double maxHeight = options.length * 50;
if (options.length == 1) {
maxHeight = 52;

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -25,6 +26,19 @@ import '../widgets/dialog.dart';
final initText = '1' * 1024;
// Workaround for Android (default input method, Microsoft SwiftKey keyboard) when using physical keyboard.
// When connecting a physical keyboard, `KeyEvent.physicalKey.usbHidUsage` are wrong is using Microsoft SwiftKey keyboard.
// https://github.com/flutter/flutter/issues/159384
// https://github.com/flutter/flutter/issues/159383
void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
if (isAndroid) {
if (isKeyboardVisible != true) {
// `enable_soft_keyboard` will be set to `true` when clicking the keyboard icon, in `openKeyboard()`.
gFFI.invokeMethod("enable_soft_keyboard", false);
}
}
}
class RemotePage extends StatefulWidget {
RemotePage({Key? key, required this.id, this.password, this.isSharedPassword})
: super(key: key);
@@ -37,12 +51,15 @@ class RemotePage extends StatefulWidget {
State<RemotePage> createState() => _RemotePageState(id);
}
class _RemotePageState extends State<RemotePage> {
class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
Timer? _timer;
bool _showBar = !isWebDesktop;
bool _showGestureHelp = false;
String _value = '';
Orientation? _currentOrientation;
double _viewInsetsBottom = 0;
Timer? _timerDidChangeMetrics;
final _blockableOverlayState = BlockableOverlayState();
@@ -57,9 +74,6 @@ 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);
@@ -98,11 +112,15 @@ class _RemotePageState extends State<RemotePage> {
if (gFFI.recordingModel.start) {
showToast(translate('Automatically record outgoing sessions'));
}
_disableAndroidSoftKeyboard(
isKeyboardVisible: keyboardVisibilityController.isVisible);
});
WidgetsBinding.instance.addObserver(this);
}
@override
Future<void> dispose() async {
WidgetsBinding.instance.removeObserver(this);
// https://github.com/flutter/flutter/issues/64935
super.dispose();
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
@@ -114,7 +132,7 @@ class _RemotePageState extends State<RemotePage> {
_physicalFocusNode.dispose();
await gFFI.close();
_timer?.cancel();
_composingTimer?.cancel();
_timerDidChangeMetrics?.cancel();
gFFI.dialogManager.dismissAll();
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
@@ -129,6 +147,26 @@ class _RemotePageState extends State<RemotePage> {
gFFI.chatModel.onVoiceCallClosed("End connetion");
}
@override
void didChangeMetrics() {
// If the soft keyboard is visible and the canvas has been changed(panned or scaled)
// Don't try reset the view style and focus the cursor.
if (gFFI.cursorModel.lastKeyboardIsVisible &&
gFFI.canvasModel.isMobileCanvasChanged) {
return;
}
final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom;
_timerDidChangeMetrics?.cancel();
_timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async {
// We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`.
if (newBottom != _viewInsetsBottom) {
gFFI.canvasModel.mobileFocusCanvasCursor();
_viewInsetsBottom = newBottom;
}
});
}
// to-do: It should be better to use transparent color instead of the bgColor.
// But for now, the transparent color will cause the canvas to be white.
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
@@ -150,7 +188,6 @@ class _RemotePageState extends State<RemotePage> {
gFFI.ffiModel.pi.version.isNotEmpty) {
gFFI.invokeMethod("enable_soft_keyboard", false);
}
_composingTimer?.cancel();
} else {
_timer?.cancel();
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
@@ -214,13 +251,6 @@ 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 &&
@@ -261,14 +291,10 @@ class _RemotePageState extends State<RemotePage> {
}
}
Future<void> handleSoftKeyboardInput(String newValue) async {
// handle mobile virtual keyboard
void handleSoftKeyboardInput(String newValue) {
if (isIOS) {
// 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);
_handleIOSSoftKeyboardInput(newValue);
} else {
_handleNonIOSSoftKeyboardInput(newValue);
}
@@ -534,7 +560,9 @@ class _RemotePageState extends State<RemotePage> {
right: 10,
child: QualityMonitor(gFFI.qualityMonitorModel),
),
KeyHelpTools(requestShow: (keyboardIsVisible || _showGestureHelp)),
KeyHelpTools(
keyboardIsVisible: keyboardIsVisible,
showGestureHelp: _showGestureHelp),
SizedBox(
width: 0,
height: 0,
@@ -554,6 +582,14 @@ class _RemotePageState extends State<RemotePage> {
controller: _textController,
// trick way to make backspace work always
keyboardType: TextInputType.multiline,
// `onChanged` may be called depending on the input method if this widget is wrapped in
// `Focus(onKeyEvent: ..., child: ...)`
// For `Backspace` button in the soft keyboard:
// en/fr input method:
// 1. The button will not trigger `onKeyEvent` if the text field is not empty.
// 2. The button will trigger `onKeyEvent` if the text field is empty.
// ko/zh/ja input method: the button will trigger `onKeyEvent`
// and the event will not popup if `KeyEventResult.handled` is returned.
onChanged: handleSoftKeyboardInput,
),
),
@@ -763,10 +799,14 @@ class _RemotePageState extends State<RemotePage> {
}
class KeyHelpTools extends StatefulWidget {
/// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode]
final bool requestShow;
final bool keyboardIsVisible;
final bool showGestureHelp;
KeyHelpTools({required this.requestShow});
/// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode]
bool get requestShow => keyboardIsVisible || showGestureHelp;
KeyHelpTools(
{required this.keyboardIsVisible, required this.showGestureHelp});
@override
State<KeyHelpTools> createState() => _KeyHelpToolsState();
@@ -811,7 +851,8 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
final size = renderObject.size;
Offset pos = renderObject.localToGlobal(Offset.zero);
gFFI.cursorModel.keyHelpToolsVisibilityChanged(
Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height));
Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height),
widget.keyboardIsVisible);
}
}
@@ -823,7 +864,8 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
inputModel.command;
if (!_pin && !hasModifierOn && !widget.requestShow) {
gFFI.cursorModel.keyHelpToolsVisibilityChanged(null);
gFFI.cursorModel
.keyHelpToolsVisibilityChanged(null, widget.keyboardIsVisible);
return Offstage();
}
final size = MediaQuery.of(context).size;
@@ -960,11 +1002,11 @@ class ImagePaint extends StatelessWidget {
Widget build(BuildContext context) {
final m = Provider.of<ImageModel>(context);
final c = Provider.of<CanvasModel>(context);
final adjust = gFFI.cursorModel.adjustForKeyboard();
var s = c.scale;
final adjust = c.getAdjustY();
return CustomPaint(
painter: ImagePainter(
image: m.image, x: c.x / s, y: (c.y - adjust) / s, scale: s),
image: m.image, x: c.x / s, y: (c.y + adjust) / s, scale: s),
);
}
}
@@ -978,7 +1020,6 @@ class CursorPaint extends StatelessWidget {
final m = Provider.of<CursorModel>(context);
final c = Provider.of<CanvasModel>(context);
final ffiModel = Provider.of<FfiModel>(context);
final adjust = gFFI.cursorModel.adjustForKeyboard();
final s = c.scale;
double hotx = m.hotx;
double hoty = m.hoty;
@@ -1010,11 +1051,12 @@ class CursorPaint extends StatelessWidget {
factor = s / mins;
}
final s2 = s < mins ? mins : s;
final adjust = c.getAdjustY();
return CustomPaint(
painter: ImagePainter(
image: image,
x: (m.x - hotx) * factor + c.x / s2,
y: (m.y - hoty) * factor + (c.y - adjust) / s2,
y: (m.y - hoty) * factor + (c.y + adjust) / s2,
scale: s2),
);
}
@@ -1217,7 +1259,9 @@ void showOptions(
toggles +
[privacyModeWidget]),
);
}, clickMaskDismiss: true, backDismiss: true);
}, clickMaskDismiss: true, backDismiss: true).then((value) {
_disableAndroidSoftKeyboard();
});
}
TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
@@ -1236,7 +1280,9 @@ TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
children: children,
),
);
}, clickMaskDismiss: true, backDismiss: true);
}, clickMaskDismiss: true, backDismiss: true).then((value) {
_disableAndroidSoftKeyboard();
});
},
);
}
@@ -1278,7 +1324,9 @@ TTextMenu? getResolutionMenu(FFI ffi, String id) {
children: children,
),
);
}, clickMaskDismiss: true, backDismiss: true);
}, clickMaskDismiss: true, backDismiss: true).then((value) {
_disableAndroidSoftKeyboard();
});
},
);
}

View File

@@ -446,7 +446,6 @@ class ServerInfo extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isPermanent = model.verificationMethod == kUsePermanentPassword;
final serverModel = Provider.of<ServerModel>(context);
const Color colorPositive = Colors.green;
@@ -486,6 +485,8 @@ class ServerInfo extends StatelessWidget {
}
}
final showOneTime = serverModel.approveMode != 'click' &&
serverModel.verificationMethod != kUsePermanentPassword;
return PaddingCard(
title: translate('Your Device'),
child: Column(
@@ -523,10 +524,10 @@ class ServerInfo extends StatelessWidget {
]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text(
isPermanent ? '-' : model.serverPasswd.value.text,
!showOneTime ? '-' : model.serverPasswd.value.text,
style: textStyleValue,
),
isPermanent
!showOneTime
? SizedBox.shrink()
: Row(children: [
IconButton(
@@ -595,7 +596,9 @@ class _PermissionCheckerState extends State<PermissionChecker> {
translate("android_version_audio_tip"),
style: const TextStyle(color: MyTheme.darkGray),
))
])
]),
PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk,
serverModel.toggleClipboard),
]));
}
}

View File

@@ -33,6 +33,8 @@ bool filterAbTagByIntersection() {
const _personalAddressBookName = "My address book";
const _legacyAddressBookName = "Legacy address book";
const kUntagged = "Untagged";
enum ForcePullAb {
listAndCurrent,
current,
@@ -424,6 +426,7 @@ class AbModel {
// #region tags
Future<bool> addTags(List<String> tagList) async {
tagList.removeWhere((e) => e == kUntagged);
final ret = await current.addTags(tagList, {});
await pullNonLegacyAfterChange();
_saveCache();

View File

@@ -100,6 +100,10 @@ class FileModel {
fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
}
void receiveEmptyDirs(Map<String, dynamic> evt) {
fileFetcher.tryCompleteEmptyDirsTask(evt['value'], evt['is_local']);
}
Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
evtLoop.pushEvent(
_FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
@@ -470,7 +474,8 @@ class FileController {
}
/// sendFiles from current side (FileController.isLocal) to other side (SelectedItems).
void sendFiles(SelectedItems items, DirectoryData otherSideData) {
Future<void> sendFiles(
SelectedItems items, DirectoryData otherSideData) async {
/// ignore wrong items side status
if (items.isLocal != isLocal) {
return;
@@ -496,6 +501,42 @@ class FileController {
debugPrint(
"path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}");
}
if (!isLocal &&
versionCmp(rootState.target!.ffiModel.pi.version, '1.3.3') < 0) {
return;
}
final List<Entry> entrys = items.items.toList();
var isRemote = isLocal == true ? true : false;
await Future.forEach(entrys, (Entry item) async {
if (!item.isDirectory) {
return;
}
final List<String> paths = [];
final emptyDirs =
await fileFetcher.readEmptyDirs(item.path, isLocal, showHidden);
if (emptyDirs.isEmpty) {
return;
} else {
for (var dir in emptyDirs) {
paths.add(dir.path);
}
}
final dirs = paths.map((path) {
return PathUtil.getOtherSidePath(directory.value.path, path,
options.value.isWindows, toPath, isWindows);
});
for (var dir in dirs) {
createDirWithRemote(dir, isRemote);
}
});
}
bool _removeCheckboxRemember = false;
@@ -689,12 +730,16 @@ class FileController {
sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
}
Future<void> createDir(String path) async {
Future<void> createDirWithRemote(String path, bool isRemote) async {
bind.sessionCreateDir(
sessionId: sessionId,
actId: JobController.jobID.next(),
path: path,
isRemote: !isLocal);
isRemote: isRemote);
}
Future<void> createDir(String path) async {
await createDirWithRemote(path, !isLocal);
}
Future<void> renameAction(Entry item, bool isLocal) async {
@@ -1064,6 +1109,7 @@ class JobResultListener<T> {
class FileFetcher {
// Map<String,Completer<FileDirectory>> localTasks = {}; // now we only use read local dir sync
Map<String, Completer<FileDirectory>> remoteTasks = {};
Map<String, Completer<List<FileDirectory>>> remoteEmptyDirsTasks = {};
Map<int, Completer<FileDirectory>> readRecursiveTasks = {};
final GetSessionID getSessionID;
@@ -1071,6 +1117,24 @@ class FileFetcher {
FileFetcher(this.getSessionID);
Future<List<FileDirectory>> registerReadEmptyDirsTask(
bool isLocal, String path) {
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
final tasks = remoteEmptyDirsTasks; // bypass now
if (tasks.containsKey(path)) {
throw "Failed to registerReadEmptyDirsTask, already have same read job";
}
final c = Completer<List<FileDirectory>>();
tasks[path] = c;
Timer(Duration(seconds: 2), () {
tasks.remove(path);
if (c.isCompleted) return;
c.completeError("Failed to read empty dirs, timeout");
});
return c.future;
}
Future<FileDirectory> registerReadTask(bool isLocal, String path) {
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
final tasks = remoteTasks; // bypass now
@@ -1104,6 +1168,25 @@ class FileFetcher {
return c.future;
}
tryCompleteEmptyDirsTask(String? msg, String? isLocalStr) {
if (msg == null || isLocalStr == null) return;
late final Map<String, Completer<List<FileDirectory>>> tasks;
try {
final map = jsonDecode(msg);
final String path = map["path"];
final List<dynamic> fdJsons = map["empty_dirs"];
final List<FileDirectory> fds =
fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList();
tasks = remoteEmptyDirsTasks;
final completer = tasks.remove(path);
completer?.complete(fds);
} catch (e) {
debugPrint("tryCompleteJob err: $e");
}
}
tryCompleteTask(String? msg, String? isLocalStr) {
if (msg == null || isLocalStr == null) return;
late final Map<Object, Completer<FileDirectory>> tasks;
@@ -1127,6 +1210,28 @@ class FileFetcher {
}
}
Future<List<FileDirectory>> readEmptyDirs(
String path, bool isLocal, bool showHidden) async {
try {
if (isLocal) {
final res = await bind.sessionReadLocalEmptyDirsRecursiveSync(
sessionId: sessionId, path: path, includeHidden: showHidden);
final List<dynamic> fdJsons = jsonDecode(res);
final List<FileDirectory> fds =
fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList();
return fds;
} else {
await bind.sessionReadRemoteEmptyDirsRecursiveSync(
sessionId: sessionId, path: path, includeHidden: showHidden);
return registerReadEmptyDirsTask(isLocal, path);
}
} catch (e) {
return Future.error(e);
}
}
Future<FileDirectory> fetchDirectory(
String path, bool isLocal, bool showHidden) async {
try {
@@ -1373,6 +1478,24 @@ class PathUtil {
static final windowsContext = path.Context(style: path.Style.windows);
static final posixContext = path.Context(style: path.Style.posix);
static String getOtherSidePath(String mainRootPath, String mainPath,
bool isMainWindows, String otherRootPath, bool isOtherWindows) {
final mainPathUtil = isMainWindows ? windowsContext : posixContext;
final relativePath = mainPathUtil.relative(mainPath, from: mainRootPath);
final names = mainPathUtil.split(relativePath);
final otherPathUtil = isOtherWindows ? windowsContext : posixContext;
String path = otherRootPath;
for (var name in names) {
path = otherPathUtil.join(path, name);
}
return path;
}
static String join(String path1, String path2, bool isWindows) {
final pathUtil = isWindows ? windowsContext : posixContext;
return pathUtil.join(path1, path2);

View File

@@ -544,7 +544,40 @@ class InputModel {
handleKeyDownEventModifiers(e);
}
if (isMobile || (isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
bool isMobileAndMapMode = false;
if (isMobile) {
// Do not use map mode if mobile -> Android. Android does not support map mode for now.
// Because simulating the physical key events(uhid) which requires root permission is not supported.
if (peerPlatform != kPeerPlatformAndroid) {
if (isIOS) {
isMobileAndMapMode = true;
} else {
// The physicalKey.usbHidUsage may be not correct for soft keyboard on Android.
// iOS does not have this issue.
// 1. Open the soft keyboard on Android
// 2. Switch to input method like zh/ko/ja
// 3. Click Backspace and Enter on the soft keyboard or physical keyboard
// 4. The physicalKey.usbHidUsage is not correct.
// PhysicalKeyboardKey#8ac83(usbHidUsage: "0x1100000042", debugName: "Key with ID 0x1100000042")
// LogicalKeyboardKey#2604c(keyId: "0x10000000d", keyLabel: "Enter", debugName: "Enter")
//
// The correct PhysicalKeyboardKey should be
// PhysicalKeyboardKey#e14a9(usbHidUsage: "0x00070028", debugName: "Enter")
// https://github.com/flutter/flutter/issues/157771
// We cannot use the debugName to determine the key is correct or not, because it's null in release mode.
// The normal `usbHidUsage` for keyboard shoud be between [0x00000010, 0x000c029f]
// https://github.com/flutter/flutter/blob/c051b69e2a2224300e20d93dbd15f4b91e8844d1/packages/flutter/lib/src/services/keyboard_key.g.dart#L5332 - 5600
final isNormalHsbHidUsage = (e.physicalKey.usbHidUsage >> 20) == 0;
isMobileAndMapMode = isNormalHsbHidUsage &&
// No need to check `!['Backspace', 'Enter'].contains(e.logicalKey.keyLabel)`
// But we still add it for more reliability.
!['Backspace', 'Enter'].contains(e.logicalKey.keyLabel);
}
}
}
final isDesktopAndMapMode =
isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode);
if (isMobileAndMapMode || isDesktopAndMapMode) {
// FIXME: e.character is wrong for dead keys, eg: ^ in de
newKeyboardMode(
e.character ?? '',
@@ -735,22 +768,22 @@ class InputModel {
}
/// Send a mouse tap event(down and up).
void tap(MouseButtons button) {
sendMouse('down', button);
sendMouse('up', button);
Future<void> tap(MouseButtons button) async {
await sendMouse('down', button);
await sendMouse('up', button);
}
void tapDown(MouseButtons button) {
sendMouse('down', button);
Future<void> tapDown(MouseButtons button) async {
await sendMouse('down', button);
}
void tapUp(MouseButtons button) {
sendMouse('up', button);
Future<void> tapUp(MouseButtons button) async {
await sendMouse('up', button);
}
/// Send scroll event with scroll distance [y].
void scroll(int y) {
bind.sessionSendMouse(
Future<void> scroll(int y) async {
await bind.sessionSendMouse(
sessionId: sessionId,
msg: json
.encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()})));
@@ -771,9 +804,9 @@ class InputModel {
}
/// Send mouse press event.
void sendMouse(String type, MouseButtons button) {
Future<void> sendMouse(String type, MouseButtons button) async {
if (!keyboardPerm) return;
bind.sessionSendMouse(
await bind.sessionSendMouse(
sessionId: sessionId,
msg: json.encode(modify({'type': type, 'buttons': button.value})));
}
@@ -797,11 +830,11 @@ class InputModel {
}
/// Send mouse movement event with distance in [x] and [y].
void moveMouse(double x, double y) {
Future<void> moveMouse(double x, double y) async {
if (!keyboardPerm) return;
var x2 = x.toInt();
var y2 = y.toInt();
bind.sessionSendMouse(
await bind.sessionSendMouse(
sessionId: sessionId,
msg: json.encode(modify({'x': '$x2', 'y': '$y2'})));
}
@@ -823,7 +856,7 @@ class InputModel {
_stopFling = true;
if (isViewOnly) return;
if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', 'pan_start', e.position);
handlePointerEvent('touch', kMouseEventTypePanStart, e.position);
}
}
@@ -866,8 +899,8 @@ class InputModel {
}
if (x != 0 || y != 0) {
if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent(
'touch', 'pan_update', Offset(x.toDouble(), y.toDouble()));
handlePointerEvent('touch', kMouseEventTypePanUpdate,
Offset(x.toDouble(), y.toDouble()));
} else {
bind.sessionSendMouse(
sessionId: sessionId,
@@ -929,7 +962,7 @@ class InputModel {
void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', 'pan_end', e.position);
handlePointerEvent('touch', kMouseEventTypePanEnd, e.position);
return;
}
@@ -1047,7 +1080,7 @@ class InputModel {
onExit: true,
);
int trySetNearestRange(int v, int min, int max, int n) {
static int tryGetNearestRange(int v, int min, int max, int n) {
if (v < min && v >= min - n) {
v = min;
}
@@ -1087,13 +1120,13 @@ class InputModel {
// to-do: handle mouse events
late final dynamic evtValue;
if (type == 'pan_update') {
if (type == kMouseEventTypePanUpdate) {
evtValue = {
'x': x.toInt(),
'y': y.toInt(),
};
} else {
final isMoveTypes = ['pan_start', 'pan_end'];
final isMoveTypes = [kMouseEventTypePanStart, kMouseEventTypePanEnd];
final pos = handlePointerDevicePos(
kPointerEventKindTouch,
x,
@@ -1148,14 +1181,14 @@ class InputModel {
return;
}
var type = '';
var type = kMouseEventTypeDefault;
var isMove = false;
switch (evt['type']) {
case _kMouseEventDown:
type = 'down';
type = kMouseEventTypeDown;
break;
case _kMouseEventUp:
type = 'up';
type = kMouseEventTypeUp;
break;
case _kMouseEventMove:
_pointerMovedAfterEnter = true;
@@ -1166,7 +1199,7 @@ class InputModel {
}
evt['type'] = type;
if (type == 'down' && !_pointerMovedAfterEnter) {
if (type == kMouseEventTypeDown && !_pointerMovedAfterEnter) {
// Move mouse to the position of the down event first.
lastMousePos = ui.Offset(x, y);
refreshMousePos();
@@ -1339,6 +1372,14 @@ class InputModel {
return null;
}
return InputModel.getPointInRemoteRect(
true, peerPlatform, kind, evtType, evtX, evtY, rect,
buttons: buttons);
}
static Point? getPointInRemoteRect(bool isLocalDesktop, String? peerPlatform,
String kind, String evtType, int evtX, int evtY, Rect rect,
{int buttons = kPrimaryMouseButton}) {
int minX = rect.left.toInt();
// https://github.com/rustdesk/rustdesk/issues/6678
// For Windows, [0,maxX], [0,maxY] should be set to enable window snapping.
@@ -1347,15 +1388,34 @@ class InputModel {
int minY = rect.top.toInt();
int maxY = (rect.top + rect.height).toInt() -
(peerPlatform == kPeerPlatformWindows ? 0 : 1);
evtX = trySetNearestRange(evtX, minX, maxX, 5);
evtY = trySetNearestRange(evtY, minY, maxY, 5);
if (kind == kPointerEventKindMouse) {
if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) {
// If left mouse up, no early return.
if (!(buttons == kPrimaryMouseButton && evtType == 'up')) {
return null;
evtX = InputModel.tryGetNearestRange(evtX, minX, maxX, 5);
evtY = InputModel.tryGetNearestRange(evtY, minY, maxY, 5);
if (isLocalDesktop) {
if (kind == kPointerEventKindMouse) {
if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) {
// If left mouse up, no early return.
if (!(buttons == kPrimaryMouseButton &&
evtType == kMouseEventTypeUp)) {
return null;
}
}
}
} else {
bool evtXInRange = evtX >= minX && evtX <= maxX;
bool evtYInRange = evtY >= minY && evtY <= maxY;
if (!(evtXInRange || evtYInRange)) {
return null;
}
if (evtX < minX) {
evtX = minX;
} else if (evtX > maxX) {
evtX = maxX;
}
if (evtY < minY) {
evtY = minY;
} else if (evtY > maxY) {
evtY = maxY;
}
}
return Point(evtX, evtY);

View File

@@ -6,8 +6,10 @@ import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/ab_model.dart';
@@ -142,6 +144,7 @@ class FfiModel with ChangeNotifier {
bool get touchMode => _touchMode;
bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid;
bool get isPeerMobile => isPeerAndroid;
bool get viewOnly => _viewOnly;
@@ -306,6 +309,8 @@ class FfiModel with ChangeNotifier {
.receive(int.parse(evt['id'] as String), evt['text'] ?? '');
} else if (name == 'file_dir') {
parent.target?.fileModel.receiveFileDir(evt);
} else if (name == 'empty_dirs') {
parent.target?.fileModel.receiveEmptyDirs(evt);
} else if (name == 'job_progress') {
parent.target?.fileModel.jobController.tryUpdateJobProgress(evt);
} else if (name == 'job_done') {
@@ -1274,13 +1279,6 @@ class ImageModel with ChangeNotifier {
if (isDesktop || isWebDesktop) {
await parent.target?.canvasModel.updateViewStyle();
await parent.target?.canvasModel.updateScrollStyle();
} else {
final size = MediaQueryData.fromWindow(ui.window).size;
final canvasWidth = size.width;
final canvasHeight = size.height;
final xscale = canvasWidth / image.width;
final yscale = canvasHeight / image.height;
parent.target?.canvasModel.scale = min(xscale, yscale);
}
if (parent.target != null) {
await initializeCursorAndCanvas(parent.target!);
@@ -1292,20 +1290,18 @@ class ImageModel with ChangeNotifier {
}
// mobile only
// for desktop, height should minus tabbar height
double get maxScale {
if (_image == null) return 1.5;
final size = MediaQueryData.fromWindow(ui.window).size;
final size = parent.target!.canvasModel.getSize();
final xscale = size.width / _image!.width;
final yscale = size.height / _image!.height;
return max(1.5, max(xscale, yscale));
}
// mobile only
// for desktop, height should minus tabbar height
double get minScale {
if (_image == null) return 1.5;
final size = MediaQueryData.fromWindow(ui.window).size;
final size = parent.target!.canvasModel.getSize();
final xscale = size.width / _image!.width;
final yscale = size.height / _image!.height;
return min(xscale, yscale) / 1.5;
@@ -1428,6 +1424,12 @@ class CanvasModel with ChangeNotifier {
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle();
Timer? _timerMobileFocusCanvasCursor;
// `isMobileCanvasChanged` is used to avoid canvas reset when changing the input method
// after showing the soft keyboard.
bool isMobileCanvasChanged = false;
final ScrollController _horizontal = ScrollController();
final ScrollController _vertical = ScrollController();
@@ -1470,15 +1472,29 @@ class CanvasModel with ChangeNotifier {
static double get bottomToEdge =>
isDesktop ? windowBorderWidth + kDragToResizeAreaPadding.bottom : 0;
updateViewStyle({refreshMousePos = true}) async {
Size getSize() {
final size = MediaQueryData.fromWindow(ui.window).size;
// If minimized, w or h may be negative here.
double w = size.width - leftToEdge - rightToEdge;
double h = size.height - topToEdge - bottomToEdge;
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
Size getSize() {
final mediaData = MediaQueryData.fromView(ui.window);
final size = mediaData.size;
// If minimized, w or h may be negative here.
double w = size.width - leftToEdge - rightToEdge;
double h = size.height - topToEdge - bottomToEdge;
if (isMobile) {
h = h -
mediaData.viewInsets.bottom -
(parent.target?.cursorModel.keyHelpToolsRectToAdjustCanvas?.bottom ??
0);
}
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
}
// mobile only
double getAdjustY() {
final bottom =
parent.target?.cursorModel.keyHelpToolsRectToAdjustCanvas?.bottom ?? 0;
return max(bottom - MediaQueryData.fromView(ui.window).padding.top, 0);
}
updateViewStyle({refreshMousePos = true, notify = true}) async {
final style = await bind.sessionGetViewStyle(sessionId: sessionId);
if (style == null) {
return;
@@ -1507,16 +1523,25 @@ class CanvasModel with ChangeNotifier {
if (kIgnoreDpi && style == kRemoteViewStyleOriginal) {
_scale = 1.0 / _devicePixelRatio;
}
_x = (size.width - displayWidth * _scale) / 2;
_y = (size.height - displayHeight * _scale) / 2;
_resetCanvasOffset(displayWidth, displayHeight);
_imageOverflow.value = _x < 0 || y < 0;
notifyListeners();
if (refreshMousePos) {
if (notify) {
notifyListeners();
}
if (!isMobile && refreshMousePos) {
parent.target?.inputModel.refreshMousePos();
}
tryUpdateScrollStyle(Duration.zero, style);
}
_resetCanvasOffset(int displayWidth, int displayHeight) {
_x = (size.width - displayWidth * _scale) / 2;
_y = (size.height - displayHeight * _scale) / 2;
if (isMobile && _lastViewStyle.style == kRemoteViewStyleOriginal) {
_moveToCenterCursor();
}
}
tryUpdateScrollStyle(Duration duration, String? style) async {
if (_scrollStyle != ScrollStyle.scrollbar) return;
style ??= await bind.sessionGetViewStyle(sessionId: sessionId);
@@ -1620,6 +1645,9 @@ class CanvasModel with ChangeNotifier {
panX(double dx) {
_x += dx;
if (isMobile) {
isMobileCanvasChanged = true;
}
notifyListeners();
}
@@ -1627,17 +1655,20 @@ class CanvasModel with ChangeNotifier {
if (isWebDesktop) {
updateViewStyle();
} else {
_x = (size.width - getDisplayWidth() * _scale) / 2;
_y = (size.height - getDisplayHeight() * _scale) / 2;
_resetCanvasOffset(getDisplayWidth(), getDisplayHeight());
}
notifyListeners();
}
panY(double dy) {
_y += dy;
if (isMobile) {
isMobileCanvasChanged = true;
}
notifyListeners();
}
// mobile only
updateScale(double v, Offset focalPoint) {
if (parent.target?.imageModel.image == null) return;
final s = _scale;
@@ -1649,13 +1680,13 @@ class CanvasModel with ChangeNotifier {
// (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX
// _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2
_x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale;
final adjustForKeyboard =
parent.target?.cursorModel.adjustForKeyboard() ?? 0.0;
// (focalPoint.dy - _y_1 + adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 + adjust) / s2 + displayOriginY
// _y_2 = focalPoint.dy + adjust - (focalPoint.dy - _y_1 + adjust) / s1 * s2
_y = focalPoint.dy +
adjustForKeyboard -
(focalPoint.dy - _y + adjustForKeyboard) / s * _scale;
final adjust = getAdjustY();
// (focalPoint.dy - _y_1 - adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 - adjust) / s2 + displayOriginY
// _y_2 = focalPoint.dy - adjust - (focalPoint.dy - _y_1 - adjust) / s1 * s2
_y = focalPoint.dy - adjust - (focalPoint.dy - _y - adjust) / s * _scale;
if (isMobile) {
isMobileCanvasChanged = true;
}
notifyListeners();
}
@@ -1666,10 +1697,7 @@ class CanvasModel with ChangeNotifier {
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;
_resetCanvasOffset(getDisplayWidth(), getDisplayHeight());
bind.sessionSetViewStyle(sessionId: sessionId, value: _lastViewStyle.style);
notifyListeners();
}
@@ -1678,6 +1706,8 @@ class CanvasModel with ChangeNotifier {
_x = 0;
_y = 0;
_scale = 1.0;
_lastViewStyle = ViewStyle.defaultViewStyle();
_timerMobileFocusCanvasCursor?.cancel();
}
updateScrollPercent() {
@@ -1695,6 +1725,42 @@ class CanvasModel with ChangeNotifier {
: 0.0;
setScrollPercent(percentX, percentY);
}
void mobileFocusCanvasCursor() {
_timerMobileFocusCanvasCursor?.cancel();
_timerMobileFocusCanvasCursor =
Timer(Duration(milliseconds: 100), () async {
await updateViewStyle(refreshMousePos: false, notify: false);
notifyListeners();
});
}
// mobile only
// Move the canvas to make the cursor visible(center) on the screen.
void _moveToCenterCursor() {
Rect? imageRect = parent.target?.ffiModel.rect;
if (imageRect == null) {
// unreachable
return;
}
final maxX = 0.0;
final minX = _size.width + (imageRect.left - imageRect.right) * _scale;
final maxY = 0.0;
final minY = _size.height + (imageRect.top - imageRect.bottom) * _scale;
Offset offsetToCenter =
parent.target?.cursorModel.getCanvasOffsetToCenterCursor() ??
Offset.zero;
if (minX < 0) {
_x = min(max(offsetToCenter.dx, minX), maxX);
} else {
// _size.width > (imageRect.right, imageRect.left) * _scale, we should not change _x
}
if (minY < 0) {
_y = min(max(offsetToCenter.dy, minY), maxY);
} else {
// _size.height > (imageRect.bottom - imageRect.top) * _scale, , we should not change _y
}
}
}
// data for cursor
@@ -1888,9 +1954,13 @@ class CursorModel with ChangeNotifier {
// `lastIsBlocked` is only used in common/widgets/remote_input.dart -> _RawTouchGestureDetectorRegionState -> onDoubleTap()
// Because onDoubleTap() doesn't have the `event` parameter, we can't get the touch event's position.
bool _lastIsBlocked = false;
double _yForKeyboardAdjust = 0;
bool _lastKeyboardIsVisible = false;
keyHelpToolsVisibilityChanged(Rect? r) {
bool get lastKeyboardIsVisible => _lastKeyboardIsVisible;
Rect? get keyHelpToolsRectToAdjustCanvas =>
_lastKeyboardIsVisible ? _keyHelpToolsRect : null;
keyHelpToolsVisibilityChanged(Rect? r, bool keyboardIsVisible) {
_keyHelpToolsRect = r;
if (r == null) {
_lastIsBlocked = false;
@@ -1900,7 +1970,11 @@ class CursorModel with ChangeNotifier {
// `lastIsBlocked` will be set when the cursor is moving or touch somewhere else.
_lastIsBlocked = true;
}
_yForKeyboardAdjust = _y;
if (isMobile && _lastKeyboardIsVisible != keyboardIsVisible) {
parent.target?.canvasModel.mobileFocusCanvasCursor();
parent.target?.canvasModel.isMobileCanvasChanged = false;
}
_lastKeyboardIsVisible = keyboardIsVisible;
}
get lastIsBlocked => _lastIsBlocked;
@@ -1939,8 +2013,10 @@ class CursorModel with ChangeNotifier {
addKey(String key) => _cacheKeys.add(key);
// remote physical display coordinate
// For update pan (mobile), onOneFingerPanStart, onOneFingerPanUpdate, onHoldDragUpdate
Rect getVisibleRect() {
final size = MediaQueryData.fromWindow(ui.window).size;
final size = parent.target?.canvasModel.getSize() ??
MediaQueryData.fromView(ui.window).size;
final xoffset = parent.target?.canvasModel.x ?? 0;
final yoffset = parent.target?.canvasModel.y ?? 0;
final scale = parent.target?.canvasModel.scale ?? 1;
@@ -1949,22 +2025,22 @@ class CursorModel with ChangeNotifier {
return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale);
}
get keyboardHeight => MediaQueryData.fromWindow(ui.window).viewInsets.bottom;
get scale => parent.target?.canvasModel.scale ?? 1.0;
double adjustForKeyboard() {
if (keyboardHeight < 100) {
return 0.0;
}
final m = MediaQueryData.fromWindow(ui.window);
final size = m.size;
final thresh = (size.height - keyboardHeight) / 2;
final h = (_yForKeyboardAdjust - getVisibleRect().top) *
scale; // local physical display height
return h - thresh;
Offset getCanvasOffsetToCenterCursor() {
// Cursor should be at the center of the visible rect.
// _x = rect.left + rect.width / 2
// _y = rect.right + rect.height / 2
// See `getVisibleRect()`
// _x = _displayOriginX - xoffset / scale + size.width / scale * 0.5;
// _y = _displayOriginY - yoffset / scale + size.height / scale * 0.5;
final size = parent.target?.canvasModel.getSize() ??
MediaQueryData.fromView(ui.window).size;
final xoffset = (_displayOriginX - _x) * scale + size.width * 0.5;
final yoffset = (_displayOriginY - _y) * scale + size.height * 0.5;
return Offset(xoffset, yoffset);
}
get scale => parent.target?.canvasModel.scale ?? 1.0;
// mobile Soft keyboard, block touch event from the KeyHelpTools
shouldBlock(double x, double y) {
if (!(parent.target?.ffiModel.touchMode ?? false)) {
@@ -1979,22 +2055,63 @@ class CursorModel with ChangeNotifier {
return false;
}
move(double x, double y) {
// For touch mode
Future<bool> move(double x, double y) async {
if (shouldBlock(x, y)) {
_lastIsBlocked = true;
return false;
}
_lastIsBlocked = false;
moveLocal(x, y, adjust: adjustForKeyboard());
parent.target?.inputModel.moveMouse(_x, _y);
if (!_moveLocalIfInRemoteRect(x, y)) {
return false;
}
await parent.target?.inputModel.moveMouse(_x, _y);
return true;
}
bool isInRemoteRect(Offset offset) {
return getRemotePosInRect(offset) != null;
}
Offset? getRemotePosInRect(Offset offset) {
final adjust = parent.target?.canvasModel.getAdjustY() ?? 0;
final newPos = _getNewPos(offset.dx, offset.dy, adjust);
final visibleRect = getVisibleRect();
if (!isPointInRect(newPos, visibleRect)) {
return null;
}
final rect = parent.target?.ffiModel.rect;
if (rect != null) {
if (!isPointInRect(newPos, rect)) {
return null;
}
}
return newPos;
}
Offset _getNewPos(double x, double y, double adjust) {
final xoffset = parent.target?.canvasModel.x ?? 0;
final yoffset = parent.target?.canvasModel.y ?? 0;
final newX = (x - xoffset) / scale + _displayOriginX;
final newY = (y - yoffset - adjust) / scale + _displayOriginY;
return Offset(newX, newY);
}
bool _moveLocalIfInRemoteRect(double x, double y) {
final newPos = getRemotePosInRect(Offset(x, y));
if (newPos == null) {
return false;
}
_x = newPos.dx;
_y = newPos.dy;
notifyListeners();
return true;
}
moveLocal(double x, double y, {double adjust = 0}) {
final xoffset = parent.target?.canvasModel.x ?? 0;
final yoffset = parent.target?.canvasModel.y ?? 0;
_x = (x - xoffset) / scale + _displayOriginX;
_y = (y - yoffset + adjust) / scale + _displayOriginY;
final newPos = _getNewPos(x, y, adjust);
_x = newPos.dx;
_y = newPos.dy;
notifyListeners();
}
@@ -2006,9 +2123,9 @@ class CursorModel with ChangeNotifier {
notifyListeners();
}
updatePan(Offset delta, Offset localPosition, bool touchMode) {
updatePan(Offset delta, Offset localPosition, bool touchMode) async {
if (touchMode) {
_handleTouchMode(delta, localPosition);
await _handleTouchMode(delta, localPosition);
return;
}
double dx = delta.dx;
@@ -2066,13 +2183,34 @@ class CursorModel with ChangeNotifier {
}
if (dx == 0 && dy == 0) return;
_x += dx;
_y += dy;
Point? newPos;
final rect = parent.target?.ffiModel.rect;
if (rect == null) {
// unreachable
return;
}
newPos = InputModel.getPointInRemoteRect(
false,
parent.target?.ffiModel.pi.platform,
kPointerEventKindMouse,
kMouseEventTypeDefault,
(_x + dx).toInt(),
(_y + dy).toInt(),
rect,
buttons: kPrimaryButton);
if (newPos == null) {
return;
}
dx = newPos.x - _x;
dy = newPos.y - _y;
_x = newPos.x.toDouble();
_y = newPos.y.toDouble();
if (tryMoveCanvasX && dx != 0) {
parent.target?.canvasModel.panX(-dx);
parent.target?.canvasModel.panX(-dx * scale);
}
if (tryMoveCanvasY && dy != 0) {
parent.target?.canvasModel.panY(-dy);
parent.target?.canvasModel.panY(-dy * scale);
}
parent.target?.inputModel.moveMouse(_x, _y);
@@ -2085,7 +2223,7 @@ class CursorModel with ChangeNotifier {
return x >= 0 && y >= 0 && x <= w && y <= h;
}
_handleTouchMode(Offset delta, Offset localPosition) {
_handleTouchMode(Offset delta, Offset localPosition) async {
bool isMoved = false;
if (_remoteWindowCoords.isNotEmpty &&
_windowRect != null &&
@@ -2101,15 +2239,50 @@ class CursorModel with ChangeNotifier {
coords.canvas.scale;
x2 += coords.cursor.offset.dx;
y2 += coords.cursor.offset.dy;
parent.target?.inputModel.moveMouse(x2, y2);
await parent.target?.inputModel.moveMouse(x2, y2);
isMoved = true;
}
}
if (!isMoved) {
final rect = parent.target?.ffiModel.rect;
if (rect == null) {
// unreachable
return;
}
Offset? movementInRect(double x, double y, Rect r) {
final isXInRect = x >= r.left && x <= r.right;
final isYInRect = y >= r.top && y <= r.bottom;
if (!(isXInRect || isYInRect)) {
return null;
}
if (x < r.left) {
x = r.left;
} else if (x > r.right) {
x = r.right;
}
if (y < r.top) {
y = r.top;
} else if (y > r.bottom) {
y = r.bottom;
}
return Offset(x, y);
}
final scale = parent.target?.canvasModel.scale ?? 1.0;
_x += delta.dx / scale;
_y += delta.dy / scale;
parent.target?.inputModel.moveMouse(_x, _y);
var movement =
movementInRect(_x + delta.dx / scale, _y + delta.dy / scale, rect);
if (movement == null) {
return;
}
movement = movementInRect(movement.dx, movement.dy, getVisibleRect());
if (movement == null) {
return;
}
_x = movement.dx;
_y = movement.dy;
await parent.target?.inputModel.moveMouse(_x, _y);
}
notifyListeners();
}
@@ -2686,6 +2859,7 @@ class FFI {
canvasModel.scale,
ffiModel.pi.currentDisplay);
}
imageModel.callbacksOnFirstImage.clear();
await imageModel.update(null);
cursorModel.clear();
ffiModel.clear();

View File

@@ -30,6 +30,7 @@ class ServerModel with ChangeNotifier {
bool _inputOk = false;
bool _audioOk = false;
bool _fileOk = false;
bool _clipboardOk = false;
bool _showElevation = false;
bool hideCm = false;
int _connectStatus = 0; // Rendezvous Server status
@@ -59,6 +60,8 @@ class ServerModel with ChangeNotifier {
bool get fileOk => _fileOk;
bool get clipboardOk => _clipboardOk;
bool get showElevation => _showElevation;
int get connectStatus => _connectStatus;
@@ -209,6 +212,10 @@ class ServerModel with ChangeNotifier {
_fileOk = fileOption != 'N';
}
// clipboard
final clipOption = await bind.mainGetOption(key: kOptionEnableClipboard);
_clipboardOk = clipOption != 'N';
notifyListeners();
}
@@ -315,6 +322,14 @@ class ServerModel with ChangeNotifier {
notifyListeners();
}
toggleClipboard() async {
_clipboardOk = !_clipboardOk;
bind.mainSetOption(
key: kOptionEnableClipboard,
value: _clipboardOk ? defaultOptionYes : 'N');
notifyListeners();
}
toggleInput() async {
if (clients.isNotEmpty) {
await showClientsMayNotBeChangedAlert(parent.target);

View File

@@ -37,10 +37,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev"
source: hosted
version: "3.4.10"
version: "3.6.1"
args:
dependency: transitive
description:
@@ -384,10 +384,10 @@ packages:
dependency: "direct main"
description:
name: extended_text
sha256: "7f382de3af12992e34bd72ddd36becf90c4720900af126cb9859f0189af71ffe"
sha256: "38c1cac571d6eaf406f4b80040c1f88561e7617ad90795aac6a1be0a8d0bb676"
url: "https://pub.dev"
source: hosted
version: "13.0.0"
version: "14.0.0"
extended_text_library:
dependency: transitive
description:
@@ -408,10 +408,10 @@ packages:
dependency: "direct main"
description:
name: ffi
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.3"
ffigen:
dependency: "direct dev"
description:
@@ -745,10 +745,10 @@ packages:
dependency: "direct main"
description:
name: image_picker
sha256: b6951e25b795d053a6ba03af5f710069c99349de9341af95155d52665cb4607c
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
url: "https://pub.dev"
source: hosted
version: "0.8.9"
version: "1.1.2"
image_picker_android:
dependency: transitive
description:
@@ -793,10 +793,10 @@ packages:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b
sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80"
url: "https://pub.dev"
source: hosted
version: "2.9.3"
version: "2.10.0"
image_picker_windows:
dependency: transitive
description:
@@ -865,18 +865,18 @@ packages:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.8.0"
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.15.0"
mime:
dependency: transitive
description:
@@ -1037,14 +1037,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
url: "https://pub.dev"
source: hosted
version: "3.7.4"
pool:
dependency: transitive
description:
@@ -1325,10 +1317,11 @@ packages:
uni_links:
dependency: "direct main"
description:
name: uni_links
sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e"
url: "https://pub.dev"
source: hosted
path: uni_links
ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f
resolved-ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f
url: "https://github.com/rustdesk-org/uni_links"
source: git
version: "0.5.1"
uni_links_desktop:
dependency: "direct main"
@@ -1558,24 +1551,24 @@ packages:
dependency: "direct main"
description:
name: win32
sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a"
url: "https://pub.dev"
source: hosted
version: "5.2.0"
version: "5.5.4"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a"
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.1.5"
window_manager:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: f19acdb008645366339444a359a45c3257c8b32e
resolved-ref: "85789bfe6e4cfaf4ecc00c52857467fdb7f26879"
url: "https://github.com/rustdesk-org/window_manager"
source: git
version: "0.3.6"
@@ -1583,8 +1576,8 @@ packages:
dependency: "direct main"
description:
path: "plugins/window_size"
ref: a738913c8ce2c9f47515382d40827e794a334274
resolved-ref: a738913c8ce2c9f47515382d40827e794a334274
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
resolved-ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
url: "https://github.com/google/flutter-desktop-embedding.git"
source: git
version: "0.1.0"
@@ -1629,5 +1622,5 @@ packages:
source: hosted
version: "0.2.1"
sdks:
dart: ">=3.3.0 <4.0.0"
flutter: ">=3.19.0"
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"

View File

@@ -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.2+51
version: 1.3.3+52
environment:
sdk: '^3.1.0'
@@ -46,7 +46,7 @@ dependencies:
http: ^1.1.0
qr_code_scanner: ^1.0.0
zxing2: ^0.2.0
image_picker: ^0.8.5
image_picker: ^1.1.2
image: ^4.0.17
back_button_interceptor: ^6.0.1
flutter_rust_bridge: "1.80.1"
@@ -62,7 +62,7 @@ dependencies:
git:
url: https://github.com/google/flutter-desktop-embedding.git
path: plugins/window_size
ref: a738913c8ce2c9f47515382d40827e794a334274
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
get: ^4.6.5
visibility_detector: ^0.4.0+2
contextmenu: ^3.0.0
@@ -78,7 +78,11 @@ dependencies:
# if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below).
git:
url: https://github.com/rustdesk-org/flutter_improved_scrolling
uni_links: ^0.5.1
uni_links:
git:
url: https://github.com/rustdesk-org/uni_links
path: uni_links
ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f
uni_links_desktop: ^0.1.6 # use 0.1.6 to make flutter 3.13 works
path: ^1.8.1
auto_size_text: ^3.0.0
@@ -104,7 +108,7 @@ dependencies:
pull_down_button: ^0.9.3
device_info_plus: ^9.1.0
qr_flutter: ^4.1.0
extended_text: 13.0.0
extended_text: 14.0.0
dev_dependencies:
icons_launcher: ^2.0.4
@@ -186,3 +190,4 @@ flutter:
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

View File

@@ -368,6 +368,16 @@ message ReadDir {
bool include_hidden = 2;
}
message ReadEmptyDirs {
string path = 1;
bool include_hidden = 2;
}
message ReadEmptyDirsResponse {
string path = 1;
repeated FileDirectory empty_dirs = 2;
}
message ReadAllFiles {
int32 id = 1;
string path = 2;
@@ -392,6 +402,7 @@ message FileAction {
FileTransferCancel cancel = 8;
FileTransferSendConfirmRequest send_confirm = 9;
FileRename rename = 10;
ReadEmptyDirs read_empty_dirs = 11;
}
}
@@ -404,6 +415,7 @@ message FileResponse {
FileTransferError error = 3;
FileTransferDone done = 4;
FileTransferDigest digest = 5;
ReadEmptyDirsResponse empty_dirs = 6;
}
}

View File

@@ -1710,6 +1710,9 @@ impl UserDefaultConfig {
pub fn get(&self, key: &str) -> String {
match key {
#[cfg(any(target_os = "android", target_os = "ios"))]
keys::OPTION_VIEW_STYLE => self.get_string(key, "adaptive", vec!["original"]),
#[cfg(not(any(target_os = "android", target_os = "ios")))]
keys::OPTION_VIEW_STYLE => self.get_string(key, "original", vec!["adaptive"]),
keys::OPTION_SCROLL_STYLE => self.get_string(key, "scrollauto", vec!["scrollbar"]),
keys::OPTION_IMAGE_QUALITY => {
@@ -2225,6 +2228,7 @@ pub mod keys {
pub const OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless";
pub const OPTION_ENABLE_HWCODEC: &str = "enable-hwcodec";
pub const OPTION_APPROVE_MODE: &str = "approve-mode";
pub const OPTION_VERIFICATION_METHOD: &str = "verification-method";
pub const OPTION_CUSTOM_RENDEZVOUS_SERVER: &str = "custom-rendezvous-server";
pub const OPTION_API_SERVER: &str = "api-server";
pub const OPTION_KEY: &str = "key";
@@ -2234,6 +2238,7 @@ pub mod keys {
pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str =
"enable-android-software-encoding-half-scale";
pub const OPTION_ENABLE_TRUSTED_DEVICES: &str = "enable-trusted-devices";
pub const OPTION_AV1_TEST: &str = "av1-test";
// buildin options
pub const OPTION_DISPLAY_NAME: &str = "display-name";
@@ -2370,6 +2375,7 @@ pub mod keys {
OPTION_ALLOW_LINUX_HEADLESS,
OPTION_ENABLE_HWCODEC,
OPTION_APPROVE_MODE,
OPTION_VERIFICATION_METHOD,
OPTION_PROXY_URL,
OPTION_PROXY_USERNAME,
OPTION_PROXY_PASSWORD,

View File

@@ -185,6 +185,51 @@ pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType<Vec<F
read_dir_recursive(&get_path(path), &get_path(""), include_hidden)
}
fn read_empty_dirs_recursive(
path: &PathBuf,
prefix: &Path,
include_hidden: bool,
) -> ResultType<Vec<FileDirectory>> {
let mut dirs = Vec::new();
if path.is_dir() {
// to-do: symbol link handling, cp the link rather than the content
// to-do: file mode, for unix
let fd = read_dir(path, include_hidden)?;
if fd.entries.is_empty() {
dirs.push(fd);
} else {
for entry in fd.entries.iter() {
match entry.entry_type.enum_value() {
Ok(FileType::Dir) => {
if let Ok(mut tmp) = read_empty_dirs_recursive(
&path.join(&entry.name),
&prefix.join(&entry.name),
include_hidden,
) {
for entry in tmp.drain(0..) {
dirs.push(entry);
}
}
}
_ => {}
}
}
}
Ok(dirs)
} else if path.is_file() {
Ok(dirs)
} else {
bail!("Not exists");
}
}
pub fn get_empty_dirs_recursive(
path: &str,
include_hidden: bool,
) -> ResultType<Vec<FileDirectory>> {
read_empty_dirs_recursive(&get_path(path), &get_path(""), include_hidden)
}
#[inline]
pub fn is_file_exists(file_path: &str) -> bool {
return Path::new(file_path).exists();

View File

@@ -27,6 +27,7 @@ pub use anyhow::{self, bail};
pub use futures_util;
pub mod config;
pub mod fs;
pub mod mem;
pub use lazy_static;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use mac_address;

View File

@@ -0,0 +1,14 @@
/// SAFETY: the returned Vec must not be resized or reserverd
pub unsafe fn aligned_u8_vec(cap: usize, align: usize) -> Vec<u8> {
use std::alloc::*;
let layout =
Layout::from_size_align(cap, align).expect("invalid aligned value, must be power of 2");
unsafe {
let ptr = alloc(layout);
if ptr.is_null() {
panic!("failed to allocate {} bytes", cap);
}
Vec::from_raw_parts(ptr, 0, cap)
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk-portable-packer"
version = "1.3.2"
version = "1.3.3"
edition = "2021"
description = "RustDesk Remote Desktop"

View File

@@ -55,7 +55,11 @@ fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf {
target = target.replace("x64", "x86");
}
println!("cargo:info={}", target);
path.push("installed");
if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") {
path = vcpkg_root.into();
} else {
path.push("installed");
}
path.push(target);
println!(
"{}",

View File

@@ -5,16 +5,20 @@ use jni::sys::jboolean;
use jni::JNIEnv;
use jni::{
objects::{GlobalRef, JClass, JObject},
strings::JNIString,
JavaVM,
};
use hbb_common::{message_proto::MultiClipboards, protobuf::Message};
use jni::errors::{Error as JniError, Result as JniResult};
use lazy_static::lazy_static;
use serde::Deserialize;
use std::ops::Not;
use std::os::raw::c_void;
use std::sync::atomic::{AtomicPtr, Ordering::SeqCst};
use std::sync::{Mutex, RwLock};
use std::time::{Duration, Instant};
lazy_static! {
static ref JVM: RwLock<Option<JavaVM>> = RwLock::new(None);
static ref MAIN_SERVICE_CTX: RwLock<Option<GlobalRef>> = RwLock::new(None); // MainService -> video service / audio service / info
@@ -22,6 +26,9 @@ lazy_static! {
static ref AUDIO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT));
static ref NDK_CONTEXT_INITED: Mutex<bool> = Default::default();
static ref MEDIA_CODEC_INFOS: RwLock<Option<MediaCodecInfos>> = RwLock::new(None);
static ref CLIPBOARD_MANAGER: RwLock<Option<GlobalRef>> = RwLock::new(None);
static ref CLIPBOARDS_HOST: Mutex<Option<MultiClipboards>> = Mutex::new(None);
static ref CLIPBOARDS_CLIENT: Mutex<Option<MultiClipboards>> = Mutex::new(None);
}
const MAX_VIDEO_FRAME_TIMEOUT: Duration = Duration::from_millis(100);
@@ -104,6 +111,14 @@ pub fn get_audio_raw<'a>(dst: &mut Vec<u8>, last: &mut Vec<u8>) -> Option<()> {
AUDIO_RAW.lock().ok()?.take(dst, last)
}
pub fn get_clipboards(client: bool) -> Option<MultiClipboards> {
if client {
CLIPBOARDS_CLIENT.lock().ok()?.take()
} else {
CLIPBOARDS_HOST.lock().ok()?.take()
}
}
#[no_mangle]
pub extern "system" fn Java_ffi_FFI_onVideoFrameUpdate(
env: JNIEnv,
@@ -132,6 +147,27 @@ pub extern "system" fn Java_ffi_FFI_onAudioFrameUpdate(
}
}
#[no_mangle]
pub extern "system" fn Java_ffi_FFI_onClipboardUpdate(
env: JNIEnv,
_class: JClass,
buffer: JByteBuffer,
) {
if let Ok(data) = env.get_direct_buffer_address(&buffer) {
if let Ok(len) = env.get_direct_buffer_capacity(&buffer) {
let data = unsafe { std::slice::from_raw_parts(data, len) };
if let Ok(clips) = MultiClipboards::parse_from_bytes(&data[1..]) {
let is_client = data[0] == 1;
if is_client {
*CLIPBOARDS_CLIENT.lock().unwrap() = Some(clips);
} else {
*CLIPBOARDS_HOST.lock().unwrap() = Some(clips);
}
}
}
}
}
#[no_mangle]
pub extern "system" fn Java_ffi_FFI_setFrameRawEnable(
env: JNIEnv,
@@ -155,10 +191,36 @@ pub extern "system" fn Java_ffi_FFI_setFrameRawEnable(
pub extern "system" fn Java_ffi_FFI_init(env: JNIEnv, _class: JClass, ctx: JObject) {
log::debug!("MainService init from java");
if let Ok(jvm) = env.get_java_vm() {
*JVM.write().unwrap() = Some(jvm);
let java_vm = jvm.get_java_vm_pointer() as *mut c_void;
let mut jvm_lock = JVM.write().unwrap();
if jvm_lock.is_none() {
*jvm_lock = Some(jvm);
}
drop(jvm_lock);
if let Ok(context) = env.new_global_ref(ctx) {
let context_jobject = context.as_obj().as_raw() as *mut c_void;
*MAIN_SERVICE_CTX.write().unwrap() = Some(context);
init_ndk_context().ok();
init_ndk_context(java_vm, context_jobject);
}
}
}
#[no_mangle]
pub extern "system" fn Java_ffi_FFI_setClipboardManager(
env: JNIEnv,
_class: JClass,
clipboard_manager: JObject,
) {
log::debug!("ClipboardManager init from java");
if let Ok(jvm) = env.get_java_vm() {
let java_vm = jvm.get_java_vm_pointer() as *mut c_void;
let mut jvm_lock = JVM.write().unwrap();
if jvm_lock.is_none() {
*jvm_lock = Some(jvm);
}
drop(jvm_lock);
if let Ok(manager) = env.new_global_ref(clipboard_manager) {
*CLIPBOARD_MANAGER.write().unwrap() = Some(manager);
}
}
}
@@ -272,6 +334,59 @@ pub fn call_main_service_key_event(data: &[u8]) -> JniResult<()> {
}
}
fn _call_clipboard_manager<S, T>(name: S, sig: T, args: &[JValue]) -> JniResult<()>
where
S: Into<JNIString>,
T: Into<JNIString> + AsRef<str>,
{
if let (Some(jvm), Some(cm)) = (
JVM.read().unwrap().as_ref(),
CLIPBOARD_MANAGER.read().unwrap().as_ref(),
) {
let mut env = jvm.attach_current_thread()?;
env.call_method(cm, name, sig, args)?;
return Ok(());
} else {
return Err(JniError::ThrowFailed(-1));
}
}
pub fn call_clipboard_manager_update_clipboard(data: &[u8]) -> JniResult<()> {
if let (Some(jvm), Some(cm)) = (
JVM.read().unwrap().as_ref(),
CLIPBOARD_MANAGER.read().unwrap().as_ref(),
) {
let mut env = jvm.attach_current_thread()?;
let data = env.byte_array_from_slice(data)?;
env.call_method(
cm,
"rustUpdateClipboard",
"([B)V",
&[JValue::Object(&JObject::from(data))],
)?;
return Ok(());
} else {
return Err(JniError::ThrowFailed(-1));
}
}
pub fn call_clipboard_manager_enable_service_clipboard(enable: bool) -> JniResult<()> {
_call_clipboard_manager(
"rustEnableServiceClipboard",
"(Z)V",
&[JValue::Bool(jboolean::from(enable))],
)
}
pub fn call_clipboard_manager_enable_client_clipboard(enable: bool) -> JniResult<()> {
_call_clipboard_manager(
"rustEnableClientClipboard",
"(Z)V",
&[JValue::Bool(jboolean::from(enable))],
)
}
pub fn call_main_service_get_by_name(name: &str) -> JniResult<String> {
if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(),
@@ -332,7 +447,14 @@ pub fn call_main_service_set_by_name(
}
}
fn init_ndk_context() -> JniResult<()> {
// Difference between MainService, MainActivity, JNI_OnLoad:
// jvm is the same, ctx is differen and ctx of JNI_OnLoad is null.
// cpal: all three works
// Service(GetByName, ...): only ctx from MainService works, so use 2 init context functions
// On app start: JNI_OnLoad or MainActivity init context
// On service start first time: MainService replace the context
fn init_ndk_context(java_vm: *mut c_void, context_jobject: *mut c_void) {
let mut lock = NDK_CONTEXT_INITED.lock().unwrap();
if *lock {
unsafe {
@@ -340,22 +462,20 @@ fn init_ndk_context() -> JniResult<()> {
}
*lock = false;
}
if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(),
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
) {
unsafe {
ndk_context::initialize_android_context(
jvm.get_java_vm_pointer() as _,
ctx.as_obj().as_raw() as _,
);
#[cfg(feature = "hwcodec")]
hwcodec::android::ffmpeg_set_java_vm(
jvm.get_java_vm_pointer() as _,
);
}
*lock = true;
return Ok(());
unsafe {
ndk_context::initialize_android_context(java_vm, context_jobject);
#[cfg(feature = "hwcodec")]
hwcodec::android::ffmpeg_set_java_vm(java_vm);
}
Err(JniError::ThrowFailed(-1))
*lock = true;
}
// https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init
#[no_mangle]
pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) -> jni::sys::jint {
if let Ok(env) = vm.get_env() {
let vm = vm.get_java_vm_pointer() as *mut std::os::raw::c_void;
init_ndk_context(vm, res);
}
jni::JNIVersion::V6.into()
}

View File

@@ -66,7 +66,7 @@ mod webrtc {
const kMaxQindex: u32 = 205; // Max qindex threshold for QP scaling.
const kBitDepth: u32 = 8;
const kLagInFrames: u32 = 0; // No look ahead.
const kRtpTicksPerSecond: i32 = 90000;
pub(super) const kTimeBaseDen: i64 = 1000;
const kMinimumFrameRate: f64 = 1.0;
pub const DEFAULT_Q_MAX: u32 = 56; // no more than 63
@@ -108,7 +108,7 @@ mod webrtc {
c.g_h = cfg.height;
c.g_threads = codec_thread_num(64) as _;
c.g_timebase.num = 1;
c.g_timebase.den = kRtpTicksPerSecond;
c.g_timebase.den = kTimeBaseDen as _;
c.g_input_bit_depth = kBitDepth;
if let Some(keyframe_interval) = cfg.keyframe_interval {
c.kf_min_dist = 0;
@@ -313,7 +313,7 @@ impl EncoderApi for AomEncoder {
}
impl AomEncoder {
pub fn encode(&mut self, pts: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames> {
pub fn encode(&mut self, ms: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames> {
let bpp = if self.i444 { 24 } else { 12 };
if data.len() < self.width * self.height * bpp / 8 {
return Err(Error::FailedCall("len not enough".to_string()));
@@ -333,13 +333,14 @@ impl AomEncoder {
stride_align as _,
data.as_ptr() as _,
));
let pts = webrtc::kTimeBaseDen / 1000 * ms;
let duration = webrtc::kTimeBaseDen / 1000;
call_aom!(aom_codec_encode(
&mut self.ctx,
&image,
pts as _,
1, // Duration
0, // Flags
duration as _, // Duration
0, // Flags
));
Ok(EncodeFrames {

View File

@@ -1,8 +1,8 @@
use std::{
collections::HashMap,
ffi::c_void,
ops::{Deref, DerefMut},
sync::{Arc, Mutex},
time::Instant,
};
#[cfg(feature = "hwcodec")]
@@ -28,7 +28,6 @@ use hbb_common::{
SupportedDecoding, SupportedEncoding, VideoFrame,
},
sysinfo::System,
tokio::time::Instant,
ResultType,
};
@@ -264,15 +263,20 @@ impl Encoder {
.unwrap_or((PreferCodec::Auto.into(), 0));
let preference = most_frequent.enum_value_or(PreferCodec::Auto);
// auto: h265 > h264 > vp9/vp8
let mut auto_codec = CodecFormat::VP9;
// auto: h265 > h264 > av1/vp9/vp8
let av1_test = Config::get_option(hbb_common::config::keys::OPTION_AV1_TEST) != "N";
let mut auto_codec = if av1_useable && av1_test {
CodecFormat::AV1
} else {
CodecFormat::VP9
};
if h264_useable {
auto_codec = CodecFormat::H264;
}
if h265_useable {
auto_codec = CodecFormat::H265;
}
if auto_codec == CodecFormat::VP9 {
if auto_codec == CodecFormat::VP9 || auto_codec == CodecFormat::AV1 {
let mut system = System::new();
system.refresh_memory();
if vp8_useable && system.total_memory() <= 4 * 1024 * 1024 * 1024 {
@@ -982,3 +986,124 @@ fn disable_av1() -> bool {
// disable it for all 32 bit platforms
std::mem::size_of::<usize>() == 4
}
#[cfg(not(target_os = "ios"))]
pub fn test_av1() {
use hbb_common::config::keys::OPTION_AV1_TEST;
use hbb_common::rand::Rng;
use std::{sync::Once, time::Duration};
if disable_av1() || !Config::get_option(OPTION_AV1_TEST).is_empty() {
log::info!("skip test av1");
return;
}
static ONCE: Once = Once::new();
ONCE.call_once(|| {
let f = || {
let (width, height, quality, keyframe_interval, i444) =
(1920, 1080, Quality::Balanced, None, false);
let frame_count = 10;
let block_size = 300;
let move_step = 50;
let generate_fake_data =
|frame_index: u32, dst_fmt: EncodeYuvFormat| -> ResultType<Vec<u8>> {
let mut rng = hbb_common::rand::thread_rng();
let mut bgra = vec![0u8; (width * height * 4) as usize];
let gradient = frame_index as f32 / frame_count as f32;
// floating block
let x0 = (frame_index * move_step) % (width - block_size);
let y0 = (frame_index * move_step) % (height - block_size);
// Fill the block with random colors
for y in 0..block_size {
for x in 0..block_size {
let index = (((y0 + y) * width + x0 + x) * 4) as usize;
if index + 3 < bgra.len() {
let noise = rng.gen_range(0..255) as f32 / 255.0;
let value = (255.0 * gradient + noise * 50.0) as u8;
bgra[index] = value;
bgra[index + 1] = value;
bgra[index + 2] = value;
bgra[index + 3] = 255;
}
}
}
let dst_stride_y = dst_fmt.stride[0];
let dst_stride_uv = dst_fmt.stride[1];
let mut dst = vec![0u8; (dst_fmt.h * dst_stride_y * 2) as usize];
let dst_y = dst.as_mut_ptr();
let dst_u = dst[dst_fmt.u..].as_mut_ptr();
let dst_v = dst[dst_fmt.v..].as_mut_ptr();
let res = unsafe {
crate::ARGBToI420(
bgra.as_ptr(),
(width * 4) as _,
dst_y,
dst_stride_y as _,
dst_u,
dst_stride_uv as _,
dst_v,
dst_stride_uv as _,
width as _,
height as _,
)
};
if res != 0 {
bail!("ARGBToI420 failed: {}", res);
}
Ok(dst)
};
let Ok(mut av1) = AomEncoder::new(
EncoderCfg::AOM(AomEncoderConfig {
width,
height,
quality,
keyframe_interval,
}),
i444,
) else {
return false;
};
let mut key_frame_time = Duration::ZERO;
let mut non_key_frame_time_sum = Duration::ZERO;
let pts = Instant::now();
let yuvfmt = av1.yuvfmt();
for i in 0..frame_count {
let Ok(yuv) = generate_fake_data(i, yuvfmt.clone()) else {
return false;
};
let start = Instant::now();
if av1
.encode(pts.elapsed().as_millis() as _, &yuv, super::STRIDE_ALIGN)
.is_err()
{
log::debug!("av1 encode failed");
if i == 0 {
return false;
}
}
if i == 0 {
key_frame_time = start.elapsed();
} else {
non_key_frame_time_sum += start.elapsed();
}
}
let non_key_frame_time = non_key_frame_time_sum / (frame_count - 1);
log::info!(
"av1 time: key: {:?}, non-key: {:?}, consume: {:?}",
key_frame_time,
non_key_frame_time,
pts.elapsed()
);
key_frame_time < Duration::from_millis(90)
&& non_key_frame_time < Duration::from_millis(30)
};
std::thread::spawn(move || {
let v = f();
Config::set_option(
OPTION_AV1_TEST.to_string(),
if v { "Y" } else { "N" }.to_string(),
);
});
});
}

View File

@@ -15,7 +15,7 @@ use hbb_common::{
};
use hwcodec::{
common::{
DataFormat,
DataFormat, HwcodecErrno,
Quality::{self, *},
RateControl::{self, *},
},
@@ -31,6 +31,7 @@ const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_NV12;
pub const DEFAULT_FPS: i32 = 30;
const DEFAULT_GOP: i32 = i32::MAX;
const DEFAULT_HW_QUALITY: Quality = Quality_Default;
pub const ERR_HEVC_POC: i32 = HwcodecErrno::HWCODEC_ERR_HEVC_COULD_NOT_FIND_POC as i32;
crate::generate_call_macro!(call_yuv, false);
@@ -69,7 +70,7 @@ impl EncoderApi for HwRamEncoder {
let b = Self::convert_quality(&config.name, config.quality);
let base_bitrate = base_bitrate(config.width as _, config.height as _);
let mut bitrate = base_bitrate * b / 100;
if base_bitrate <= 0 {
if bitrate <= 0 {
bitrate = base_bitrate;
}
bitrate = Self::check_bitrate_range(&config, bitrate);
@@ -179,7 +180,7 @@ impl EncoderApi for HwRamEncoder {
let b = Self::convert_quality(&self.config.name, quality);
let mut bitrate = base_bitrate(self.config.width as _, self.config.height as _) * b / 100;
if bitrate > 0 {
bitrate = Self::check_bitrate_range(&self.config, self.bitrate);
bitrate = Self::check_bitrate_range(&self.config, bitrate);
self.encoder.set_bitrate(bitrate as _).ok();
self.bitrate = bitrate;
}

View File

@@ -158,6 +158,7 @@ impl Recorder {
#[cfg(not(feature = "hwcodec"))]
_ => bail!("unsupported codec type"),
};
// pts is None when new inner is created
self.pts = None;
self.send_state(RecordState::NewFile(ctx2.filename.clone()));
}
@@ -194,33 +195,33 @@ impl Recorder {
match frame {
video_frame::Union::Vp8s(vp8s) => {
for f in vp8s.frames.iter() {
self.check_pts(f.pts, w, h, format)?;
self.check_pts(f.pts, f.key, w, h, format)?;
self.as_mut().map(|x| x.write_video(f));
}
}
video_frame::Union::Vp9s(vp9s) => {
for f in vp9s.frames.iter() {
self.check_pts(f.pts, w, h, format)?;
self.check_pts(f.pts, f.key, w, h, format)?;
self.as_mut().map(|x| x.write_video(f));
}
}
video_frame::Union::Av1s(av1s) => {
for f in av1s.frames.iter() {
self.check_pts(f.pts, w, h, format)?;
self.check_pts(f.pts, f.key, w, h, format)?;
self.as_mut().map(|x| x.write_video(f));
}
}
#[cfg(feature = "hwcodec")]
video_frame::Union::H264s(h264s) => {
for f in h264s.frames.iter() {
self.check_pts(f.pts, w, h, format)?;
self.check_pts(f.pts, f.key, w, h, format)?;
self.as_mut().map(|x| x.write_video(f));
}
}
#[cfg(feature = "hwcodec")]
video_frame::Union::H265s(h265s) => {
for f in h265s.frames.iter() {
self.check_pts(f.pts, w, h, format)?;
self.check_pts(f.pts, f.key, w, h, format)?;
self.as_mut().map(|x| x.write_video(f));
}
}
@@ -230,8 +231,18 @@ impl Recorder {
Ok(())
}
fn check_pts(&mut self, pts: i64, w: usize, h: usize, format: CodecFormat) -> ResultType<()> {
fn check_pts(
&mut self,
pts: i64,
key: bool,
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
if self.pts.is_none() && !key {
bail!("first frame is not key frame");
}
let old_pts = self.pts;
self.pts = Some(pts);
if old_pts.clone().unwrap_or_default() > pts {
@@ -342,7 +353,7 @@ impl Drop for WebmRecorder {
#[cfg(feature = "hwcodec")]
struct HwRecorder {
muxer: Muxer,
muxer: Option<Muxer>,
ctx: RecorderContext,
ctx2: RecorderContext2,
written: bool,
@@ -362,7 +373,7 @@ impl RecorderApi for HwRecorder {
})
.map_err(|_| anyhow!("Failed to create hardware muxer"))?;
Ok(HwRecorder {
muxer,
muxer: Some(muxer),
ctx,
ctx2,
written: false,
@@ -376,7 +387,11 @@ impl RecorderApi for HwRecorder {
self.key = true;
}
if self.key {
let ok = self.muxer.write_video(&frame.data, frame.key).is_ok();
let ok = self
.muxer
.as_mut()
.map(|m| m.write_video(&frame.data, frame.key).is_ok())
.unwrap_or_default();
if ok {
self.written = true;
}
@@ -390,9 +405,11 @@ impl RecorderApi for HwRecorder {
#[cfg(feature = "hwcodec")]
impl Drop for HwRecorder {
fn drop(&mut self) {
self.muxer.write_tail().ok();
self.muxer.as_mut().map(|m| m.write_tail().ok());
let mut state = RecordState::WriteTail;
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
// The process cannot access the file because it is being used by another process
self.muxer = None;
std::fs::remove_file(&self.ctx2.filename).ok();
state = RecordState::RemoveFile;
}

View File

@@ -64,7 +64,7 @@ impl EncoderApi for VRamEncoder {
let b = Self::convert_quality(config.quality, &config.feature);
let base_bitrate = base_bitrate(config.width as _, config.height as _);
let mut bitrate = base_bitrate * b / 100;
if base_bitrate <= 0 {
if bitrate <= 0 {
bitrate = base_bitrate;
}
let gop = config.keyframe_interval.unwrap_or(MAX_GOP as _) as i32;

View File

@@ -7,10 +7,11 @@ use winapi::{
shared::{
dxgi::*,
dxgi1_2::*,
dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM,
dxgitype::*,
minwindef::{DWORD, FALSE, TRUE, UINT},
ntdef::LONG,
windef::HMONITOR,
windef::{HMONITOR, RECT},
winerror::*,
// dxgiformat::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE},
},
@@ -57,6 +58,7 @@ pub struct Capturer {
saved_raw_data: Vec<u8>, // for faster compare and copy
output_texture: bool,
adapter_desc1: DXGI_ADAPTER_DESC1,
rotate: Rotate,
}
impl Capturer {
@@ -151,6 +153,7 @@ impl Capturer {
(*duplication).GetDesc(&mut desc);
}
}
let rotate = Self::create_rotations(device.0, context.0, &display);
Ok(Capturer {
device,
@@ -168,9 +171,143 @@ impl Capturer {
saved_raw_data: Vec::new(),
output_texture: false,
adapter_desc1,
rotate,
})
}
fn create_rotations(
device: *mut ID3D11Device,
context: *mut ID3D11DeviceContext,
display: &Display,
) -> Rotate {
let mut video_context: *mut ID3D11VideoContext = ptr::null_mut();
let mut video_device: *mut ID3D11VideoDevice = ptr::null_mut();
let mut video_processor_enum: *mut ID3D11VideoProcessorEnumerator = ptr::null_mut();
let mut video_processor: *mut ID3D11VideoProcessor = ptr::null_mut();
let processor_rotation = match display.rotation() {
DXGI_MODE_ROTATION_ROTATE90 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_90),
DXGI_MODE_ROTATION_ROTATE180 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_180),
DXGI_MODE_ROTATION_ROTATE270 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_270),
_ => None,
};
if let Some(processor_rotation) = processor_rotation {
println!("create rotations");
if !device.is_null() && !context.is_null() {
unsafe {
(*context).QueryInterface(
&IID_ID3D11VideoContext,
&mut video_context as *mut *mut _ as *mut *mut _,
);
if !video_context.is_null() {
(*device).QueryInterface(
&IID_ID3D11VideoDevice,
&mut video_device as *mut *mut _ as *mut *mut _,
);
if !video_device.is_null() {
let (input_width, input_height) = match display.rotation() {
DXGI_MODE_ROTATION_ROTATE90 | DXGI_MODE_ROTATION_ROTATE270 => {
(display.height(), display.width())
}
_ => (display.width(), display.height()),
};
let (output_width, output_height) = (display.width(), display.height());
let content_desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC {
InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
InputFrameRate: DXGI_RATIONAL {
Numerator: 30,
Denominator: 1,
},
InputWidth: input_width as _,
InputHeight: input_height as _,
OutputFrameRate: DXGI_RATIONAL {
Numerator: 30,
Denominator: 1,
},
OutputWidth: output_width as _,
OutputHeight: output_height as _,
Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
};
(*video_device).CreateVideoProcessorEnumerator(
&content_desc,
&mut video_processor_enum,
);
if !video_processor_enum.is_null() {
let mut caps: D3D11_VIDEO_PROCESSOR_CAPS = mem::zeroed();
if S_OK == (*video_processor_enum).GetVideoProcessorCaps(&mut caps)
{
if caps.FeatureCaps
& D3D11_VIDEO_PROCESSOR_FEATURE_CAPS_ROTATION
!= 0
{
(*video_device).CreateVideoProcessor(
video_processor_enum,
0,
&mut video_processor,
);
if !video_processor.is_null() {
(*video_context).VideoProcessorSetStreamRotation(
video_processor,
0,
TRUE,
processor_rotation,
);
(*video_context)
.VideoProcessorSetStreamAutoProcessingMode(
video_processor,
0,
FALSE,
);
(*video_context).VideoProcessorSetStreamFrameFormat(
video_processor,
0,
D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
);
(*video_context).VideoProcessorSetStreamSourceRect(
video_processor,
0,
TRUE,
&RECT {
left: 0,
top: 0,
right: input_width as _,
bottom: input_height as _,
},
);
(*video_context).VideoProcessorSetStreamDestRect(
video_processor,
0,
TRUE,
&RECT {
left: 0,
top: 0,
right: output_width as _,
bottom: output_height as _,
},
);
}
}
}
}
}
}
}
}
}
let video_context = ComPtr(video_context);
let video_device = ComPtr(video_device);
let video_processor_enum = ComPtr(video_processor_enum);
let video_processor = ComPtr(video_processor);
let rotated_texture = ComPtr(ptr::null_mut());
Rotate {
video_context,
video_device,
video_processor_enum,
video_processor,
texture: (rotated_texture, false),
}
}
pub fn is_gdi(&self) -> bool {
self.gdi_capturer.is_some()
}
@@ -253,17 +390,7 @@ impl Capturer {
pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result<Frame<'a>> {
if self.output_texture {
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)))
Ok(Frame::Texture(self.get_texture(timeout)?))
} else {
let width = self.width;
let height = self.height;
@@ -338,7 +465,7 @@ impl Capturer {
}
}
fn get_texture(&mut self, timeout: UINT) -> io::Result<*mut c_void> {
fn get_texture(&mut self, timeout: UINT) -> io::Result<(*mut c_void, usize)> {
unsafe {
if self.duplication.0.is_null() {
return Err(std::io::ErrorKind::AddrNotAvailable.into());
@@ -362,7 +489,86 @@ impl Capturer {
);
let texture = ComPtr(texture);
self.texture = texture;
Ok(self.texture.0 as *mut c_void)
let mut final_texture = self.texture.0 as *mut c_void;
let mut rotation = match self.display.rotation() {
DXGI_MODE_ROTATION_ROTATE90 => 90,
DXGI_MODE_ROTATION_ROTATE180 => 180,
DXGI_MODE_ROTATION_ROTATE270 => 270,
_ => 0,
};
if rotation != 0
&& !self.texture.is_null()
&& !self.rotate.video_context.is_null()
&& !self.rotate.video_device.is_null()
&& !self.rotate.video_processor_enum.is_null()
&& !self.rotate.video_processor.is_null()
{
let mut desc: D3D11_TEXTURE2D_DESC = mem::zeroed();
(*self.texture.0).GetDesc(&mut desc);
if rotation == 90 || rotation == 270 {
let tmp = desc.Width;
desc.Width = desc.Height;
desc.Height = tmp;
}
if !self.rotate.texture.1 {
self.rotate.texture.1 = true;
let mut rotated_texture: *mut ID3D11Texture2D = ptr::null_mut();
desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
(*self.device.0).CreateTexture2D(&desc, ptr::null(), &mut rotated_texture);
self.rotate.texture.0 = ComPtr(rotated_texture);
}
if !self.rotate.texture.0.is_null()
&& desc.Width == self.width as u32
&& desc.Height == self.height as u32
{
let input_view_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC {
FourCC: 0,
ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D,
Texture2D: D3D11_TEX2D_VPIV {
ArraySlice: 0,
MipSlice: 0,
},
};
let mut input_view = ptr::null_mut();
(*self.rotate.video_device.0).CreateVideoProcessorInputView(
self.texture.0 as *mut _,
self.rotate.video_processor_enum.0 as *mut _,
&input_view_desc,
&mut input_view,
);
if !input_view.is_null() {
let input_view = ComPtr(input_view);
let mut output_view_desc: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC =
mem::zeroed();
output_view_desc.ViewDimension = D3D11_VPOV_DIMENSION_TEXTURE2D;
output_view_desc.u.Texture2D_mut().MipSlice = 0;
let mut output_view = ptr::null_mut();
(*self.rotate.video_device.0).CreateVideoProcessorOutputView(
self.rotate.texture.0 .0 as *mut _,
self.rotate.video_processor_enum.0 as *mut _,
&output_view_desc,
&mut output_view,
);
if !output_view.is_null() {
let output_view = ComPtr(output_view);
let mut stream_data: D3D11_VIDEO_PROCESSOR_STREAM = mem::zeroed();
stream_data.Enable = TRUE;
stream_data.pInputSurface = input_view.0;
(*self.rotate.video_context.0).VideoProcessorBlt(
self.rotate.video_processor.0,
output_view.0,
0,
1,
&stream_data,
);
final_texture = self.rotate.texture.0 .0 as *mut c_void;
rotation = 0;
}
}
}
}
Ok((final_texture, rotation))
}
}
@@ -666,3 +872,11 @@ fn wrap_hresult(x: HRESULT) -> io::Result<()> {
})
.into())
}
struct Rotate {
video_context: ComPtr<ID3D11VideoContext>,
video_device: ComPtr<ID3D11VideoDevice>,
video_processor_enum: ComPtr<ID3D11VideoProcessorEnumerator>,
video_processor: ComPtr<ID3D11VideoProcessor>,
texture: (ComPtr<ID3D11Texture2D>, bool),
}

View File

@@ -1,5 +1,5 @@
pkgname=rustdesk
pkgver=1.3.2
pkgver=1.3.3
pkgrel=0
epoch=
pkgdesc=""

View File

@@ -1,8 +1,10 @@
Name: rustdesk
Version: 1.3.2
Version: 1.3.3
Release: 0
Summary: RPM package
License: GPL-3.0
URL: https://rustdesk.com
Vendor: rustdesk <info@rustdesk.com>
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
Recommends: libayatana-appindicator3-1
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)

View File

@@ -1,8 +1,10 @@
Name: rustdesk
Version: 1.3.2
Version: 1.3.3
Release: 0
Summary: RPM package
License: GPL-3.0
URL: https://rustdesk.com
Vendor: rustdesk <info@rustdesk.com>
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau libva pam gstreamer1-plugins-base
Recommends: libayatana-appindicator-gtk3
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)

View File

@@ -1,8 +1,10 @@
Name: rustdesk
Version: 1.3.2
Version: 1.3.3
Release: 0
Summary: RPM package
License: GPL-3.0
URL: https://rustdesk.com
Vendor: rustdesk <info@rustdesk.com>
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau1 libva2 pam gstreamer1-plugins-base
Recommends: libayatana-appindicator-gtk3

View File

@@ -1,15 +0,0 @@
cmake_minimum_required(VERSION 3.20)
project(oboe_wrapper CXX)
include(GNUInstallDirs)
add_library(oboe_wrapper STATIC
oboe.cc
)
target_include_directories(oboe_wrapper PRIVATE "${CURRENT_INSTALLED_DIR}/include")
install(TARGETS oboe_wrapper
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")

View File

@@ -1,118 +0,0 @@
#include <oboe/Oboe.h>
#include <math.h>
#include <deque>
#include <pthread.h>
// I got link problem with std::mutex, so use pthread instead
class CThreadLock
{
public:
CThreadLock();
virtual ~CThreadLock();
void Lock();
void Unlock();
private:
pthread_mutex_t mutexlock;
};
CThreadLock::CThreadLock()
{
// init lock here
pthread_mutex_init(&mutexlock, 0);
}
CThreadLock::~CThreadLock()
{
// deinit lock here
pthread_mutex_destroy(&mutexlock);
}
void CThreadLock::Lock()
{
// lock
pthread_mutex_lock(&mutexlock);
}
void CThreadLock::Unlock()
{
// unlock
pthread_mutex_unlock(&mutexlock);
}
class Player : public oboe::AudioStreamDataCallback
{
public:
Player(int channels, int sample_rate)
{
this->channels = channels;
oboe::AudioStreamBuilder builder;
// The builder set methods can be chained for convenience.
builder.setSharingMode(oboe::SharingMode::Exclusive)
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
->setChannelCount(channels)
->setSampleRate(sample_rate)
->setFormat(oboe::AudioFormat::Float)
->setDataCallback(this)
->openManagedStream(outStream);
// Typically, start the stream after querying some stream information, as well as some input from the user
outStream->requestStart();
}
~Player() {
outStream->requestStop();
}
oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) override
{
float *floatData = (float *)audioData;
int i = 0;
mtx.Lock();
auto n = channels * numFrames;
for (; i < n && i < (int)buffer.size(); ++i, ++floatData)
{
*floatData = buffer.front();
buffer.pop_front();
}
mtx.Unlock();
for (; i < n; ++i, ++floatData)
{
*floatData = 0;
}
return oboe::DataCallbackResult::Continue;
}
void push(const float *v, int n)
{
mtx.Lock();
for (auto i = 0; i < n; ++i, ++v)
buffer.push_back(*v);
// in case memory overuse
if (buffer.size() > 48 * 1024 * 120)
buffer.clear();
mtx.Unlock();
}
private:
oboe::ManagedStream outStream;
int channels;
std::deque<float> buffer;
CThreadLock mtx;
};
extern "C"
{
void *create_oboe_player(int channels, int sample_rate)
{
return new Player(channels, sample_rate);
}
void push_oboe_data(void *player, const float* v, int n)
{
static_cast<Player *>(player)->push(v, n);
}
void destroy_oboe_player(void *player)
{
delete static_cast<Player *>(player);
}
}

View File

@@ -1,8 +0,0 @@
vcpkg_configure_cmake(
SOURCE_PATH "${CMAKE_CURRENT_LIST_DIR}"
OPTIONS
-DCURRENT_INSTALLED_DIR=${CURRENT_INSTALLED_DIR}
PREFER_NINJA
)
vcpkg_cmake_install()

View File

@@ -1,19 +0,0 @@
{
"name": "oboe-wrapper",
"version": "0",
"description": "None",
"dependencies": [
{
"name": "vcpkg-cmake",
"host": true
},
{
"name": "vcpkg-cmake-config",
"host": true
},
{
"name": "oboe",
"host": false
}
]
}

View File

@@ -2,14 +2,12 @@ use async_trait::async_trait;
use bytes::Bytes;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use clipboard_master::{CallbackResult, ClipboardHandler};
#[cfg(not(any(target_os = "android", target_os = "linux")))]
use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
Device, Host, StreamConfig,
};
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};
@@ -73,8 +71,10 @@ use crate::{
ui_session_interface::{InvokeUiSession, Session},
};
#[cfg(not(target_os = "ios"))]
use crate::clipboard::CLIPBOARD_INTERVAL;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::clipboard::{check_clipboard, ClipboardSide, CLIPBOARD_INTERVAL};
use crate::clipboard::{check_clipboard, ClipboardSide};
#[cfg(not(feature = "flutter"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::ui_session_interface::SessionPermissionConfig;
@@ -117,8 +117,7 @@ pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str =
pub const SCRAP_X11_REQUIRED: &str = "x11 expected";
pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required";
#[cfg(not(any(target_os = "android", target_os = "linux")))]
pub const AUDIO_BUFFER_MS: usize = 150;
pub const AUDIO_BUFFER_MS: usize = 3000;
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -134,13 +133,12 @@ pub(crate) struct ClientClipboardContext {
/// Client of the remote desktop.
pub struct Client;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
struct TextClipboardState {
is_required: bool,
running: bool,
}
#[cfg(not(any(target_os = "android", target_os = "linux")))]
lazy_static::lazy_static! {
static ref AUDIO_HOST: Host = cpal::default_host();
}
@@ -148,6 +146,10 @@ lazy_static::lazy_static! {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
lazy_static::lazy_static! {
static ref ENIGO: Arc<Mutex<enigo::Enigo>> = Arc::new(Mutex::new(enigo::Enigo::new()));
}
#[cfg(not(target_os = "ios"))]
lazy_static::lazy_static! {
static ref TEXT_CLIPBOARD_STATE: Arc<Mutex<TextClipboardState>> = Arc::new(Mutex::new(TextClipboardState::new()));
}
@@ -163,66 +165,6 @@ pub fn get_key_state(key: enigo::Key) -> bool {
ENIGO.lock().unwrap().get_key_state(key)
}
cfg_if::cfg_if! {
if #[cfg(target_os = "android")] {
use hbb_common::libc::{c_float, c_int};
type Oboe = *mut c_void;
extern "C" {
fn create_oboe_player(channels: c_int, sample_rate: c_int) -> Oboe;
fn push_oboe_data(oboe: Oboe, d: *const c_float, n: c_int);
fn destroy_oboe_player(oboe: Oboe);
}
struct OboePlayer {
raw: Oboe,
}
impl Default for OboePlayer {
fn default() -> Self {
Self {
raw: std::ptr::null_mut(),
}
}
}
impl OboePlayer {
fn new(channels: i32, sample_rate: i32) -> Self {
unsafe {
Self {
raw: create_oboe_player(channels, sample_rate),
}
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn is_null(&self) -> bool {
self.raw.is_null()
}
fn push(&mut self, d: &[f32]) {
if self.raw.is_null() {
return;
}
unsafe {
push_oboe_data(self.raw, d.as_ptr(), d.len() as _);
}
}
}
impl Drop for OboePlayer {
fn drop(&mut self) {
unsafe {
if !self.raw.is_null() {
destroy_oboe_player(self.raw);
}
}
}
}
}
}
impl Client {
/// Start a new connection.
pub async fn start(
@@ -712,12 +654,12 @@ impl Client {
#[inline]
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
pub fn set_is_text_clipboard_required(b: bool) {
TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b;
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
fn try_stop_clipboard() {
// There's a bug here.
// If session is closed by the peer, `has_sessions_running()` will always return true.
@@ -812,9 +754,41 @@ impl Client {
Some(rx_started)
}
#[cfg(target_os = "android")]
fn try_start_clipboard(_p: Option<()>) -> Option<UnboundedReceiver<()>> {
let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap();
if clipboard_lock.running {
return None;
}
clipboard_lock.running = true;
log::info!("Start text clipboard loop");
std::thread::spawn(move || {
loop {
if !TEXT_CLIPBOARD_STATE.lock().unwrap().running {
break;
}
if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required {
std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL));
continue;
}
if let Some(msg) = crate::clipboard::get_clipboards_msg(true) {
crate::flutter::send_text_clipboard_msg(msg);
}
std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL));
}
log::info!("Stop text clipboard loop");
TEXT_CLIPBOARD_STATE.lock().unwrap().running = false;
});
None
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
impl TextClipboardState {
fn new() -> Self {
Self {
@@ -887,100 +861,135 @@ impl ClipboardHandler for ClientClipboardHandler {
#[derive(Default)]
pub struct AudioHandler {
audio_decoder: Option<(AudioDecoder, Vec<f32>)>,
#[cfg(target_os = "android")]
oboe: Option<OboePlayer>,
#[cfg(target_os = "linux")]
simple: Option<psimple::Simple>,
#[cfg(not(any(target_os = "android", target_os = "linux")))]
audio_buffer: AudioBuffer,
sample_rate: (u32, u32),
#[cfg(not(any(target_os = "android", target_os = "linux")))]
audio_stream: Option<Box<dyn StreamTrait>>,
channels: u16,
#[cfg(not(any(target_os = "android", target_os = "linux")))]
device_channel: u16,
#[cfg(not(any(target_os = "android", target_os = "linux")))]
ready: Arc<std::sync::Mutex<bool>>,
}
#[cfg(not(any(target_os = "android", target_os = "linux")))]
struct AudioBuffer(pub Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>);
struct AudioBuffer(
pub Arc<std::sync::Mutex<ringbuf::HeapRb<f32>>>,
usize,
[usize; 30],
);
#[cfg(not(any(target_os = "android", target_os = "linux")))]
impl Default for AudioBuffer {
fn default() -> Self {
Self(Arc::new(std::sync::Mutex::new(
ringbuf::HeapRb::<f32>::new(48000 * 2 * AUDIO_BUFFER_MS / 1000), // 48000hz, 2 channel
)))
Self(
Arc::new(std::sync::Mutex::new(
ringbuf::HeapRb::<f32>::new(48000 * 2 * AUDIO_BUFFER_MS / 1000), // 48000hz, 2 channel
)),
48000 * 2,
[0; 30],
)
}
}
#[cfg(not(any(target_os = "android", target_os = "linux")))]
impl AudioBuffer {
pub fn resize(&self, sample_rate: usize, channels: usize) {
pub fn resize(&mut self, sample_rate: usize, channels: usize) {
let capacity = sample_rate * channels * AUDIO_BUFFER_MS / 1000;
let old_capacity = self.0.lock().unwrap().capacity();
if capacity != old_capacity {
*self.0.lock().unwrap() = ringbuf::HeapRb::<f32>::new(capacity);
self.1 = sample_rate * channels;
log::info!("Audio buffer resized from {old_capacity} to {capacity}");
}
}
// clear when full to avoid long time noise
#[inline]
pub fn clear_if_full(&self) {
let full = self.0.lock().unwrap().is_full();
if full {
self.0.lock().unwrap().clear();
log::trace!("Audio buffer cleared");
fn try_shrink(&mut self, having: usize) {
extern crate chrono;
use chrono::prelude::*;
let mut i = (having * 10) / self.1;
if i > 29 {
i = 29;
}
self.2[i] += 1;
#[allow(non_upper_case_globals)]
static mut tms: i64 = 0;
let dt = Local::now().timestamp_millis();
unsafe {
if tms == 0 {
tms = dt;
return;
} else if dt < tms + 12000 {
return;
}
tms = dt;
}
// the safer water mark to drop
let mut zero = 0;
// the water mark taking most of time
let mut max = 0;
for i in 0..30 {
if self.2[i] == 0 && zero == i {
zero += 1;
}
if self.2[i] > self.2[max] {
self.2[max] = 0;
max = i;
} else {
self.2[i] = 0;
}
}
zero = zero * 2 / 3;
// how many data can be dropped:
// 1. will not drop if buffered data is less than 600ms
// 2. choose based on min(zero, max)
const N: usize = 4;
self.2[max] = 0;
if max < 6 {
return;
} else if max > zero * N {
max = zero * N;
}
let mut lock = self.0.lock().unwrap();
let cap = lock.capacity();
let having = lock.occupied_len();
let skip = (cap * max / (30 * N) + 1) & (!1);
if (having > skip * 3) && (skip > 0) {
lock.skip(skip);
log::info!("skip {skip}, based {max} {zero}");
}
}
/// append pcm to audio buffer, if buffered data
/// exceeds AUDIO_BUFFER_MS, only AUDIO_BUFFER_MS
/// will be kept.
fn append_pcm2(&self, buffer: &[f32]) -> usize {
let mut lock = self.0.lock().unwrap();
let cap = lock.capacity();
if buffer.len() > cap {
lock.push_slice_overwrite(buffer);
return cap;
}
let having = lock.occupied_len() + buffer.len();
if having > cap {
lock.skip(having - cap);
}
lock.push_slice_overwrite(buffer);
lock.occupied_len()
}
/// append pcm to audio buffer, trying to drop data
/// when data is too much (per 12 seconds) based
/// statistics.
pub fn append_pcm(&mut self, buffer: &[f32]) {
let having = self.append_pcm2(buffer);
self.try_shrink(having);
}
}
impl AudioHandler {
/// Start the audio playback.
#[cfg(target_os = "linux")]
fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> {
use psimple::Simple;
use pulse::sample::{Format, Spec};
use pulse::stream::Direction;
let spec = Spec {
format: Format::F32le,
channels: format0.channels as _,
rate: format0.sample_rate as _,
};
if !spec.is_valid() {
bail!("Invalid audio format");
}
self.simple = Some(Simple::new(
None, // Use the default server
&crate::get_app_name(), // Our applications name
Direction::Playback, // We want a playback stream
None, // Use the default device
"playback", // Description of our stream
&spec, // Our sample format
None, // Use default channel map
None, // Use default buffering attributes
)?);
self.sample_rate = (format0.sample_rate, format0.sample_rate);
Ok(())
}
/// Start the audio playback.
#[cfg(target_os = "android")]
fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> {
self.oboe = Some(OboePlayer::new(
format0.channels as _,
format0.sample_rate as _,
));
self.sample_rate = (format0.sample_rate, format0.sample_rate);
Ok(())
}
/// Start the audio playback.
#[cfg(not(any(target_os = "android", target_os = "linux")))]
fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> {
let device = AUDIO_HOST
.default_output_device()
@@ -993,7 +1002,9 @@ impl AudioHandler {
let sample_format = config.sample_format();
log::info!("Default output format: {:?}", config);
log::info!("Remote input format: {:?}", format0);
let config: StreamConfig = config.into();
let mut config: StreamConfig = config.into();
config.buffer_size = cpal::BufferSize::Fixed(64);
self.sample_rate = (format0.sample_rate, config.sample_rate.0);
let mut build_output_stream = |config: StreamConfig| match sample_format {
cpal::SampleFormat::I8 => self.build_output_stream::<i8>(&config, &device),
@@ -1041,28 +1052,16 @@ impl AudioHandler {
/// Handle audio frame and play it.
#[inline]
pub fn handle_frame(&mut self, frame: AudioFrame) {
#[cfg(not(any(target_os = "android", target_os = "linux")))]
if self.audio_stream.is_none() || !self.ready.lock().unwrap().clone() {
return;
}
#[cfg(target_os = "linux")]
if self.simple.is_none() {
log::debug!("PulseAudio simple binding does not exists");
return;
}
#[cfg(target_os = "android")]
if self.oboe.is_none() {
return;
}
self.audio_decoder.as_mut().map(|(d, buffer)| {
if let Ok(n) = d.decode_float(&frame.data, buffer, false) {
let channels = self.channels;
let n = n * (channels as usize);
#[cfg(not(any(target_os = "android", target_os = "linux")))]
{
let sample_rate0 = self.sample_rate.0;
let sample_rate = self.sample_rate.1;
let audio_buffer = self.audio_buffer.0.clone();
let mut buffer = buffer[0..n].to_owned();
if sample_rate != sample_rate0 {
buffer = crate::audio_resample(
@@ -1081,25 +1080,13 @@ impl AudioHandler {
self.device_channel,
);
}
self.audio_buffer.clear_if_full();
audio_buffer.lock().unwrap().push_slice_overwrite(&buffer);
}
#[cfg(target_os = "android")]
{
self.oboe.as_mut().map(|x| x.push(&buffer[0..n]));
}
#[cfg(target_os = "linux")]
{
let data_u8 =
unsafe { std::slice::from_raw_parts::<u8>(buffer.as_ptr() as _, n * 4) };
self.simple.as_mut().map(|x| x.write(data_u8));
self.audio_buffer.append_pcm(&buffer);
}
}
});
}
/// Build audio output stream for current device.
#[cfg(not(any(target_os = "android", target_os = "linux")))]
fn build_output_stream<T: cpal::Sample + cpal::SizedSample + cpal::FromSample<f32>>(
&mut self,
config: &StreamConfig,
@@ -1117,18 +1104,46 @@ impl AudioHandler {
let timeout = None;
let stream = device.build_output_stream(
config,
move |data: &mut [T], _: &_| {
move |data: &mut [T], info: &cpal::OutputCallbackInfo| {
if !*ready.lock().unwrap() {
*ready.lock().unwrap() = true;
}
let mut lock = audio_buffer.lock().unwrap();
let mut n = data.len();
if lock.occupied_len() < n {
n = lock.occupied_len();
let mut lock = audio_buffer.lock().unwrap();
let mut having = lock.occupied_len();
// android two timestamps, one from zero, another not
#[cfg(not(target_os = "android"))]
if having < n {
let tms = info.timestamp();
let how_long = tms
.playback
.duration_since(&tms.callback)
.unwrap_or(Duration::from_millis(0));
// must long enough to fight back scheuler delay
if how_long > Duration::from_millis(6) && how_long < Duration::from_millis(3000)
{
drop(lock);
std::thread::sleep(how_long.div_f32(1.2));
lock = audio_buffer.lock().unwrap();
having = lock.occupied_len();
}
if having < n {
n = having;
}
}
#[cfg(target_os = "android")]
if having < n {
n = having;
}
let mut elems = vec![0.0f32; n];
lock.pop_slice(&mut elems);
if n > 0 {
lock.pop_slice(&mut elems);
}
drop(lock);
let mut input = elems.into_iter();
for sample in data.iter_mut() {
*sample = match input.next() {
@@ -1394,18 +1409,18 @@ impl LoginConfigHandler {
let server = server_key.next().unwrap_or_default();
let args = server_key.next().unwrap_or_default();
let key = if server == PUBLIC_SERVER {
PUBLIC_RS_PUB_KEY
PUBLIC_RS_PUB_KEY.to_owned()
} else {
let mut args_map: HashMap<&str, &str> = HashMap::new();
let mut args_map: HashMap<String, &str> = HashMap::new();
for arg in args.split('&') {
if let Some(kv) = arg.find('=') {
let k = &arg[0..kv];
let k = arg[0..kv].to_lowercase();
let v = &arg[kv + 1..];
args_map.insert(k, v);
}
}
let key = args_map.remove("key").unwrap_or_default();
key
key.to_owned()
};
// here we can check <id>/r@server
@@ -1413,7 +1428,7 @@ impl LoginConfigHandler {
if real_id != raw_id {
force_relay = true;
}
self.other_server = Some((real_id.clone(), server.to_owned(), key.to_owned()));
self.other_server = Some((real_id.clone(), server.to_owned(), key));
id = format!("{real_id}@{server}");
} else {
let real_id = crate::ui_interface::handle_relay_id(&id);
@@ -2260,74 +2275,60 @@ impl LoginConfigHandler {
/// Media data.
pub enum MediaData {
VideoQueue(usize),
VideoQueue,
VideoFrame(Box<VideoFrame>),
AudioFrame(Box<AudioFrame>),
AudioFormat(AudioFormat),
Reset(Option<usize>),
Reset,
RecordScreen(bool),
}
pub type MediaSender = mpsc::Sender<MediaData>;
struct VideoHandlerController {
handler: VideoHandler,
skip_beginning: u32,
}
/// Start video and audio thread.
/// Return two [`MediaSender`], they should be given to the media producer.
/// Start video thread.
///
/// # Arguments
///
/// * `video_callback` - The callback for video frame. Being called when a video frame is ready.
pub fn start_video_audio_threads<F, T>(
pub fn start_video_thread<F, T>(
session: Session<T>,
display: usize,
video_receiver: mpsc::Receiver<MediaData>,
video_queue: Arc<RwLock<ArrayQueue<VideoFrame>>>,
fps: Arc<RwLock<Option<usize>>>,
chroma: Arc<RwLock<Option<Chroma>>>,
discard_queue: Arc<RwLock<bool>>,
video_callback: F,
) -> (
MediaSender,
MediaSender,
Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
Arc<RwLock<Option<usize>>>,
Arc<RwLock<Option<Chroma>>>,
)
where
) where
F: 'static + FnMut(usize, &mut scrap::ImageRgb, *mut c_void, bool) + Send,
T: InvokeUiSession,
{
let (video_sender, video_receiver) = mpsc::channel::<MediaData>();
let video_queue_map: Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>> = Default::default();
let video_queue_map_cloned = video_queue_map.clone();
let mut video_callback = video_callback;
let fps = Arc::new(RwLock::new(None));
let decode_fps_map = fps.clone();
let chroma = Arc::new(RwLock::new(None));
let chroma_cloned = chroma.clone();
let mut last_chroma = None;
std::thread::spawn(move || {
#[cfg(windows)]
sync_cpu_usage();
get_hwcodec_config();
let mut handler_controller_map = HashMap::new();
let mut video_handler = None;
let mut count = 0;
let mut duration = std::time::Duration::ZERO;
let mut skip_beginning = 0;
loop {
if let Ok(data) = video_receiver.recv() {
match data {
MediaData::VideoFrame(_) | MediaData::VideoQueue(_) => {
MediaData::VideoFrame(_) | MediaData::VideoQueue => {
let vf = match data {
MediaData::VideoFrame(vf) => *vf,
MediaData::VideoQueue(display) => {
if let Some(video_queue) =
video_queue_map.read().unwrap().get(&display)
{
if let Some(vf) = video_queue.pop() {
vf
} else {
MediaData::VideoFrame(vf) => {
*discard_queue.write().unwrap() = false;
*vf
}
MediaData::VideoQueue => {
if let Some(vf) = video_queue.read().unwrap().pop() {
if discard_queue.read().unwrap().clone() {
continue;
}
vf
} else {
continue;
}
@@ -2340,36 +2341,25 @@ where
let display = vf.display as usize;
let start = std::time::Instant::now();
let format = CodecFormat::from(&vf);
if !handler_controller_map.contains_key(&display) {
if video_handler.is_none() {
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,
skip_beginning: 0,
},
);
video_handler = Some(handler);
}
if let Some(handler_controller) = handler_controller_map.get_mut(&display) {
if let Some(handler) = video_handler.as_mut() {
let mut pixelbuffer = true;
let mut tmp_chroma = None;
let format_changed =
handler_controller.handler.decoder.format() != format;
match handler_controller.handler.handle_frame(
vf,
&mut pixelbuffer,
&mut tmp_chroma,
) {
let format_changed = handler.decoder.format() != format;
match handler.handle_frame(vf, &mut pixelbuffer, &mut tmp_chroma) {
Ok(true) => {
video_callback(
display,
&mut handler_controller.handler.rgb,
handler_controller.handler.texture.texture,
&mut handler.rgb,
handler.texture.texture,
pixelbuffer,
);
@@ -2381,7 +2371,7 @@ where
// fps calculation
fps_calculate(
handler_controller,
&mut skip_beginning,
&fps,
format_changed,
start.elapsed(),
@@ -2410,53 +2400,36 @@ where
// check invalid decoders
let mut should_update_supported = false;
handler_controller_map
.iter()
.map(|(_, h)| {
if !h.handler.decoder.valid() || h.handler.fail_counter >= MAX_DECODE_FAIL_COUNTER {
let mut lc = session.lc.write().unwrap();
let format = h.handler.decoder.format();
if !lc.mark_unsupported.contains(&format) {
lc.mark_unsupported.push(format);
should_update_supported = true;
log::info!("mark {format:?} decoder as unsupported, valid:{}, fail_counter:{}, all unsupported:{:?}", h.handler.decoder.valid(), h.handler.fail_counter, lc.mark_unsupported);
}
if let Some(handler) = video_handler.as_mut() {
if !handler.decoder.valid()
|| handler.fail_counter >= MAX_DECODE_FAIL_COUNTER
{
let mut lc = session.lc.write().unwrap();
let format = handler.decoder.format();
if !lc.mark_unsupported.contains(&format) {
lc.mark_unsupported.push(format);
should_update_supported = true;
log::info!("mark {format:?} decoder as unsupported, valid:{}, fail_counter:{}, all unsupported:{:?}", handler.decoder.valid(), handler.fail_counter, lc.mark_unsupported);
}
})
.count();
}
}
if should_update_supported {
session.send(Data::Message(
session.lc.read().unwrap().update_supported_decodings(),
));
}
}
MediaData::Reset(display) => {
if let Some(display) = display {
if let Some(handler_controler) =
handler_controller_map.get_mut(&display)
{
handler_controler.handler.reset(None);
}
} else {
for (_, handler_controler) in handler_controller_map.iter_mut() {
handler_controler.handler.reset(None);
}
MediaData::Reset => {
if let Some(handler) = video_handler.as_mut() {
handler.reset(None);
}
}
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,
);
}
let id = session.lc.read().unwrap().id.clone();
if let Some(handler) = video_handler.as_mut() {
handler.record_screen(start, id, display);
}
}
_ => {}
@@ -2467,14 +2440,6 @@ where
}
log::info!("Video decoder loop exits");
});
let audio_sender = start_audio_thread();
return (
video_sender,
audio_sender,
video_queue_map_cloned,
decode_fps_map,
chroma_cloned,
);
}
/// Start an audio thread
@@ -2506,7 +2471,7 @@ pub fn start_audio_thread() -> MediaSender {
#[inline]
fn fps_calculate(
handler_controller: &mut VideoHandlerController,
skip_beginning: &mut usize,
fps: &Arc<RwLock<Option<usize>>>,
format_changed: bool,
elapsed: std::time::Duration,
@@ -2516,11 +2481,11 @@ fn fps_calculate(
if format_changed {
*count = 0;
*duration = std::time::Duration::ZERO;
handler_controller.skip_beginning = 0;
*skip_beginning = 0;
}
// // The first frame will be very slow
if handler_controller.skip_beginning < 3 {
handler_controller.skip_beginning += 1;
if *skip_beginning < 3 {
*skip_beginning += 1;
return;
}
*duration += elapsed;

View File

@@ -43,6 +43,18 @@ pub trait FileManager: Interface {
self.send(Data::CancelJob(id));
}
fn read_empty_dirs(&self, path: String, include_hidden: bool) {
let mut msg_out = Message::new();
let mut file_action = FileAction::new();
file_action.set_read_empty_dirs(ReadEmptyDirs {
path,
include_hidden,
..Default::default()
});
msg_out.set_file_action(file_action);
self.send(Data::Message(msg_out));
}
fn read_remote_dir(&self, path: String, include_hidden: bool) {
let mut msg_out = Message::new();
let mut file_action = FileAction::new();

View File

@@ -1,5 +1,6 @@
use std::{
collections::HashMap,
ffi::c_void,
num::NonZeroI64,
sync::{
atomic::{AtomicUsize, Ordering},
@@ -8,9 +9,9 @@ use std::{
};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::clipboard::{update_clipboard, ClipboardSide, CLIPBOARD_INTERVAL};
use crate::clipboard::{update_clipboard, ClipboardSide};
#[cfg(not(any(target_os = "ios")))]
use crate::{audio_service, ConnInner, CLIENT_SERVER};
use crate::{audio_service, clipboard::CLIPBOARD_INTERVAL, ConnInner, CLIENT_SERVER};
use crate::{
client::{
self, new_voice_call_request, Client, Data, Interface, MediaData, MediaSender,
@@ -49,8 +50,6 @@ use scrap::CodecFormat;
pub struct Remote<T: InvokeUiSession> {
handler: Session<T>,
video_queue_map: Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
video_sender: MediaSender,
audio_sender: MediaSender,
receiver: mpsc::UnboundedReceiver<Data>,
sender: mpsc::UnboundedSender<Data>,
@@ -67,13 +66,11 @@ pub struct Remote<T: InvokeUiSession> {
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
client_conn_id: i32, // used for file clipboard
data_count: Arc<AtomicUsize>,
frame_count_map: Arc<RwLock<HashMap<usize, usize>>>,
video_format: CodecFormat,
elevation_requested: bool,
fps_control: FpsControl,
decode_fps: Arc<RwLock<Option<usize>>>,
chroma: Arc<RwLock<Option<Chroma>>>,
peer_info: ParsedPeerInfo,
video_threads: HashMap<usize, VideoThread>,
chroma: Arc<RwLock<Option<Chroma>>>,
}
#[derive(Default)]
@@ -94,20 +91,12 @@ impl ParsedPeerInfo {
impl<T: InvokeUiSession> Remote<T> {
pub fn new(
handler: Session<T>,
video_queue: Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
video_sender: MediaSender,
audio_sender: MediaSender,
receiver: mpsc::UnboundedReceiver<Data>,
sender: mpsc::UnboundedSender<Data>,
frame_count_map: Arc<RwLock<HashMap<usize, usize>>>,
decode_fps: Arc<RwLock<Option<usize>>>,
chroma: Arc<RwLock<Option<Chroma>>>,
) -> Self {
Self {
handler,
video_queue_map: video_queue,
video_sender,
audio_sender,
audio_sender: crate::client::start_audio_thread(),
receiver,
sender,
read_jobs: Vec::new(),
@@ -120,15 +109,13 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
client_conn_id: 0,
data_count: Arc::new(AtomicUsize::new(0)),
frame_count_map,
video_format: CodecFormat::Unknown,
stop_voice_call_sender: None,
voice_call_request_timestamp: None,
elevation_requested: false,
fps_control: Default::default(),
decode_fps,
chroma,
peer_info: Default::default(),
video_threads: Default::default(),
chroma: Default::default(),
}
}
@@ -250,7 +237,6 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
_ = status_timer.tick() => {
self.fps_control(direct);
let elapsed = fps_instant.elapsed().as_millis();
if elapsed < 1000 {
continue;
@@ -260,14 +246,14 @@ impl<T: InvokeUiSession> Remote<T> {
speed = speed * 1000 / elapsed as usize;
let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32);
let mut frame_count_map_write = self.frame_count_map.write().unwrap();
let frame_count_map = frame_count_map_write.clone();
frame_count_map_write.values_mut().for_each(|v| *v = 0);
drop(frame_count_map_write);
let fps = frame_count_map.iter().map(|(k, v)| {
let fps = self.video_threads.iter().map(|(k, v)| {
// Correcting the inaccuracy of status_timer
(k.clone(), (*v as i32) * 1000 / elapsed as i32)
(k.clone(), (*v.frame_count.read().unwrap() as i32) * 1000 / elapsed as i32)
}).collect::<HashMap<usize, i32>>();
self.video_threads.iter().for_each(|(_, v)| {
*v.frame_count.write().unwrap() = 0;
});
self.fps_control(direct, fps.clone());
let chroma = self.chroma.read().unwrap().clone();
let chroma = match chroma {
Some(Chroma::I444) => "4:4:4",
@@ -275,10 +261,16 @@ impl<T: InvokeUiSession> Remote<T> {
None => "-",
};
let chroma = Some(chroma.to_string());
let codec_format = if self.video_format == CodecFormat::Unknown {
None
} else {
Some(self.video_format.clone())
};
self.handler.update_quality_status(QualityStatus {
speed: Some(speed),
fps,
chroma,
codec_format,
..Default::default()
});
}
@@ -302,7 +294,7 @@ impl<T: InvokeUiSession> Remote<T> {
.unwrap()
.set_disconnected(round);
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
if _set_disconnected_ok {
Client::try_stop_clipboard();
}
@@ -498,6 +490,22 @@ impl<T: InvokeUiSession> Remote<T> {
self.check_clipboard_file_context();
}
Data::Message(msg) => {
match &msg.union {
Some(message::Union::Misc(misc)) => match misc.union {
Some(misc::Union::RefreshVideo(_)) => {
self.video_threads.iter().for_each(|(_, v)| {
*v.discard_queue.write().unwrap() = true;
});
}
Some(misc::Union::RefreshVideoDisplay(display)) => {
if let Some(v) = self.video_threads.get_mut(&(display as usize)) {
*v.discard_queue.write().unwrap() = true;
}
}
_ => {}
},
_ => {}
}
allow_err!(peer.send(&msg).await);
}
Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => {
@@ -838,7 +846,10 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
Data::RecordScreen(start) => {
let _ = self.video_sender.send(MediaData::RecordScreen(start));
self.handler.lc.write().unwrap().record = start;
for (_, v) in self.video_threads.iter_mut() {
v.video_sender.send(MediaData::RecordScreen(start)).ok();
}
}
Data::ElevateDirect => {
let mut request = ElevationRequest::new();
@@ -881,9 +892,18 @@ impl<T: InvokeUiSession> Remote<T> {
.on_voice_call_closed("Closed manually by the peer");
allow_err!(peer.send(&msg).await);
}
Data::ResetDecoder(display) => {
self.video_sender.send(MediaData::Reset(display)).ok();
}
Data::ResetDecoder(display) => match display {
Some(display) => {
if let Some(v) = self.video_threads.get_mut(&display) {
v.video_sender.send(MediaData::Reset).ok();
}
}
None => {
for (_, v) in self.video_threads.iter_mut() {
v.video_sender.send(MediaData::Reset).ok();
}
}
},
_ => {}
}
true
@@ -1011,100 +1031,115 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
// Currently, this function only considers decoding speed and queue length, not network delay.
// The controlled end can consider auto fps as the maximum decoding fps.
#[inline]
fn fps_control(&mut self, direct: bool) {
fn fps_control(&mut self, direct: bool, real_fps_map: HashMap<usize, i32>) {
self.video_threads.iter_mut().for_each(|(k, v)| {
let real_fps = real_fps_map.get(k).cloned().unwrap_or_default();
if real_fps == 0 {
v.fps_control.inactive_counter += 1;
} else {
v.fps_control.inactive_counter = 0;
}
});
let custom_fps = self.handler.lc.read().unwrap().custom_fps.clone();
let custom_fps = custom_fps.lock().unwrap().clone();
let mut custom_fps = custom_fps.unwrap_or(30);
if custom_fps < 5 || custom_fps > 120 {
custom_fps = 30;
}
let ctl = &mut self.fps_control;
let len = self
.video_queue_map
.read()
.unwrap()
let inactive_threshold = 15;
let max_queue_len = self
.video_threads
.iter()
.map(|v| v.1.len())
.map(|v| v.1.video_queue.read().unwrap().len())
.max()
.unwrap_or_default();
let decode_fps = self.decode_fps.read().unwrap().clone();
let Some(mut decode_fps) = decode_fps else {
let min_decode_fps = self
.video_threads
.iter()
.filter(|v| v.1.fps_control.inactive_counter < inactive_threshold)
.map(|v| *v.1.decode_fps.read().unwrap())
.min()
.flatten();
let Some(min_decode_fps) = min_decode_fps else {
return;
};
if cfg!(feature = "flutter") {
let active_displays = ctl
.last_active_time
.iter()
.filter(|t| t.1.elapsed().as_secs() < 5)
.count();
if active_displays > 1 {
decode_fps = decode_fps / active_displays;
}
}
let mut limited_fps = if direct {
decode_fps * 9 / 10 // 30 got 27
min_decode_fps * 9 / 10 // 30 got 27
} else {
decode_fps * 4 / 5 // 30 got 24
min_decode_fps * 4 / 5 // 30 got 24
};
if limited_fps > custom_fps {
limited_fps = custom_fps;
}
let last_auto_fps = self.handler.lc.read().unwrap().last_auto_fps.clone();
let should_decrease = (len > 1
&& last_auto_fps.clone().unwrap_or(custom_fps as _) > limited_fps)
|| len > std::cmp::max(1, limited_fps / 2);
// increase judgement
if len <= 1 {
if ctl.idle_counter < usize::MAX {
let displays = self.video_threads.keys().cloned().collect::<Vec<_>>();
let mut fps_trending = |display: usize| {
let thread = self.video_threads.get_mut(&display)?;
let ctl = &mut thread.fps_control;
let len = thread.video_queue.read().unwrap().len();
let decode_fps = thread.decode_fps.read().unwrap().clone()?;
let last_auto_fps = last_auto_fps.clone().unwrap_or(custom_fps as _);
if ctl.inactive_counter > inactive_threshold {
return None;
}
if len > 1 && last_auto_fps > limited_fps || len > std::cmp::max(1, decode_fps / 2) {
ctl.idle_counter = 0;
return Some(false);
}
if len <= 1 {
ctl.idle_counter += 1;
if ctl.idle_counter > 3 && last_auto_fps + 3 <= limited_fps {
return Some(true);
}
}
} else {
ctl.idle_counter = 0;
}
let mut should_increase = false;
if let Some(last_auto_fps) = last_auto_fps.clone() {
// ever set
if last_auto_fps + 3 <= limited_fps && ctl.idle_counter > 3 {
// limited_fps is 3 larger than last set, and idle time is more than 3 seconds
should_increase = true;
if len > 1 {
ctl.idle_counter = 0;
}
}
None
};
let trendings: Vec<_> = displays.iter().map(|k| fps_trending(*k)).collect();
let should_decrease = trendings.iter().any(|v| *v == Some(false));
let should_increase = !should_decrease && trendings.iter().any(|v| *v == Some(true));
if last_auto_fps.is_none() || should_decrease || should_increase {
// limited_fps to ensure decoding is faster than encoding
let mut auto_fps = limited_fps;
if should_decrease && limited_fps < len {
if should_decrease && limited_fps < max_queue_len {
auto_fps = limited_fps / 2;
}
if auto_fps < 1 {
auto_fps = 1;
}
let mut misc = Misc::new();
misc.set_option(OptionMessage {
custom_fps: auto_fps as _,
..Default::default()
});
let mut msg = Message::new();
msg.set_misc(misc);
self.sender.send(Data::Message(msg)).ok();
log::info!("Set fps to {}", auto_fps);
ctl.last_queue_size = len;
self.handler.lc.write().unwrap().last_auto_fps = Some(auto_fps);
if Some(auto_fps) != last_auto_fps {
let mut misc = Misc::new();
misc.set_option(OptionMessage {
custom_fps: auto_fps as _,
..Default::default()
});
let mut msg = Message::new();
msg.set_misc(misc);
self.sender.send(Data::Message(msg)).ok();
log::info!("Set fps to {}", auto_fps);
self.handler.lc.write().unwrap().last_auto_fps = Some(auto_fps);
}
}
// send refresh
for (display, video_queue) in self.video_queue_map.read().unwrap().iter() {
let tolerable = std::cmp::min(decode_fps, video_queue.capacity() / 2);
for (display, thread) in self.video_threads.iter_mut() {
let ctl = &mut thread.fps_control;
let video_queue = thread.video_queue.read().unwrap();
let tolerable = std::cmp::min(min_decode_fps, video_queue.capacity() / 2);
if ctl.refresh_times < 20 // enough
&& (video_queue.len() > tolerable
&& (ctl.refresh_times == 0 || ctl.last_refresh_instant.elapsed().as_secs() > 10))
&& (ctl.refresh_times == 0 || ctl.last_refresh_instant.map(|t|t.elapsed().as_secs() > 10).unwrap_or(false)))
{
// Refresh causes client set_display, left frames cause flickering.
while let Some(_) = video_queue.pop() {}
drop(video_queue);
self.handler.refresh_video(*display as _);
log::info!("Refresh display {} to reduce delay", display);
ctl.refresh_times += 1;
ctl.last_refresh_instant = Instant::now();
ctl.last_refresh_instant = Some(Instant::now());
}
}
}
@@ -1120,39 +1155,29 @@ impl<T: InvokeUiSession> Remote<T> {
self.send_toggle_virtual_display_msg(peer).await;
self.send_toggle_privacy_mode_msg(peer).await;
}
let incoming_format = CodecFormat::from(&vf);
if self.video_format != incoming_format {
self.video_format = incoming_format.clone();
self.handler.update_quality_status(QualityStatus {
codec_format: Some(incoming_format),
..Default::default()
})
};
self.video_format = CodecFormat::from(&vf);
let display = vf.display as usize;
let mut video_queue_write = self.video_queue_map.write().unwrap();
if !video_queue_write.contains_key(&display) {
video_queue_write.insert(
display,
ArrayQueue::<VideoFrame>::new(crate::client::VIDEO_QUEUE_SIZE),
);
if !self.video_threads.contains_key(&display) {
self.new_video_thread(display);
}
let Some(thread) = self.video_threads.get_mut(&display) else {
return true;
};
if Self::contains_key_frame(&vf) {
if let Some(video_queue) = video_queue_write.get_mut(&display) {
while let Some(_) = video_queue.pop() {}
}
self.video_sender
thread
.video_sender
.send(MediaData::VideoFrame(Box::new(vf)))
.ok();
} else {
if let Some(video_queue) = video_queue_write.get_mut(&display) {
video_queue.force_push(vf);
let video_queue = thread.video_queue.read().unwrap();
if video_queue.force_push(vf).is_some() {
drop(video_queue);
self.handler.refresh_video(display as _);
} else {
thread.video_sender.send(MediaData::VideoQueue).ok();
}
self.video_sender.send(MediaData::VideoQueue(display)).ok();
}
self.fps_control
.last_active_time
.insert(display, Instant::now());
}
Some(message::Union::Hash(hash)) => {
self.handler
@@ -1177,7 +1202,7 @@ impl<T: InvokeUiSession> Remote<T> {
self.check_clipboard_file_context();
if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) {
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
let rx = Client::try_start_clipboard(None);
#[cfg(not(feature = "flutter"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -1188,7 +1213,7 @@ impl<T: InvokeUiSession> Remote<T> {
},
));
// To make sure current text clipboard data is updated.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
if let Some(mut rx) = rx {
timeout(CLIPBOARD_INTERVAL, rx.recv()).await.ok();
}
@@ -1209,6 +1234,11 @@ impl<T: InvokeUiSession> Remote<T> {
});
}
}
// to-do: Android, is `sync_init_clipboard` really needed?
// https://github.com/rustdesk/rustdesk/discussions/9010
#[cfg(target_os = "android")]
crate::flutter::update_text_clipboard_required();
// on connection established client
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
@@ -1240,7 +1270,7 @@ impl<T: InvokeUiSession> Remote<T> {
if !self.handler.lc.read().unwrap().disable_clipboard.v {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
update_clipboard(vec![cb], ClipboardSide::Client);
#[cfg(any(target_os = "android", target_os = "ios"))]
#[cfg(target_os = "ios")]
{
let content = if cb.compress {
hbb_common::compress::decompress(&cb.content)
@@ -1251,12 +1281,16 @@ impl<T: InvokeUiSession> Remote<T> {
self.handler.clipboard(content);
}
}
#[cfg(target_os = "android")]
crate::clipboard::handle_msg_clipboard(cb);
}
}
Some(message::Union::MultiClipboards(_mcb)) => {
if !self.handler.lc.read().unwrap().disable_clipboard.v {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
update_clipboard(_mcb.clipboards, ClipboardSide::Client);
#[cfg(target_os = "android")]
crate::clipboard::handle_msg_multi_clipboards(_mcb);
}
}
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
@@ -1265,6 +1299,9 @@ impl<T: InvokeUiSession> Remote<T> {
}
Some(message::Union::FileResponse(fr)) => {
match fr.union {
Some(file_response::Union::EmptyDirs(res)) => {
self.handler.update_empty_dirs(res);
}
Some(file_response::Union::Dir(fd)) => {
#[cfg(windows)]
let entries = fd.entries.to_vec();
@@ -1421,14 +1458,14 @@ impl<T: InvokeUiSession> Remote<T> {
Ok(Permission::Keyboard) => {
*self.handler.server_keyboard_enabled.write().unwrap() = p.enabled;
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
crate::flutter::update_text_clipboard_required();
self.handler.set_permission("keyboard", p.enabled);
}
Ok(Permission::Clipboard) => {
*self.handler.server_clipboard_enabled.write().unwrap() = p.enabled;
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
crate::flutter::update_text_clipboard_required();
self.handler.set_permission("clipboard", p.enabled);
}
@@ -1457,9 +1494,10 @@ impl<T: InvokeUiSession> Remote<T> {
}
Some(misc::Union::SwitchDisplay(s)) => {
self.handler.handle_peer_switch_display(&s);
self.video_sender
.send(MediaData::Reset(Some(s.display as _)))
.ok();
if let Some(thread) = self.video_threads.get_mut(&(s.display as usize)) {
thread.video_sender.send(MediaData::Reset).ok();
}
if s.width > 0 && s.height > 0 {
self.handler.set_display(
s.x,
@@ -1907,6 +1945,53 @@ impl<T: InvokeUiSession> Remote<T> {
});
}
}
fn new_video_thread(&mut self, display: usize) {
let video_queue = Arc::new(RwLock::new(ArrayQueue::new(client::VIDEO_QUEUE_SIZE)));
let (video_sender, video_receiver) = std::sync::mpsc::channel::<MediaData>();
let decode_fps = Arc::new(RwLock::new(None));
let frame_count = Arc::new(RwLock::new(0));
let discard_queue = Arc::new(RwLock::new(false));
let video_thread = VideoThread {
video_queue: video_queue.clone(),
video_sender,
decode_fps: decode_fps.clone(),
frame_count: frame_count.clone(),
fps_control: Default::default(),
discard_queue: discard_queue.clone(),
};
let handler = self.handler.ui_handler.clone();
crate::client::start_video_thread(
self.handler.clone(),
display,
video_receiver,
video_queue,
decode_fps,
self.chroma.clone(),
discard_queue,
move |display: usize,
data: &mut scrap::ImageRgb,
_texture: *mut c_void,
pixelbuffer: bool| {
*frame_count.write().unwrap() += 1;
if pixelbuffer {
handler.on_rgba(display, data);
} else {
#[cfg(all(feature = "vram", feature = "flutter"))]
handler.on_texture(display, _texture);
}
},
);
self.video_threads.insert(display, video_thread);
let auto_record = self.handler.lc.read().unwrap().record;
if auto_record && self.video_threads.len() == 1 {
let mut misc = Misc::new();
misc.set_client_record_status(true);
let mut msg = Message::new();
msg.set_misc(misc);
self.sender.send(Data::Message(msg)).ok();
}
}
}
struct RemoveJob {
@@ -1939,22 +2024,19 @@ impl RemoveJob {
}
}
#[derive(Debug, Default)]
struct FpsControl {
last_queue_size: usize,
refresh_times: usize,
last_refresh_instant: Instant,
last_refresh_instant: Option<Instant>,
idle_counter: usize,
last_active_time: HashMap<usize, Instant>,
inactive_counter: usize,
}
impl Default for FpsControl {
fn default() -> Self {
Self {
last_queue_size: Default::default(),
refresh_times: Default::default(),
last_refresh_instant: Instant::now(),
idle_counter: 0,
last_active_time: Default::default(),
}
}
struct VideoThread {
video_queue: Arc<RwLock<ArrayQueue<VideoFrame>>>,
video_sender: MediaSender,
decode_fps: Arc<RwLock<Option<usize>>>,
frame_count: Arc<RwLock<usize>>,
discard_queue: Arc<RwLock<bool>>,
fps_control: FpsControl,
}

View File

@@ -1,4 +1,6 @@
#[cfg(not(target_os = "android"))]
use arboard::{ClipboardData, ClipboardFormat};
#[cfg(not(target_os = "android"))]
use clipboard_master::{ClipboardHandler, Master, Shutdown};
use hbb_common::{bail, log, message_proto::*, ResultType};
use std::{
@@ -16,6 +18,7 @@ const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner";
// Add special format for Excel XML Spreadsheet
const CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET: &'static str = "XML Spreadsheet";
#[cfg(not(target_os = "android"))]
lazy_static::lazy_static! {
static ref ARBOARD_MTX: Arc<Mutex<()>> = Arc::new(Mutex::new(()));
// cache the clipboard msg
@@ -27,9 +30,12 @@ lazy_static::lazy_static! {
static ref CLIPBOARD_CTX: Arc<Mutex<Option<ClipboardContext>>> = Arc::new(Mutex::new(None));
}
#[cfg(not(target_os = "android"))]
const CLIPBOARD_GET_MAX_RETRY: usize = 3;
#[cfg(not(target_os = "android"))]
const CLIPBOARD_GET_RETRY_INTERVAL_DUR: Duration = Duration::from_millis(33);
#[cfg(not(target_os = "android"))]
const SUPPORTED_FORMATS: &[ClipboardFormat] = &[
ClipboardFormat::Text,
ClipboardFormat::Html,
@@ -146,6 +152,7 @@ impl ClipboardContext {
}
}
#[cfg(not(target_os = "android"))]
pub fn check_clipboard(
ctx: &mut Option<ClipboardContext>,
side: ClipboardSide,
@@ -194,6 +201,7 @@ pub fn check_clipboard_cm() -> ResultType<MultiClipboards> {
}
}
#[cfg(not(target_os = "android"))]
fn update_clipboard_(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
let mut to_update_data = proto::from_multi_clipbards(multi_clipboards);
if to_update_data.is_empty() {
@@ -224,17 +232,20 @@ fn update_clipboard_(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
}
}
#[cfg(not(target_os = "android"))]
pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
std::thread::spawn(move || {
update_clipboard_(multi_clipboards, side);
});
}
#[cfg(not(target_os = "android"))]
#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))]
pub struct ClipboardContext {
inner: arboard::Clipboard,
}
#[cfg(not(target_os = "android"))]
#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))]
#[allow(unreachable_code)]
impl ClipboardContext {
@@ -337,10 +348,20 @@ impl ClipboardContext {
pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool {
use hbb_common::get_version_number;
get_version_number(peer_version) >= get_version_number("1.3.0")
&& !["", "Android", &whoami::Platform::Ios.to_string()].contains(&peer_platform)
if get_version_number(peer_version) < get_version_number("1.3.0") {
return false;
}
if ["", &whoami::Platform::Ios.to_string()].contains(&peer_platform) {
return false;
}
if "Android" == peer_platform && get_version_number(peer_version) < get_version_number("1.3.3")
{
return false;
}
true
}
#[cfg(not(target_os = "android"))]
pub fn get_current_clipboard_msg(
peer_version: &str,
peer_platform: &str,
@@ -406,6 +427,7 @@ impl std::fmt::Display for ClipboardSide {
}
}
#[cfg(not(target_os = "android"))]
pub fn start_clipbard_master_thread(
handler: impl ClipboardHandler + Send + 'static,
tx_start_res: Sender<(Option<Shutdown>, String)>,
@@ -437,6 +459,7 @@ pub fn start_clipbard_master_thread(
pub use proto::get_msg_if_not_support_multi_clip;
mod proto {
#[cfg(not(target_os = "android"))]
use arboard::ClipboardData;
use hbb_common::{
compress::{compress as compress_func, decompress},
@@ -459,6 +482,7 @@ mod proto {
}
}
#[cfg(not(target_os = "android"))]
fn image_to_proto(a: arboard::ImageData) -> Clipboard {
match &a {
arboard::ImageData::Rgba(rgba) => {
@@ -519,6 +543,7 @@ mod proto {
}
}
#[cfg(not(target_os = "android"))]
fn clipboard_data_to_proto(data: ClipboardData) -> Option<Clipboard> {
let d = match data {
ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text),
@@ -531,6 +556,7 @@ mod proto {
Some(d)
}
#[cfg(not(target_os = "android"))]
pub fn create_multi_clipboards(vec_data: Vec<ClipboardData>) -> MultiClipboards {
MultiClipboards {
clipboards: vec_data
@@ -541,6 +567,7 @@ mod proto {
}
}
#[cfg(not(target_os = "android"))]
fn from_clipboard(clipboard: Clipboard) -> Option<ClipboardData> {
let data = if clipboard.compress {
decompress(&clipboard.content)
@@ -569,6 +596,7 @@ mod proto {
}
}
#[cfg(not(target_os = "android"))]
pub fn from_multi_clipbards(multi_clipboards: Vec<Clipboard>) -> Vec<ClipboardData> {
multi_clipboards
.into_iter()
@@ -597,3 +625,49 @@ mod proto {
})
}
}
#[cfg(target_os = "android")]
pub fn handle_msg_clipboard(mut cb: Clipboard) {
use hbb_common::protobuf::Message;
if cb.compress {
cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content));
}
let multi_clips = MultiClipboards {
clipboards: vec![cb],
..Default::default()
};
if let Ok(bytes) = multi_clips.write_to_bytes() {
let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes);
}
}
#[cfg(target_os = "android")]
pub fn handle_msg_multi_clipboards(mut mcb: MultiClipboards) {
use hbb_common::protobuf::Message;
for cb in mcb.clipboards.iter_mut() {
if cb.compress {
cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content));
}
}
if let Ok(bytes) = mcb.write_to_bytes() {
let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes);
}
}
#[cfg(target_os = "android")]
pub fn get_clipboards_msg(client: bool) -> Option<Message> {
let mut clipboards = scrap::android::ffi::get_clipboards(client)?;
let mut msg = Message::new();
for c in &mut clipboards.clipboards {
let compressed = hbb_common::compress::compress(&c.content);
let compress = compressed.len() < c.content.len();
if compress {
c.content = compressed.into();
}
c.compress = compress;
}
msg.set_multi_clipboards(clipboards);
Some(msg)
}

View File

@@ -5,7 +5,7 @@ use std::{
task::Poll,
};
use serde_json::Value;
use serde_json::{json, Map, Value};
use hbb_common::{
allow_err,
@@ -837,7 +837,7 @@ async fn check_software_update_() -> hbb_common::ResultType<()> {
let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data);
}
}
*SOFTWARE_UPDATE_URL.lock().unwrap() = response_url;
*SOFTWARE_UPDATE_URL.lock().unwrap() = response_url;
}
Ok(())
}
@@ -1051,6 +1051,11 @@ pub fn get_supported_keyboard_modes(version: i64, peer_platform: &str) -> Vec<Ke
}
pub fn make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> String {
let fd_json = _make_fd_to_json(id, path, entries);
serde_json::to_string(&fd_json).unwrap_or("".into())
}
pub fn _make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> Map<String, Value> {
use serde_json::json;
let mut fd_json = serde_json::Map::new();
fd_json.insert("id".into(), json!(id));
@@ -1066,7 +1071,33 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> Strin
entries_out.push(entry_map);
}
fd_json.insert("entries".into(), json!(entries_out));
serde_json::to_string(&fd_json).unwrap_or("".into())
fd_json
}
pub fn make_vec_fd_to_json(fds: &[FileDirectory]) -> String {
let mut fd_jsons = vec![];
for fd in fds.iter() {
let fd_json = _make_fd_to_json(fd.id, fd.path.clone(), &fd.entries);
fd_jsons.push(fd_json);
}
serde_json::to_string(&fd_jsons).unwrap_or("".into())
}
pub fn make_empty_dirs_response_to_json(res: &ReadEmptyDirsResponse) -> String {
let mut map: Map<String, Value> = serde_json::Map::new();
map.insert("path".into(), json!(res.path));
let mut fd_jsons = vec![];
for fd in res.empty_dirs.iter() {
let fd_json = _make_fd_to_json(fd.id, fd.path.clone(), &fd.entries);
fd_jsons.push(fd_json);
}
map.insert("empty_dirs".into(), fd_jsons.into());
serde_json::to_string(&map).unwrap_or("".into())
}
/// The function to handle the url scheme sent by the system.

View File

@@ -56,8 +56,8 @@ pub fn get_custom_server_from_string(s: &str) -> ResultType<CustomServer> {
*
* This allows using a ',' (comma) symbol as a final delimiter.
*/
if s.contains("host=") {
let stripped = &s[s.find("host=").unwrap_or(0)..s.len()];
if s.to_lowercase().contains("host=") {
let stripped = &s[s.to_lowercase().find("host=").unwrap_or(0)..s.len()];
let strs: Vec<&str> = stripped.split(",").collect();
let mut host = String::default();
let mut key = String::default();
@@ -65,16 +65,17 @@ pub fn get_custom_server_from_string(s: &str) -> ResultType<CustomServer> {
let mut relay = String::default();
let strs_iter = strs.iter();
for el in strs_iter {
if el.starts_with("host=") {
let el_lower = el.to_lowercase();
if el_lower.starts_with("host=") {
host = el.chars().skip(5).collect();
}
if el.starts_with("key=") {
if el_lower.starts_with("key=") {
key = el.chars().skip(4).collect();
}
if el.starts_with("api=") {
if el_lower.starts_with("api=") {
api = el.chars().skip(4).collect();
}
if el.starts_with("relay=") {
if el_lower.starts_with("relay=") {
relay = el.chars().skip(6).collect();
}
}
@@ -169,6 +170,18 @@ mod test {
relay: "server.example.net".to_owned(),
}
);
assert_eq!(
get_custom_server_from_string(
"rustdesk-Host=server.example.net,Key=Zm9vYmFyLiwyCg==,RELAY=server.example.net.exe"
)
.unwrap(),
CustomServer {
host: "server.example.net".to_owned(),
key: "Zm9vYmFyLiwyCg==".to_owned(),
api: "".to_owned(),
relay: "server.example.net".to_owned(),
}
);
let lic = CustomServer {
host: "1.1.1.1".to_owned(),
key: "5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=".to_owned(),

View File

@@ -726,6 +726,20 @@ impl InvokeUiSession for FlutterHandler {
}
}
fn update_empty_dirs(&self, res: ReadEmptyDirsResponse) {
self.push_event(
"empty_dirs",
&[
("is_local", "false"),
(
"value",
&crate::common::make_empty_dirs_response_to_json(&res),
),
],
&[],
);
}
// unused in flutter
fn update_transfer_list(&self) {}
@@ -1250,15 +1264,17 @@ fn try_send_close_event(event_stream: &Option<StreamSink<EventToUI>>) {
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
pub fn update_text_clipboard_required() {
let is_required = sessions::get_sessions()
.iter()
.any(|s| s.is_text_clipboard_required());
#[cfg(target_os = "android")]
let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required);
Client::set_is_text_clipboard_required(is_required);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
pub fn send_text_clipboard_msg(msg: Message) {
for s in sessions::get_sessions() {
if s.is_text_clipboard_required() {
@@ -2051,7 +2067,7 @@ pub mod sessions {
}
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
pub fn has_sessions_running(conn_type: ConnType) -> bool {
SESSIONS.read().unwrap().iter().any(|((_, r#type), s)| {
*r#type == conn_type && s.session_handlers.read().unwrap().len() != 0

View File

@@ -1,6 +1,6 @@
use crate::{
client::file_trait::FileManager,
common::make_fd_to_json,
common::{make_fd_to_json, make_vec_fd_to_json},
flutter::{
self, session_add, session_add_existed, session_start_, sessions, try_sync_peer_option,
},
@@ -274,7 +274,7 @@ pub fn session_toggle_option(session_id: SessionID, value: String) {
session.toggle_option(value.clone());
try_sync_peer_option(&session, &session_id, &value, None);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" {
crate::flutter::update_text_clipboard_required();
}
@@ -682,6 +682,27 @@ pub fn session_read_local_dir_sync(
"".to_string()
}
pub fn session_read_local_empty_dirs_recursive_sync(
_session_id: SessionID,
path: String,
include_hidden: bool,
) -> String {
if let Ok(fds) = fs::get_empty_dirs_recursive(&path, include_hidden) {
return make_vec_fd_to_json(&fds);
}
"".to_string()
}
pub fn session_read_remote_empty_dirs_recursive_sync(
session_id: SessionID,
path: String,
include_hidden: bool,
) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.read_empty_dirs(path, include_hidden);
}
}
pub fn session_get_platform(session_id: SessionID, is_remote: bool) -> String {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
return session.get_platform(is_remote);
@@ -774,13 +795,6 @@ pub fn main_get_sound_inputs() -> Vec<String> {
vec![String::from("")]
}
pub fn main_get_default_sound_input() -> Option<String> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
return get_default_sound_input();
#[cfg(any(target_os = "android", target_os = "ios"))]
None
}
pub fn main_get_login_device_info() -> SyncReturn<String> {
SyncReturn(get_login_device_info_json())
}
@@ -817,6 +831,17 @@ pub fn main_show_option(_key: String) -> SyncReturn<bool> {
SyncReturn(false)
}
#[inline]
#[cfg(target_os = "android")]
fn enable_server_clipboard(keyboard_enabled: &str, clip_enabled: &str) {
use scrap::android::ffi::call_clipboard_manager_enable_service_clipboard;
let keyboard_enabled =
config::option2bool(config::keys::OPTION_ENABLE_KEYBOARD, &keyboard_enabled);
let clip_enabled = config::option2bool(config::keys::OPTION_ENABLE_CLIPBOARD, &clip_enabled);
crate::ui_cm_interface::switch_permission_all("clipboard".to_owned(), clip_enabled);
let _ = call_clipboard_manager_enable_service_clipboard(keyboard_enabled && clip_enabled);
}
pub fn main_set_option(key: String, value: String) {
#[cfg(target_os = "android")]
if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) {
@@ -824,6 +849,11 @@ pub fn main_set_option(key: String, value: String) {
config::keys::OPTION_ENABLE_KEYBOARD,
&value,
));
enable_server_clipboard(&value, &get_option(config::keys::OPTION_ENABLE_CLIPBOARD));
}
#[cfg(target_os = "android")]
if key.eq(config::keys::OPTION_ENABLE_CLIPBOARD) {
enable_server_clipboard(&get_option(config::keys::OPTION_ENABLE_KEYBOARD), &value);
}
if key.eq("custom-rendezvous-server") {
set_option(key, value.clone());
@@ -2301,6 +2331,16 @@ pub fn session_request_new_display_init_msgs(session_id: SessionID, display: usi
}
}
pub fn main_audio_support_loopback() -> SyncReturn<bool> {
#[cfg(target_os = "windows")]
let is_surpport = true;
#[cfg(feature = "screencapturekit")]
let is_surpport = crate::audio_service::is_screen_capture_kit_available();
#[cfg(not(any(target_os = "windows", feature = "screencapturekit")))]
let is_surpport = false;
SyncReturn(is_surpport)
}
#[cfg(target_os = "android")]
pub mod server_side {
use hbb_common::{config, log};

View File

@@ -45,6 +45,10 @@ pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true);
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum FS {
ReadEmptyDirs {
dir: String,
include_hidden: bool,
},
ReadDir {
dir: String,
include_hidden: bool,

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -44,7 +44,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("id_change_tip", "只可以使用字母 a-z, A-Z, 0-9, _ (下划线)。首字母必须是 a-z, A-Z。长度在 6 与 16 之间。"),
("Website", "网站"),
("About", "关于"),
("Slogan_tip", ""),
("Slogan_tip", "在这个混乱的世界中,用心制作!"),
("Privacy Statement", "隐私声明"),
("Mute", "静音"),
("Build Date", "构建日期"),
@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", "上传文件夹"),
("Upload files", "上传文件"),
("Clipboard is synchronized", "剪贴板已同步"),
("Update client clipboard", "更新客户端的粘贴板"),
("Untagged", "无标签"),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -364,7 +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", ""),
("Automatically record outgoing sessions", "Ausgehende Sitzungen automatisch aufzeichnen"),
("Change", "Ändern"),
("Start session recording", "Sitzungsaufzeichnung starten"),
("Stop session recording", "Sitzungsaufzeichnung beenden"),
@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", "Ordner hochladen"),
("Upload files", "Dateien hochladen"),
("Clipboard is synchronized", "Zwischenablage ist synchronisiert"),
("Update client clipboard", "Client-Zwischenablage aktualisieren"),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", "Subir carpeta"),
("Upload files", "Subir archivos"),
("Clipboard is synchronized", "Portapapeles sincronizado"),
("Update client clipboard", "Actualizar portapapeles del cliente"),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -1,7 +1,7 @@
lazy_static::lazy_static! {
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[
("Status", "Státusz"),
("Status", "Állapot"),
("Your Desktop", "Saját asztal"),
("desk_tip", "A számítógép ezzel a jelszóval és azonosítóval érhető el távolról."),
("Password", "Jelszó"),
@@ -19,16 +19,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recent sessions", "Legutóbbi munkamanetek"),
("Address book", "Címjegyzék"),
("Confirmation", "Megerősítés"),
("TCP tunneling", "TCP tunneling"),
("TCP tunneling", "TCP alagútépítés"),
("Remove", "Eltávolít"),
("Refresh random password", "Új véletlenszerű jelszó"),
("Set your own password", "Saját jelszó beállítása"),
("Enable keyboard/mouse", "Billentyűzet/egér engedélyezése"),
("Enable clipboard", "Megosztott vágólap engedélyezése"),
("Enable file transfer", "Fájlátvitel engedélyezése"),
("Enable TCP tunneling", "TCP tunneling engedélyezése"),
("Enable TCP tunneling", "TCP alagútépítés engedélyezése"),
("IP Whitelisting", "IP engedélyezési lista"),
("ID/Relay Server", "ID/Relay szerver"),
("ID/Relay Server", "ID/Továbbító szerver"),
("Import server config", "Szerver konfiguráció importálása"),
("Export Server Config", "Szerver konfiguráció exportálása"),
("Import server configuration successfully", "Szerver konfiguráció sikeresen importálva"),
@@ -38,13 +38,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop service", "Szolgáltatás leállítása"),
("Change ID", "Azonosító megváltoztatása"),
("Your new ID", "Az új azonosítód"),
("length %min% to %max%", ""),
("starts with a letter", ""),
("allowed characters", ""),
("length %min% to %max%", "hossz %min% és %max% között"),
("starts with a letter", "betűvel kezdődik"),
("allowed characters", "engedélyezett karakterek"),
("id_change_tip", "Csak a-z, A-Z, 0-9 csoportokba tartozó karakterek, illetve a _ karakter van engedélyezve. Az első karakternek mindenképpen a-z, A-Z csoportokba kell esnie. Az azonosító hosszúsága 6-tól, 16 karakter."),
("Website", "Weboldal"),
("Website", "Webhely"),
("About", "Rólunk"),
("Slogan_tip", ""),
("Slogan_tip", "Szenvedéllyel programozva - egy káoszba süllyedő világban!"),
("Privacy Statement", "Adatvédelmi nyilatkozat"),
("Mute", "Némítás"),
("Build Date", "Build ideje"),
@@ -52,7 +52,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Home", "Kezdőképernyő"),
("Audio Input", "Hangátvitel"),
("Enhancements", "Fejlesztések"),
("Hardware Codec", "Hardware kodek"),
("Hardware Codec", "Hardveres kódek"),
("Adaptive bitrate", "Adaptív bitráta"),
("ID Server", "ID szerver"),
("Relay Server", "Továbbító szerver"),
@@ -127,7 +127,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Gyorsan reagáló"),
("Custom", "Egyedi"),
("Show remote cursor", "Távoli kurzor megjelenítése"),
("Show quality monitor", ""),
("Show quality monitor", "Minőségi monitor megjelenítése"),
("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 Ctrl + Alt + Del", "Illessze be a Ctrl + Alt + Del"),
@@ -143,7 +143,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Failed to connect via rendezvous server", "Nem sikerült csatlakozni a kiszolgáló szerveren keresztül"),
("Failed to connect via relay server", "Nem sikerült csatlakozni a közvetítő szerveren keresztül"),
("Failed to make direct connection to remote desktop", "Nem sikerült közvetlen kapcsolatot létesíteni a távoli számítógéppel"),
("Set Password", "Jelszó Beállítása"),
("Set Password", "Jelszó beállítása"),
("OS Password", "Operációs rendszer jelszavának beállítása"),
("install_tip", "Előfordul, hogy bizonyos esetekben hiba léphet fel a Portable verzió használata során. A megfelelő működés érdekében, kérem telepítse a RustDesk alkalmazást a számítógépre."),
("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"),
@@ -153,7 +153,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("config_acc", "A távoli vezérléshez a RustDesk-nek \"Kisegítő lehetőség\" engedélyre van szüksége"),
("config_screen", "A távoli vezérléshez szükséges a \"Képernyőfelvétel\" engedély megadása"),
("Installing ...", "Telepítés..."),
("Install", "Telepítés"),
("Install", "Telepítsd"),
("Installation", "Telepítés"),
("Installation Path", "Telepítési útvonal"),
("Create start menu shortcuts", "Start menü parancsikonok létrehozása"),
@@ -164,7 +164,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Generating ...", "Létrehozás..."),
("Your installation is lower version.", "A telepített verzió alacsonyabb."),
("not_close_tcp_tip", "Ne zárja be ezt az ablakot miközben a tunnelt használja"),
("Listening ...", "Keresés..."),
("Listening ...", "Figyelés..."),
("Remote Host", "Távoli kiszolgáló"),
("Remote Port", "Távoli port"),
("Action", "Indítás"),
@@ -192,7 +192,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Auto Login", "Automatikus bejelentkezés"),
("Enable direct IP access", "Közvetlen IP-elérés engedélyezése"),
("Rename", "Átnevezés"),
("Space", ""),
("Space", "Szóköz"),
("Create desktop shortcut", "Asztali parancsikon létrehozása"),
("Change Path", "Elérési út módosítása"),
("Create Folder", "Mappa létrehozás"),
@@ -210,17 +210,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Closed manually by the peer", "A kapcsolatot a másik fél manuálisan bezárta"),
("Enable remote configuration modification", "Távoli konfiguráció módosítás engedélyezése"),
("Run without install", "Futtatás feltelepítés nélkül"),
("Connect via relay", ""),
("Connect via relay", "Csatlakozás közvetítőn keresztül"),
("Always connect via relay", "Mindig közvetítőn keresztüli csatlakozás"),
("whitelist_tip", "Csak az engedélyezési listán szereplő címek csatlakozhatnak"),
("Login", "Belépés"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Verify", "Ellenőrzés"),
("Remember me", "Emlékezz rám"),
("Trust this device", "Bízzon ebben az eszközben"),
("Verification code", "Ellenőrző kód"),
("verification_tip", "A regisztrált e-mail címre egy ellenőrző kódot küldtek. Adja meg az ellenőrző kódot az újbóli bejelentkezéshez."),
("Logout", "Kilépés"),
("Tags", "Tagok"),
("Tags", "Címkék"),
("Search ID", "Azonosító keresése..."),
("whitelist_sep", "A címeket veszővel, pontosvesszővel, szóközzel, vagy új sorral válassza el"),
("Add ID", "Azonosító hozzáadása"),
@@ -268,7 +268,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Note", "Megyjegyzés"),
("Connection", "Kapcsolat"),
("Share Screen", "Képernyőmegosztás"),
("Chat", "Chat"),
("Chat", "Csevegés"),
("Total", "Összes"),
("items", "elemek"),
("Selected", "Kijelölt"),
@@ -286,8 +286,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_service_will_start_tip", "A \"Képernyőrögzítés\" bekapcsolásával automatikus elindul a szolgáltatás, lehetővé téve, hogy más eszközök csatlakozási kérelmet küldhessenek"),
("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."),
("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."),
("android_start_service_tip", ""),
("android_permission_may_not_change_tip", ""),
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \" Közvetítő szolgáltatás indítása\" gombra, vagy aktiválja a \"Képernyőfelvétel\" engedélyt."),
("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új csatlakozás után módosulnak."),
("Account", "Fiók"),
("Overwrite", "Felülírás"),
("This file exists, skip or overwrite this file?", "Ez a fájl már létezik, kihagyja vagy felülírja ezt a fájlt?"),
@@ -306,11 +306,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "RustDesk futtatása a háttérben"),
("Ignore Battery Optimizations", "Akkumulátorkímélő figyelmen kívűl hagyása"),
("android_open_battery_optimizations_tip", "Ha le szeretné tiltani ezt a funkciót, lépjen a RustDesk alkalmazás beállítási oldalára, keresse meg az [Akkumulátorkímélő] lehetőséget és válassza a nincs korlátozás lehetőséget."),
("Start on boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Start on boot", "Indítás bekapcsoláskor"),
("Start the screen sharing service on boot, requires special permissions", "Indítsa el a képernyőmegosztó szolgáltatást rendszerindításkor, speciális engedélyeket igényel"),
("Connection not allowed", "A csatlakozás nem engedélyezett"),
("Legacy mode", ""),
("Map mode", ""),
("Legacy mode", "Kompatibilitási mód"),
("Map mode", "Hozzárendelési mód"),
("Translate mode", "Fordító mód"),
("Use permanent password", "Állandó jelszó használata"),
("Use both passwords", "Mindkét jelszó használata"),
@@ -323,15 +323,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Copied", "Másolva"),
("Exit Fullscreen", "Kilépés teljes képernyős módból"),
("Fullscreen", "Teljes képernyő"),
("Mobile Actions", "mobil műveletek"),
("Mobile Actions", "Mobil műveletek"),
("Select Monitor", "Válasszon képernyőt"),
("Control Actions", "Irányítási műveletek"),
("Display Settings", "Megjelenítési beállítások"),
("Ratio", "Arány"),
("Image Quality", "Képminőség"),
("Scroll Style", "Görgetési stílus"),
("Show Toolbar", ""),
("Hide Toolbar", ""),
("Show Toolbar", "Eszköztár megjelenítése"),
("Hide Toolbar", "Eszköztár eljertése"),
("Direct Connection", "Közvetlen kapcsolat"),
("Relay Connection", "Közvetett csatlakozás"),
("Secure Connection", "Biztonságos kapcsolat"),
@@ -342,7 +342,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Security", "Biztonság"),
("Theme", "Téma"),
("Dark Theme", "Sötét téma"),
("Light Theme", ""),
("Light Theme", "Világos téma"),
("Dark", "Sötét"),
("Light", "Világos"),
("Follow System", "Rendszer téma követése"),
@@ -359,12 +359,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Audio Input Device", "Audio bemeneti eszköz"),
("Use IP Whitelisting", "Engedélyezési lista használata"),
("Network", "Hálózat"),
("Pin Toolbar", ""),
("Unpin Toolbar", ""),
("Pin Toolbar", "Eszköztár kitűzése"),
("Unpin Toolbar", "Eszköztár kitűzésének feloldása"),
("Recording", "Felvétel"),
("Directory", "Könyvtár"),
("Automatically record incoming sessions", "A bejövő munkamenetek automatikus rögzítése"),
("Automatically record outgoing sessions", ""),
("Automatically record outgoing sessions", "A kimenő munkamenetek automatikus rögzítése"),
("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"),
@@ -372,9 +372,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable LAN discovery", "Felfedezés enegedélyezése"),
("Deny LAN discovery", "Felfedezés tiltása"),
("Write a message", "Üzenet írása"),
("Prompt", ""),
("Please wait for confirmation of UAC...", ""),
("elevated_foreground_window_tip", ""),
("Prompt", "Kérés"),
("Please wait for confirmation of UAC...", "Kérjük, várjon az UAC megerősítésére..."),
("elevated_foreground_window_tip", "A távvezérelt számítógép jelenleg nyitott ablakához magasabb szintű jogok szükségesek. Ezért jelenleg nem lehetséges az egér és a billentyűzet használata. Kérje meg azt a felhasználót, akinek a számítógépét távolról vezérli, hogy minimalizálja az ablakot, vagy növelje a jogokat. A jövőbeni probléma elkerülése érdekében ajánlott a szoftvert a távvezérelt számítógépre telepíteni."),
("Disconnected", "Szétkapcsolva"),
("Other", "Egyéb"),
("Confirm before closing multiple tabs", "Biztos, hogy bezárja az összes lapot?"),
@@ -389,269 +389,271 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("This PC", "Ez a számítógép"),
("or", "vagy"),
("Continue with", "Folytatás a következővel"),
("Elevate", ""),
("Zoom cursor", ""),
("Accept sessions via password", ""),
("Accept sessions via click", ""),
("Accept sessions via both", ""),
("Please wait for the remote side to accept your session request...", ""),
("Elevate", "Hozzáférés engedélyezése"),
("Zoom cursor", "Kurzor nagyítása"),
("Accept sessions via password", "Munkamenetek elfogadása jelszóval"),
("Accept sessions via click", "Munkamenetek elfogadása kattintással"),
("Accept sessions via both", "Munkamenetek fogadása mindkettőn keresztül"),
("Please wait for the remote side to accept your session request...", "Kérjük, várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét..."),
("One-time Password", "Egyszer használatos jelszó"),
("Use one-time password", "Használj ideiglenes jelszót"),
("Use one-time password", "Használjon ideiglenes jelszót"),
("One-time password length", "Egyszer használatos jelszó hossza"),
("Request access to your device", ""),
("Hide connection management window", ""),
("hide_cm_tip", ""),
("wayland_experiment_tip", ""),
("Right click to select tabs", ""),
("Skipped", ""),
("Add to address book", ""),
("Request access to your device", "Hozzáférés kérése az eszközéhez"),
("Hide connection management window", "Kapcsolatkezelő ablak elrejtése"),
("hide_cm_tip", "Ez csak akkor lehetséges, ha a hozzáférés állandó jelszóval történik."),
("wayland_experiment_tip", "A Wayland-támogatás csak kísérleti jellegű. Kérjük, használja az X11-et, ha felügyelet nélküli hozzáférésre van szüksége."),
("Right click to select tabs", "Jobb klikk a lapok kiválasztásához"),
("Skipped", "Kihagyott"),
("Add to address book", "Hozzáadás a címjegyzékhez"),
("Group", "Csoport"),
("Search", "Keresés"),
("Closed manually by web console", ""),
("Local keyboard type", ""),
("Select local keyboard type", ""),
("software_render_tip", ""),
("Always use software rendering", ""),
("config_input", ""),
("config_microphone", ""),
("request_elevation_tip", ""),
("Wait", ""),
("Elevation Error", ""),
("Ask the remote user for authentication", ""),
("Choose this if the remote account is administrator", ""),
("Transmit the username and password of administrator", ""),
("still_click_uac_tip", ""),
("Request Elevation", ""),
("wait_accept_uac_tip", ""),
("Elevate successfully", ""),
("uppercase", ""),
("lowercase", ""),
("digit", ""),
("special character", ""),
("length>=8", ""),
("Weak", ""),
("Medium", ""),
("Strong", ""),
("Switch Sides", ""),
("Please confirm if you want to share your desktop?", ""),
("Display", ""),
("Closed manually by web console", "Kézzel zárva a webkonzolon keresztül"),
("Local keyboard type", "Helyi billentyűzet típusa"),
("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"),
("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres renderelés használata segíthet. A szoftvert újra kell indítani."),
("Always use software rendering", "Mindig szoftveres renderelést használjon"),
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \" Bemenet figyelése\" jogosultságot."),
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."),
("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."),
("Wait", "Várjon"),
("Elevation Error", "Emeltszintű hozzáférési hiba"),
("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"),
("Choose this if the remote account is administrator", "Válassza ezt, ha a távoli fiók rendszergazda"),
("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"),
("still_click_uac_tip", "A távoli felhasználónak továbbra is a \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
("Request Elevation", "Emelt szintű jogok igénylése"),
("wait_accept_uac_tip", "Kérjük, várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."),
("Elevate successfully", "Emelt szintű jogok megadva"),
("uppercase", "NAGYBETŰS"),
("lowercase", "kisbetűs"),
("digit", "szám"),
("special character", "különleges karakter"),
("length>=8", "hossz>=8"),
("Weak", "Gyenge"),
("Medium", "Közepes"),
("Strong", "Erős"),
("Switch Sides", "Oldalváltás"),
("Please confirm if you want to share your desktop?", "Kérjük, erősítse meg, hogy meg akarja-e osztani az asztalát?"),
("Display", "Képernyő"),
("Default View Style", "Alapértelmezett megjelenítés"),
("Default Scroll Style", "Alapértelmezett görgetés"),
("Default Image Quality", "Alapértelmezett képminőség"),
("Default Codec", "Alapértelmezett kódek"),
("Bitrate", ""),
("FPS", ""),
("Auto", ""),
("Other Default Options", ""),
("Voice call", ""),
("Text chat", ""),
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
("Bitrate", "Bitsebesség"),
("FPS", "FPS"),
("Auto", "Automatikus"),
("Other Default Options", "Egyéb alapértelmezett beállítások"),
("Voice call", "Hanghívás"),
("Text chat", "Szöveges csevegés"),
("Stop voice call", "Hanghívás leállítása"),
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy relé-kiszolgálón keresztül.\nHa az első próbálkozáskor relé-kapcsolatot szeretne létrehozni, használhatja a \"/r\" utótagot. az azonosítóhoz vagy a \"Mindig relé-kiszolgálón keresztül csatlakozom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
("Reconnect", "Újracsatlakoztatás"),
("Codec", "Kódek"),
("Resolution", "Felbontás"),
("No transfers in progress", ""),
("Set one-time password length", ""),
("RDP Settings", ""),
("Sort by", ""),
("New Connection", ""),
("Restore", ""),
("Minimize", ""),
("Maximize", ""),
("Your Device", ""),
("empty_recent_tip", ""),
("empty_favorite_tip", ""),
("empty_lan_tip", ""),
("empty_address_book_tip", ""),
("eg: admin", ""),
("Empty Username", ""),
("Empty Password", ""),
("Me", ""),
("identical_file_tip", ""),
("show_monitors_tip", ""),
("View Mode", ""),
("login_linux_tip", ""),
("verify_rustdesk_password_tip", ""),
("remember_account_tip", ""),
("os_account_desk_tip", ""),
("OS Account", ""),
("another_user_login_title_tip", ""),
("another_user_login_text_tip", ""),
("xorg_not_found_title_tip", ""),
("xorg_not_found_text_tip", ""),
("no_desktop_title_tip", ""),
("no_desktop_text_tip", ""),
("No transfers in progress", "Nincs folyamatban átvitel"),
("Set one-time password length", "Állítsa be az egyszeri jelszó hosszát"),
("RDP Settings", "RDP beállítások"),
("Sort by", "Rendezés"),
("New Connection", "Új kapcsolat"),
("Restore", "Visszaállítás"),
("Minimize", "Minimalizálás"),
("Maximize", "Maximalizálás"),
("Your Device", "Az Ön eszköze"),
("empty_recent_tip", "Hoppá, nincsenek aktuális munkamenetek!\nIdeje ütemezni egy újat."),
("empty_favorite_tip", "Még nincs kedvenc távoli állomás?\nHagyd, hogy találjunk valakit, akivel kapcsolatba tudunk lépni, és add hozzá a kedvenceidhez!"),
("empty_lan_tip", "Ó, nem, úgy tűnik, még nem fedeztünk fel egy távoli helyszínt."),
("empty_address_book_tip", "Ó, kedvesem, úgy tűnik, hogy jelenleg nincsenek távoli állomások a címjegyzékében."),
("eg: admin", "pl: admin"),
("Empty Username", "Üres felhasználónév"),
("Empty Password", "Üres jelszó"),
("Me", "Én"),
("identical_file_tip", "Ez a fájl megegyezik a távoli állomás fájljával."),
("show_monitors_tip", "Képernyők megjelenítése az eszköztáron"),
("View Mode", "Nézet mód"),
("login_linux_tip", "Az X-asztal munkamenet megnyitásához be kell jelentkeznie egy távoli Linux-fiókba."),
("verify_rustdesk_password_tip", "RustDesk jelszó megerősítése"),
("remember_account_tip", "Emlékezzen erre a fiókra"),
("os_account_desk_tip", "Ezzel a fiókkal bejelentkezhet a távoli operációs rendszerbe, és aktiválhatja az asztali munkamenetet fej nélküli módban."),
("OS Account", "OS fiók"),
("another_user_login_title_tip", "Egy másik felhasználó már bejelentkezett."),
("another_user_login_text_tip", "Különálló"),
("xorg_not_found_title_tip", "Xorg nem található."),
("xorg_not_found_text_tip", "Kérjük, telepítse az Xorg-ot."),
("no_desktop_title_tip", "Nem áll rendelkezésre asztali környezet."),
("no_desktop_text_tip", "Kérjük, telepítse a GNOME asztalt."),
("No need to elevate", ""),
("System Sound", ""),
("Default", ""),
("New RDP", ""),
("Fingerprint", ""),
("Copy Fingerprint", ""),
("no fingerprints", ""),
("Select a peer", ""),
("Select peers", ""),
("Plugins", ""),
("Uninstall", ""),
("Update", ""),
("Enable", ""),
("Disable", ""),
("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
("Collapse toolbar", ""),
("Accept and Elevate", ""),
("accept_and_elevate_btn_tooltip", ""),
("clipboard_wait_response_timeout_tip", ""),
("Incoming connection", ""),
("Outgoing connection", ""),
("Exit", ""),
("Open", ""),
("logout_tip", ""),
("System Sound", "A jogok növelése nem szükséges"),
("Default", "Alapértelmezett"),
("New RDP", "Új RDP"),
("Fingerprint", "Ujjlenyomat"),
("Copy Fingerprint", "Ujjlenyomat másolása"),
("no fingerprints", "nincsenek ujjlenyomatok"),
("Select a peer", "Egy távoli állomás kiválasztása"),
("Select peers", "Távoli állomások kiválasztása"),
("Plugins", "Beépülő modulok"),
("Uninstall", "Eltávolítás"),
("Update", "Frissítés"),
("Enable", "Engedélyezés"),
("Disable", "Letiltás"),
("Options", "Beállítások"),
("resolution_original_tip", "Eredeti felbontás"),
("resolution_fit_local_tip", "Helyi felbontás beállítása"),
("resolution_custom_tip", "Testreszabható felbontás"),
("Collapse toolbar", "Eszköztár összecsukása"),
("Accept and Elevate", "Elfogadás és magasabb szintű jogrosultságra emelés"),
("accept_and_elevate_btn_tooltip", "Fogadja el a kapcsolatot, és növelje az UAC-engedélyeket."),
("clipboard_wait_response_timeout_tip", "Időtúllépés, amíg a másolat válaszára vár."),
("Incoming connection", "Bejövő kapcsolat"),
("Outgoing connection", "Kimenő kapcsolat"),
("Exit", "Kilépés"),
("Open", "Megnyitás"),
("logout_tip", "Biztosan le szeretne iratkozni?"),
("Service", "Szolgáltatás"),
("Start", "Indítás"),
("Stop", "Leállítás"),
("exceed_max_devices", ""),
("Sync with recent sessions", ""),
("Sort tags", ""),
("Open connection in new tab", ""),
("Move tab to new window", ""),
("Can not be empty", ""),
("Already exists", ""),
("Change Password", ""),
("Refresh Password", ""),
("ID", ""),
("Grid View", ""),
("List View", ""),
("Select", ""),
("Toggle Tags", ""),
("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
("Installation Successful!", ""),
("Installation failed!", ""),
("Reverse mouse wheel", ""),
("{} sessions", ""),
("scam_title", ""),
("scam_text1", ""),
("scam_text2", ""),
("Don't show again", ""),
("I Agree", ""),
("Decline", ""),
("Timeout in minutes", ""),
("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""),
("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
("pull_group_failed_tip", ""),
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("Test", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
("Use all my displays for the remote session", ""),
("selinux_tip", ""),
("Change view", ""),
("Big tiles", ""),
("Small tiles", ""),
("List", ""),
("Virtual display", ""),
("Plug out all", ""),
("True color (4:4:4)", ""),
("Enable blocking user input", ""),
("id_input_tip", ""),
("privacy_mode_impl_mag_tip", ""),
("privacy_mode_impl_virtual_display_tip", ""),
("Enter privacy mode", ""),
("Exit privacy mode", ""),
("idd_not_support_under_win10_2004_tip", ""),
("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", ""),
("Read-only", ""),
("Read/Write", ""),
("Full Control", ""),
("share_warning_tip", ""),
("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", ""),
("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", ""),
("Floating window", ""),
("floating_window_tip", ""),
("Keep screen on", ""),
("Never", ""),
("During controlled", ""),
("During service is on", ""),
("Capture screen using DirectX", ""),
("Back", ""),
("Apps", ""),
("Volume up", ""),
("Volume down", ""),
("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", ""),
("web_id_input_tip", ""),
("Download", ""),
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("exceed_max_devices", "Elérte a felügyelt eszközök maximális számát."),
("Sync with recent sessions", "Szinkronizálás a legutóbbi munkamenetekkel"),
("Sort tags", "Címkék rendezése"),
("Open connection in new tab", "Kapcsolat megnyitása új lapon"),
("Move tab to new window", "Lap áthelyezése új ablakba"),
("Can not be empty", "Nem lehet üres"),
("Already exists", "Már létezik"),
("Change Password", "Jelszó módosítása"),
("Refresh Password", "Jelszó frissítése"),
("ID", "ID"),
("Grid View", "Mozaik nézet"),
("List View", "Lista nézet"),
("Select", "Kiválasztás"),
("Toggle Tags", "Címke kapcsoló"),
("pull_ab_failed_tip", "A címjegyzék frissítése nem sikerült"),
("push_ab_failed_tip", "A címjegyzék szinkronizálása a szerverrel nem sikerült"),
("synced_peer_readded_tip", "A legutóbbi munkamenetekben jelen lévő eszközök ismét felkerülnek a címjegyzékbe."),
("Change Color", "Szín módosítása"),
("Primary Color", "Elsődleges szín"),
("HSV Color", "HSV szín"),
("Installation Successful!", "Sikeres telepítés!"),
("Installation failed!", "A telepítés nem sikerült!"),
("Reverse mouse wheel", "Fordított egérgörgő"),
("{} sessions", "{} munkamenet"),
("scam_title", "Lehet, hogy átverték!"),
("scam_text1", "Ha olyan valakivel beszél telefonon, akit NEM ISMER, akiben NEM BÍZIK MEG, és aki arra kéri, hogy használja a RustDesket és indítsa el a szolgáltatást, ne folytassa, és azonnal tegye le a telefont."),
("scam_text2", "Valószínűleg egy csaló próbálja ellopni a pénzét vagy más személyes adatait."),
("Don't show again", "Ne mutasd újra"),
("I Agree", "Elfogadom"),
("Decline", "Elutasítom"),
("Timeout in minutes", "Időtúllépés percekben"),
("auto_disconnect_option_tip", "A bejövő munkamenetek automatikus bezárása, ha a felhasználó inaktív"),
("Connection failed due to inactivity", "A kapcsolat inaktivitás miatt megszakadt"),
("Check for software update on startup", "Szoftverfrissítés keresése indításkor"),
("upgrade_rustdesk_server_pro_to_{}_tip", "Kérjük, frissítse a RustDesk Server Pro-t a(z) {} vagy újabb verzióra!"),
("pull_group_failed_tip", "A csoport frissítése nem sikerült"),
("Filter by intersection", "Szűrés metszéspontok szerint"),
("Remove wallpaper during incoming sessions", "Távolítsa el a háttérképet a bejövő munkamenetek során"),
("Test", "Teszt"),
("display_is_plugged_out_msg", "A képernyő nincs csatlakoztatva, váltson az első képernyőre."),
("No displays", "Nincsenek kijelzők"),
("Open in new window", "Megnyitás új ablakban"),
("Show displays as individual windows", "Kijelzők megjelenítése egyedi ablakokként"),
("Use all my displays for the remote session", "Az összes kijelzőm használata a távoli munkamenethez"),
("selinux_tip", "A SELinux engedélyezve van az eszközén, ami azt okozhatja, hogy a RustDesk nem fut megfelelően, mint ellenőrzött webhely."),
("Change view", "Nézet módosítása"),
("Big tiles", "Nagy csempék"),
("Small tiles", "Kis csempék"),
("List", "Lista"),
("Virtual display", "Virtuális kijelző"),
("Plug out all", "Kapcsolja ki az összeset"),
("True color (4:4:4)", "Valódi szín (4:4:4)"),
("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"),
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik szerveren lévő eszközhöz szeretne hozzáférni, adja meg a szerver címét (<id>@<szerver_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános szerveren lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" lehetőséget. in. A kulcsra nincs szükség nyilvános szerverek esetén.\n\nHa az első kapcsolathoz relé-kapcsolatot akar kényszeríteni, adjon hozzá \"/r\" az azonosító végén, például \"9123456234/r\"."),
("privacy_mode_impl_mag_tip", "1. mód"),
("privacy_mode_impl_virtual_display_tip", "2. mód"),
("Enter privacy mode", "Lépjen be az adatvédelmi módba"),
("Exit privacy mode", "Lépjen ki az adatvédelmi módból"),
("idd_not_support_under_win10_2004_tip", "A közvetett grafikus illesztőprogram nem támogatott. Windows 10, 2004-es vagy újabb verzió szükséges."),
("input_source_1_tip", "1. bemeneti forrás"),
("input_source_2_tip", "2. bemeneti forrás"),
("Swap control-command key", "Vezérlő- és parancsgombok cseréje"),
("swap-left-right-mouse", "Bal és jobb egérgomb felcserélése"),
("2FA code", "2FA kód"),
("More", "További"),
("enable-2fa-title", "Kétfaktoros hitelesítés aktiválása"),
("enable-2fa-desc", "Kérjük, most állítsa be a hitelesítőt. Használhat egy hitelesítési alkalmazást, például az Authy, a Microsoft vagy a Google Authenticator alkalmazást a telefonján vagy az asztali számítógépén.\n\nScannelje be a QR-kódot az alkalmazással, és adja meg az alkalmazás által megjelenített kódot a kétfaktoros hitelesítés aktiválásához."),
("wrong-2fa-code", "A kód nem ellenőrizhető. Ellenőrizze, hogy a kód és a helyi idő beállításai helyesek-e."),
("enter-2fa-title", "Kétfaktoros hitelesítés"),
("Email verification code must be 6 characters.", "Az e-mail ellenőrző kódnak 6 karakterből kell állnia."),
("2FA code must be 6 digits.", "A 2FA-kódnak 6 számjegyűnek kell lennie."),
("Multiple Windows sessions found", "Több Windows munkamenet található"),
("Please select the session you want to connect to", "Kérjük, válassza ki a munkamenetet, amelyhez csatlakozni szeretne"),
("powered_by_me", "Üzemeltető: RustDesk"),
("outgoing_only_desk_tip", "Ez a RustDesk testreszabott kimenete.\nMás eszközökhöz csatlakozhat, de más eszközök nem csatlakozhatnak az Ön eszközéhez."),
("preset_password_warning", "Ez egy testreszabott kimenet a RustDeskből egy előre beállított jelszóval. Bárki, aki ismeri ezt a jelszót, teljes irányítást szerezhet a készülék felett. Ha nem kívánja ezt megtenni, kérjük, azonnal távolítsa el ezt a szoftvert."),
("Security Alert", "Biztonsági riasztás"),
("My address book", "Címjegyzékem"),
("Personal", "Személyes"),
("Owner", "Tulajdonos"),
("Set shared password", "Megosztott jelszó beállítása"),
("Exist in", "Létezik"),
("Read-only", "Csak olvasható"),
("Read/Write", "Olvasás/Írás"),
("Full Control", "Teljes ellenőrzés"),
("share_warning_tip", "A fenti mezők megosztottak és mások számára is láthatóak."),
("Everyone", "Mindenki"),
("ab_web_console_tip", "További információk a webes konzolról"),
("allow-only-conn-window-open-tip", "Csak akkor engedélyezze a csatlakozást, ha a RustDesk ablak nyitva van."),
("no_need_privacy_mode_no_physical_displays_tip", "Nincsenek fizikai képernyők; Nincs szükség az adatvédelmi üzemmód használatára."),
("Follow remote cursor", "Kövesse a távoli kurzort"),
("Follow remote window focus", "Kövesse a távoli ablak fókuszt"),
("default_proxy_tip", "A szabványos protokoll és port SOCKS5 és 1080"),
("no_audio_input_device_tip", "Nem található hangbemeneti eszköz."),
("Incoming", "Bejövő"),
("Outgoing", "Kimenő"),
("Clear Wayland screen selection", "Wayland képernyő kiválasztásának törlése"),
("clear_Wayland_screen_selection_tip", "A képernyőválasztás törlése után újra kiválaszthatja a megosztandó képernyőt."),
("confirm_clear_Wayland_screen_selection_tip", "Biztos, hogy törölni szeretné a Wayland képernyő kiválasztását?"),
("android_new_voice_call_tip", "Új hanghívás-kérés érkezett. Ha elfogadja a megkeresést, a hang átvált hangkommunikációra."),
("texture_render_tip", "Használja a textúra renderelést a képek simábbá tételéhez. Ezt az opciót kikapcsolhatja, ha renderelési problémái vannak."),
("Use texture rendering", "Textúra renderelés használata"),
("Floating window", "Lebegő ablak"),
("floating_window_tip", "Segít, ha a RustDesk a háttérben fut."),
("Keep screen on", "Tartsa a képernyőt bekapcsolva"),
("Never", "Soha"),
("During controlled", "Amikor ellenőrzött"),
("During service is on", "Amikor a szolgáltatás fut"),
("Capture screen using DirectX", "Képernyő rögzítése DirectX használatával"),
("Back", "Vissza"),
("Apps", "Alkalmazások"),
("Volume up", "Hangerő fel"),
("Volume down", "Hangerő le"),
("Power", "Teljesítmény"),
("Telegram bot", "Telegram bot"),
("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."),
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjelrel kezdődik (\"/\"), pl. B. \"/hello\" az aktiváláshoz.\n"),
("cancel-2fa-confirm-tip", "Biztos, hogy le akarja mondani a 2FA-t?"),
("cancel-bot-confirm-tip", "Biztos, hogy le akarod mondani a Telegram botot?"),
("About RustDesk", "A RustDeskről"),
("Send clipboard keystrokes", "Vágólap billentyűleütések küldése"),
("network_error_tip", "Kérjük, ellenőrizze a hálózati kapcsolatot, majd próbálja meg újra."),
("Unlock with PIN", "Feloldás PIN-kóddal"),
("Requires at least {} characters", "Legalább {} karakter szükséges"),
("Wrong PIN", "Hibás PIN"),
("Set PIN", "PIN-kód beállítása"),
("Enable trusted devices", "Megbízható eszközök engedélyezése"),
("Manage trusted devices", "Megbízható eszközök kezelése"),
("Platform", "Platform"),
("Days remaining", "Hátralévő napok"),
("enable-trusted-devices-tip", "A 2FA-ellenőrzés kihagyása megbízható eszközökön"),
("Parent directory", "Szülőkönyvtár"),
("Resume", "Folytatás"),
("Invalid file name", "Érvénytelen fájlnév"),
("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."),
("Authentication Required", "Hitelesítés szükséges"),
("Authenticate", "Hitelesítés"),
("web_id_input_tip", "Azonos szerveren lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik szerveren lévő eszközhöz szeretne hozzáférni, kérjük, adja meg a szerver címét (<id>@<szerver_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános szerveren lévő eszközhöz szeretne hozzáférni, kérjük, adja meg a \"<id>@public\" betűt. in. A kulcsra nincs szükség a nyilvános szerverek esetében."),
("Download", "Letöltés"),
("Upload folder", "Mappa feltöltése"),
("Upload files", "Fájlok feltöltése"),
("Clipboard is synchronized", "A vágólap szinkronizálva van"),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", ""),
("Upload files", ""),
("Clipboard is synchronized", ""),
("Update client clipboard", ""),
("Untagged", ""),
].iter().cloned().collect();
}

View File

@@ -653,5 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", "Cartella upload"),
("Upload files", "File upload"),
("Clipboard is synchronized", "Gli appunti sono sincronizzati"),
("Update client clipboard", "Aggiorna appunti client"),
("Untagged", ""),
].iter().cloned().collect();
}

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