Compare commits

...

342 Commits
1.2.5 ... 1.2.7

Author SHA1 Message Date
rustdesk
97f26f880b default-connect-password option 2024-07-23 10:46:50 +08:00
rustdesk
22c6f5e589 aggresive online query for self-hosting 2024-07-23 10:26:56 +08:00
お餅のCreeeper
b828768fa9 Update ja.rs and Add "About Rustdesk" (#8784)
* Update ja.rs

Signed-off-by: creeper-0910 <56744841+creeper-0910@users.noreply.github.com>

* Update ja.rs

Signed-off-by: creeper-0910 <56744841+creeper-0910@users.noreply.github.com>

* Add 'About RustDesk'

Signed-off-by: creeper-0910 <56744841+creeper-0910@users.noreply.github.com>

---------

Signed-off-by: creeper-0910 <56744841+creeper-0910@users.noreply.github.com>
2024-07-23 08:58:48 +08:00
dignow
31e7b6acf1 refact: msi, revision version (#8782)
* refact: msi, revision version

Signed-off-by: dignow <linlong1265@gmail.com>

* check revision version

Signed-off-by: dignow <linlong1265@gmail.com>

* refact: msi, default revision number

Signed-off-by: dignow <linlong1265@gmail.com>

* Simple refact

Signed-off-by: dignow <linlong1265@gmail.com>

---------

Signed-off-by: dignow <linlong1265@gmail.com>
2024-07-22 23:37:25 +08:00
21pages
14b505130b remove end slash when setting server config (#8779)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-22 17:11:32 +08:00
21pages
22f3425ace fix custom client show ip whiltelist warning (#8778)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-22 17:00:29 +08:00
rustdesk
4723d6a830 mouse wrong commit ref 2024-07-22 10:29:00 +08:00
DiegoGZ
c4f3c0f133 Update ca.rs - Added new translation into Catalan and a few corrections (#8777)
Added new translation into Catalan and a few corrections
2024-07-22 09:58:04 +08:00
Bot_wxt1221
de375c91bb fix: nixos's headless mode. (#8774) 2024-07-21 17:05:32 +08:00
dignow
d3454f07d3 fix: move some crates to rustdesk-org (#8772)
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2024-07-20 23:49:40 +08:00
fufesou
cf8ef2533a fix: keyboard, linux, repeated keys, #6793 (#8757)
* fix: keyboard, linux, repeat keys, #6793

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

* fix: keyboard, linux->linux, may also repeat keys with bad network

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-20 00:42:49 +08:00
お餅のCreeeper
6ad662260e Update ja.rs (#8758)
* Update ja.rs

Signed-off-by: creeper-0910 <56744841+creeper-0910@users.noreply.github.com>

* Update README-JP.md

Signed-off-by: creeper-0910 <56744841+creeper-0910@users.noreply.github.com>

---------

Signed-off-by: creeper-0910 <56744841+creeper-0910@users.noreply.github.com>
2024-07-19 23:56:39 +08:00
21pages
2b54a553c7 buildin options and add to mobile (#8759)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-19 23:55:52 +08:00
rustdesk
85ded0a3e5 no idea why publish sciter windows tar.gz, remove it 2024-07-19 00:32:19 +08:00
21pages
5c16a8302e Revert vcpkg ffmpeg (#8751)
* Revert "fix linux ffmpeg link, still link ffmpeg in hwcodec (#8747)"

This reverts commit 48464835f5.

* Revert "fix linux armv7 ffmpeg arch, linux x64 sciter add hwcodec feature (#8744)"

This reverts commit b68d7a3054.

* Revert "arm linux remove cuda-llvm option (#8735)"

This reverts commit bc672b3367.

* Reapply "try fix arm64 linux ci and publish error log (#8730)" (#8732)

This reverts commit e283d33f28.

* Revert "try fix arm64 linux ci and publish error log (#8730)"

This reverts commit a4565bf0da.

* Revert "F-Droid: enable hwcodec for future builds (#8726)"

This reverts commit 188f85b042.

* Revert "remove unused vcpkg ffmpeg code (#8725)"

This reverts commit 72c96f22b6.

* Revert "install ffmpeg lib with vcpkg (#8724)"

This reverts commit 0143eaf601.

* update hwcodec to reverted

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-18 22:28:35 +08:00
21pages
04c175c62e fix custom client settings tab visibility (#8752)
* fix custom client settings tab visibility

only control with tabKeys

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

* fix preset-user-name typo

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-18 22:27:31 +08:00
fufesou
2be05608d8 fix: windows, headless, not work when exiting RDP (#8753)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-18 22:23:45 +08:00
21pages
c3c99ba107 fix missing retry (#8750)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-18 17:16:25 +08:00
fufesou
a81d6468cc fix: desktop settings, disconnect, timeout (#8749)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-18 15:57:36 +08:00
21pages
48464835f5 fix linux ffmpeg link, still link ffmpeg in hwcodec (#8747)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-18 13:18:21 +08:00
rustdesk
edc5d86ee7 fix hide-help-cards and fix https://github.com/rustdesk/rustdesk/issues/8687 2024-07-18 11:26:11 +08:00
fufesou
e9c8ba5393 fix: macos, remove unused workaround (#8746)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-18 10:38:15 +08:00
rustdesk
a72bc0fb28 hide-help-cards 2024-07-18 09:47:04 +08:00
rustdesk
5a8c8cbf7c hide-help-cards 2024-07-18 09:47:04 +08:00
21pages
b68d7a3054 fix linux armv7 ffmpeg arch, linux x64 sciter add hwcodec feature (#8744)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-17 22:37:05 +08:00
rustdesk
9e931a6f04 change remove-preset-password-warning default value to Y 2024-07-17 21:17:54 +08:00
rustdesk
f0587796e2 OPTION_PRESET_STRATEGY_NAME, OPTION_PRESET_USERNAME 2024-07-17 20:11:30 +08:00
fufesou
875ac28ab5 fix: macos, remove useless workaround (#8738)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-17 19:13:03 +08:00
お餅のCreeeper
6821bef5e5 Update README-JP.md and ja.rs (#8737)
* Update README-JP.md

Signed-off-by: creeper-0910 <56744841+creeper-0910@users.noreply.github.com>

* Update ja.rs

Signed-off-by: creeper-0910 <56744841+creeper-0910@users.noreply.github.com>

* Fix README-JP.md

Signed-off-by: creeper-0910 <56744841+creeper-0910@users.noreply.github.com>

---------

Signed-off-by: creeper-0910 <56744841+creeper-0910@users.noreply.github.com>
2024-07-17 19:11:04 +08:00
rustdesk
930561f431 remove unused protobuf 2024-07-17 18:17:30 +08:00
21pages
bc672b3367 arm linux remove cuda-llvm option (#8735)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-17 17:19:21 +08:00
21pages
e283d33f28 Revert "try fix arm64 linux ci and publish error log (#8730)" (#8732)
setup a selfhost and will fix it later

This reverts commit a4565bf0da.
2024-07-17 13:35:32 +08:00
fufesou
901505e8be fix: macos, load multi dylib instances (#8731)
Multiple dylib instances will cause some global instances to be invalid.

eg. lazy_static objects in rust side, will be created more than once.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-17 11:49:11 +08:00
21pages
a4565bf0da try fix arm64 linux ci and publish error log (#8730)
FFmepg can be built on arm64 ubuntu vm, possible reason is that the
condition of nvcodec can't be satisfied. If this still can't work, nvcodec can
be removed for arm linux.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-17 11:40:50 +08:00
fufesou
092e4089c7 fix: try workaround, macos, subwindow, frozen (#8729)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-17 09:55:46 +08:00
Vasyl Gello
188f85b042 F-Droid: enable hwcodec for future builds (#8726)
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-16 16:35:10 +08:00
21pages
72c96f22b6 remove unused vcpkg ffmpeg code (#8725)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-16 15:46:20 +08:00
21pages
0143eaf601 install ffmpeg lib with vcpkg (#8724)
* use vcpkg to install ffmpeg and build sdk from source, so no prebuild lib in hwcodec.
* link ffmpeg in rustdesk directly, ffmpeg can be used as basic library.
* for windows developers, `VCPKG_DEFAULT_HOST_TRIPLET` env need to be set to `x64-windows-static` during installation.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-16 15:10:24 +08:00
21pages
09466680d3 mobile virtual display, resolution menu, proxy setting (#8717)
1. Merge code of mobile and desktop virtual display menu.
2. Mobile add seperate resolution menu, only support changing
   resolutions.
3. Android add proxy setting

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-15 18:53:14 +08:00
fufesou
eec879a801 refact: macos, hide&show on leaving view (#8712)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-15 10:50:54 +08:00
21pages
3f11d9cdb6 remove Instant sub (#8714)
which cause crash when connect to windows just startup

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-15 10:49:09 +08:00
21pages
8512c2b2b0 fix lan peers batch deletion (#8715)
`peers.map((p) async {})` is not sync and peers are loaded before deletion.

```
let mut peers = config::LanPeers::load().peers;
peers.retain(|x| x.id != id);
```

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-15 10:47:19 +08:00
fufesou
e2a7e38a39 fix: build ios (#8709)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-14 15:05:56 +08:00
21pages
3a0ece1447 mobile view mode menu (#8707)
* mobile add view mode menu, disable same menus as desktop
* show forbidden cursor when not view mode && no keyboard permission &&
  not show remote cursor
* hide keyboard/mouse bottom action when keyboard disabled
* fix not listen keyboard permission change
* mobile missing changing resolution and http proxy setting, will add if
  needed.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-14 15:04:35 +08:00
rustdesk
d0a54a6cc6 hc 2024-07-14 05:47:42 +08:00
RustDesk
bed214bd37 Update playground.yml 2024-07-14 04:56:03 +08:00
RustDesk
5f31211db3 Update playground.yml 2024-07-14 04:35:23 +08:00
RustDesk
29b8875c1c Update playground.yml 2024-07-14 04:28:22 +08:00
RustDesk
3367c541b2 Update playground.yml 2024-07-14 04:08:11 +08:00
fufesou
30afe4f779 refact: seperate audio device for voice call (#8703)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-14 04:07:02 +08:00
fufesou
d18e95703e fix: mouse forward back (#8705)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-14 03:49:16 +08:00
RustDesk
9adc083def Update playground.yml 2024-07-14 03:47:15 +08:00
RustDesk
0dc664474a Update playground.yml 2024-07-14 03:46:48 +08:00
Vasyl Gello
5e8fe239fa build_fdroid.sh: Fix review points by Licaon_Kter (#8701)
* NDK should be downloaded via verified Android SDK's sdkmanager
  * vcpkg should be built from sources not prebuilt

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-13 20:46:05 +08:00
RustDesk
7a3100a87c Update playground.yml 2024-07-13 16:08:07 +08:00
RustDesk
8a1acedae5 Update playground.yml 2024-07-13 15:51:51 +08:00
RustDesk
f5bcc17636 Update playground.yml 2024-07-13 15:45:59 +08:00
RustDesk
883c630206 Update playground.yml 2024-07-13 15:35:24 +08:00
RustDesk
a95a6ab733 Update playground.yml 2024-07-13 15:24:01 +08:00
RustDesk
46605fab1b Update playground.yml 2024-07-13 14:33:17 +08:00
RustDesk
9d26fec631 Update playground.yml 2024-07-13 11:33:15 +08:00
RustDesk
294a6ce9bc Update playground.yml 2024-07-13 11:15:00 +08:00
RustDesk
183ea47ba4 Update playground.yml 2024-07-13 01:29:17 +08:00
RustDesk
06e04143a8 Update playground.yml 2024-07-13 01:26:09 +08:00
RustDesk
a532b36e28 Update playground.yml 2024-07-13 01:03:43 +08:00
fufesou
c873b69662 refact: unwrap offstage for cursor paint (#8693)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-13 00:44:08 +08:00
RustDesk
b30f84623b Update playground.yml 2024-07-12 23:44:25 +08:00
fufesou
888e993534 fix: unable to close on fullscreen (#8690)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-12 22:04:11 +08:00
rustdesk
1d59a7fe5f adjust uni_link_desktop version to make work with old flutter 2024-07-12 18:07:27 +08:00
RustDesk
2c027cdcf5 Update playground.yml 2024-07-12 17:55:25 +08:00
RustDesk
fe513dd967 Update playground.yml 2024-07-12 17:26:00 +08:00
RustDesk
d652b99d5b Update playground.yml 2024-07-12 16:33:55 +08:00
RustDesk
c2716c2509 Update playground.yml 2024-07-12 15:44:51 +08:00
21pages
821f7245b0 videotoolbox ram codec (#8683)
* h265 encoding: the second frame always fails, use repeat encoding to achieve real-time encoding
* h264 encoding: Not supported because encoding fails too frequently, about 50%, with one failure followed by another success.
* h264/h265 decoding: No issues found.
* Does not support dynamically changing the bitrate and changing the quality by resetting the encoder.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-12 11:08:51 +08:00
RustDesk
0ea88ce6ff Update playground.yml 2024-07-12 11:01:12 +08:00
RustDesk
21f41e98a0 Update playground.yml 2024-07-12 10:44:04 +08:00
RustDesk
282ea02ebf Update playground.yml 2024-07-12 10:41:13 +08:00
RustDesk
170200fa49 Update playground.yml 2024-07-12 10:14:25 +08:00
rustdesk
d8cee6507d test old flutter for mac intel freeze issue 2024-07-12 10:07:55 +08:00
fufesou
2391b18046 fix: ubuntu 18.04, service, Exec format error (#8680)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-11 23:13:41 +08:00
fufesou
b5a7165015 feat: android, controlled side, clipboard text (#8677)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-11 12:02:19 +08:00
RustDesk
ef4d84657b Revert "feat: clipboard, multi format (#8672)" (#8673)
This reverts commit 011647511c.
2024-07-11 00:36:29 +08:00
fufesou
011647511c feat: clipboard, multi format (#8672)
* feat: clipboard, multi format

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

* inline

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-11 00:05:25 +08:00
21pages
e2d217a138 fix wayland Display::all() comment (#8664)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-09 22:10:39 +08:00
21pages
f07936a911 wayland not call check_get_displays_changed_msg (#8637)
* wayland not call check_get_displays_changed_msg

For wayland, call Display::all() in video service will cause block, I
don't know the reason.

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

* Update display_service.rs

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-07-09 18:01:30 +08:00
21pages
0bb4d43e9e add custom client option to allow remote cm modification (#8660)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-09 15:19:16 +08:00
Vasyl Gello
6f74080a2d build_fdroid: Move sudo-deps back to f-droid spec (#8659)
... as requested by linsui here:
https://gitlab.com/fdroid/fdroiddata/-/merge_requests/15343#note_1988918695

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-09 14:36:05 +08:00
rustdesk
8a370e640a add hide-username-on-card, https://github.com/rustdesk/rustdesk-server-pro/issues/284#issuecomment-2216521407 2024-07-09 13:45:54 +08:00
Vasyl Gello
d007408061 Factor out F-Droid buildscript (#8625)
Now RustDesk maintainers can make proactive changes in F-Droid
packaging script so F-Droid side is (fully) automated.

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-09 11:45:08 +08:00
fufesou
02572e9032 fix: obx, no rx value (#8652)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-08 21:51:16 +08:00
21pages
af66d2a73b main window add block mask, cm add keyboard block (#8640)
* block window body only so user can still click minisize button.
* cm doesn't show mask
* Remove focusable Offstage in tabbar_widget.dart

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-08 20:08:05 +08:00
bovirus
eb5ab4d7d9 Update Italian language (#8645) 2024-07-08 19:59:29 +08:00
rustdesk
c02b4f994a fix two tray icon on macos 2024-07-08 14:46:28 +08:00
XLion
d093fdc256 Translation (#8634)
* Update tw.rs

* Update en.rs: Using `.` instead of `,`

* Update tw.rs: Fix formatting
2024-07-08 12:10:02 +08:00
FastAct
2af799f46e Update nl.rs (#8632) 2024-07-08 12:09:50 +08:00
rustdesk
3c7e24c605 add MOUSE_MOVE_TIME to key event, may work for
https://github.com/rustdesk/rustdesk/issues/8633 when window resume via
keyboard rather than mouse
2024-07-08 11:38:42 +08:00
rustdesk
7d961d895b add assign function in device.py for user/strategy/ab 2024-07-07 16:35:57 +08:00
rustdesk
53dbc2fa6f add OPTION_DISABLE_UDP 2024-07-07 09:25:27 +08:00
rustdesk
024220e58a add more fine-grained hide options for https://github.com/rustdesk/rustdesk-server-pro/issues/276 2024-07-06 23:41:53 +08:00
rustdesk
8621b93436 add display-name option for https://github.com/rustdesk/rustdesk-server-pro/issues/277 2024-07-06 23:10:50 +08:00
rustdesk
ac88121c4a fix ci 2024-07-06 11:46:33 +08:00
Vasilis553
90df80ed78 Update el.rs (#8628)
* Update el.rs

* Update el.rs
2024-07-06 11:35:29 +08:00
rustdesk
d4f3a87276 change to remove-preset-password-warning 2024-07-05 20:09:26 +08:00
rustdesk
48efdcf1f0 add remove-preset-password-warning-of 2024-07-05 19:26:20 +08:00
fufesou
0511cdbb21 feat: clipboard svg (#8615)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-05 09:30:59 +08:00
Thadah D. Denyse
8747b9847f Add basque language (#8597)
* Add basque language

* Update lang.rs
2024-07-05 09:30:07 +08:00
RustDesk
92d0fe1c3f Revert "feat: clipboard svg (#8608)" (#8612)
This reverts commit a9015bcf70.
2024-07-04 21:31:19 +08:00
fufesou
a9015bcf70 feat: clipboard svg (#8608)
* feat: clipboard svg

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

* fix: is_last_plain, reset on clipboard event

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-04 21:23:08 +08:00
flusheDData
f8f2686267 Update es.rs (#8609)
* Update es.rs

New terms added

* Update es.rs

Mistype correction
2024-07-04 20:21:28 +08:00
solokot
c2bd1b8965 Update ru.rs (#8610) 2024-07-04 20:21:18 +08:00
Nevaran
4eeee5b7ee Update bg.rs (#8611)
Added more translated parts, some might need more context to be more accurate
2024-07-04 20:21:06 +08:00
fufesou
dfc224ec01 fix: #8599 (#8603)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-04 20:20:25 +08:00
fufesou
86ff768241 clear unwrap (#8605)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-04 20:18:53 +08:00
jxdv
94addb162b update sk && cz tr (#8595)
* update sk translations

* update cz translations
2024-07-04 11:43:09 +08:00
rustdesk
bea65f8739 fix ci 2024-07-03 21:47:13 +08:00
rustdesk
92f570831d return x11 for tty session 2024-07-03 21:37:25 +08:00
Kleofass
9349210a87 Update lv.rs (#8591) 2024-07-03 20:40:57 +08:00
Vasyl Gello
95f4274eca vcpkg: sort triplets and restore local opus port for 1.5.1 (#8585)
* ci/flutter-build: Sort out triplets in job matrices

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>

* Restore local port for opus 1.5.1

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>

---------

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-03 17:57:58 +08:00
Vasyl Gello
a6febb2816 [WIP] Fix F-Droid version file workflow (#8583) 2024-07-03 17:12:21 +08:00
Vasyl Gello
e294dafe7c Bump vcpkg baseline to 2024.06.15 (#8582)
* res/vcpkg: drop old opus port overlay

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>

* res/vcpkg: bump aom to 3.9.1

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>

* res/vcpkg: bump libvpx to 1.14.1

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>

* Bump vcpkg baseline to 2024.06.15

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>

---------

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-07-03 15:53:17 +08:00
21pages
d00582e929 linux x11 rgb565 capture (#8580)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-03 14:20:41 +08:00
Vasyl Gello
6d2e985593 res/vcpkg: Bump aom to 3.9.1, opus to 1.5.1 and libvpx to 1.14.1 (#8555)
* res/vcpkg: drop old opus port overlay

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>

* res/vcpkg: bump aom to 3.9.1

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>

* res/vcpkg: bump libvpx to 1.14.1

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>

---------

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-03 14:12:19 +08:00
Mr-Update
182e8c4ac0 Update de.rs (#8577) 2024-07-03 11:55:59 +08:00
bovirus
40019b80f6 Update Italian language (#8576) 2024-07-03 11:55:47 +08:00
fufesou
2f40b9dc04 fix: build ios (#8570)
ld: Undefined symbols:
     _kSecMatchSubjectWholeString, referenced from:
         security_framework::item::ItemSearchOptions::search::he568de2b0004b0c0 in liblibrustdesk.a[872](security_framework-88ef6afe340eb2ab.security_framework.e4562ffe63567184-cgu.0.rcgu.o)

TODO:

There's also a warning after commit "2116fec20b5a2165df97e329c156baf308668efc".
No idea how to fix it. But it does not stop building.

ld: warning: Could not find or use auto-linked framework 'CoreAudioTypes': framework 'CoreAudioTypes' not found

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-02 15:37:45 +08:00
21pages
8602b036bd remove special treatment when only use permanent passwrod but no password set (#8566)
1. Remove special treatment when only use permanent passwrod  but no password set, it has  no need and `Connection not allowd` prompt make user confusing.
2. When only use permanent password is chosen and the permanent password
   is empty, pop up the set-password dialog, if still not set in the
   dialog, back to the old choice
3. Add cancel confirm for 2fa and telegram bot

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-02 14:32:22 +08:00
fufesou
51db8e706d fix: win, clipboard image (#8561)
The window must belong to the current thread for clipboard-master.

https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage#:~:text=The%20window%20must%20belong%20to%20the%20current%20thread.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-02 00:46:55 +08:00
21pages
a0dc38f749 try fix linux arm64 build (#8560)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-02 00:19:18 +08:00
Mr-Update
625b610cfd Update de.rs (#8559) 2024-07-02 00:18:54 +08:00
fufesou
62a8349739 fix: clipboard data, decompress, buf too small (#8556)
* fix: clipboard data, decompress, buf too small

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

* fix: compress image

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

* decompress image, use default level

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

* chore

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

* decompress, zstd::decode_all

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-02 00:18:38 +08:00
rustdesk
0ab500c27c bring fdroid.yml back 2024-07-01 16:50:27 +08:00
21pages
285e974d1a ci for linux x86_64 sciter deb and flatpak (#8545)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-01 11:01:35 +08:00
rustdesk
e71d86c124 move clipboard in common.rs to clipboard.rs 2024-07-01 02:14:58 +08:00
rustdesk
14343e89d4 fix ci 2024-07-01 01:52:39 +08:00
rustdesk
3f2dfa521c fix ci 2024-07-01 01:43:16 +08:00
rustdesk
cd73368cb9 make clipboard dropped after no sub 2024-07-01 00:24:23 +08:00
rustdesk
84b5cd70ed initialize change_count to 1 so that first data always got 2024-06-30 23:27:33 +08:00
rustdesk
01672bc697 clipboard image, not tested, todo: set_html 2024-06-30 21:57:32 +08:00
21pages
763174657b add type to all Getx put/get/delete/isRegistered (#8550)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-30 21:24:18 +08:00
rustdesk
15fa80fb26 fix ci 2024-06-30 19:17:24 +08:00
FastAct
d537e2563d Update nl.rs (#8546) 2024-06-30 19:04:22 +08:00
rustdesk
1719e478e3 remove debug code 2024-06-30 17:07:23 +08:00
rustdesk
1f129e6ef3 change update_clipboard to threaded, since creating a context may take long 2024-06-30 17:05:09 +08:00
21pages
25d0ced8ba fix ci (#8543)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-30 13:54:40 +08:00
rustdesk
2116fec20b wl-clipboard-rs 0.9 and refresh cargo.lock 2024-06-30 12:07:32 +08:00
rustdesk
1252f45506 fix 100% cpu usage of clipboard disabled on remote menu 2024-06-30 10:57:15 +08:00
fufesou
1f4c62e480 fix: linux, arboard::new(), retry (#8538)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-30 01:13:45 +08:00
fufesou
bd334769fa fix: win, virtual display, privacy mode (#8537)
Restore display settings.

Only restore by the display registry settings.

Do not change display settings twice when turning off privacy mode.

Only restore the registry values.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-30 00:44:10 +08:00
jxdv
750368af7b Update cs tr (#8536)
* update common tr

* update tips
2024-06-29 22:26:06 +08:00
jxdv
2fb35c3596 Update sk tr (#8535)
* translate normal

* translate bot tips
2024-06-29 22:16:40 +08:00
21pages
5114a9d369 shared memory size use the max resolution of all displays (#8534)
The case of setting a monitor inserted later as the primary monitor is not taken into account

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-29 21:50:21 +08:00
solokot
4b6ba7938f Update ru.rs (#8533)
* Update ru.rs

* Update ru.rs
2024-06-29 21:41:09 +08:00
Vedant
1e400d2a64 Update winget-releaser to latest (#8532) 2024-06-29 21:17:10 +08:00
fufesou
967e63266f fix: win, multi-display settings changed (#8531)
1. Windows, multi-display, as the controlled side.
2. Connect
3. Turn on privacy mode, with mode 2.
4. Turn off privacy mode.

Then the display settings are continues changed when connect &
disconnect.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-29 20:16:22 +08:00
21pages
f9b0a88213 fix switch to primary display but can't see UAC (#8527)
How to reproduce: elevate at primary display->switch to another
display->
trigger UAC->switch to primary display->can't see UAC

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-29 17:14:09 +08:00
21pages
d67afa49b4 portable service only run on main display (#8525)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-29 14:17:24 +08:00
fufesou
1fd170b089 fix: mobile more actions, check version 1.2.7 (#8526)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-29 11:50:40 +08:00
rustdesk
a632718e80 typo 2024-06-29 10:46:21 +08:00
rustdesk
9f72d05749 do not run self-host in ci commit flow 2024-06-28 23:32:23 +08:00
fufesou
c062813c6d Upgrade arboard (#8522)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-06-28 23:15:33 +08:00
21pages
3ae1638125 fix extracted forground window not foreground (#8521)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-28 22:36:29 +08:00
bovirus
96aff38862 Update Italian language (#8520) 2024-06-28 16:28:43 +08:00
rustdesk
ed3fb1efa4 bump to 1.2.7 2024-06-28 09:59:10 +08:00
fufesou
d689bbf38e refact: mobile more actions, divider (#8512)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-28 00:57:16 +08:00
fufesou
c1bbdaf9ae refact: mobile min cursor size, 12 (#8510)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-27 23:19:37 +08:00
21pages
ab9e1013b2 remove dpiAware from manifest in ci for self-extracted executable (#8508)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-27 22:23:25 +08:00
Samuel FORESTIER
e1140b1bea Tidy up configuration files UNIX permissions (#7983)
* Add missing libpam package causing build to fail using Docker image

```
wrapper.h:1:10: fatal error: 'security/pam_appl.h' file not found
```

Signed-off-by: Samuel FORESTIER <samuel+dev@forestier.app>

* Tidy up configuration files UNIX permissions

Signed-off-by: Samuel FORESTIER <samuel+dev@forestier.app>

---------

Signed-off-by: Samuel FORESTIER <samuel+dev@forestier.app>
2024-06-27 20:14:32 +08:00
21pages
cfd27c8d87 wakelock not block system sleep (#8504)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-27 20:09:51 +08:00
rustdesk
a18947eed2 fix typo 2024-06-27 18:24:04 +08:00
bovirus
f8592e0d5b Update Italian language (#8502)
* Update Italian language

* Fix typo
2024-06-27 18:10:41 +08:00
rustdesk
5bfdf05ff2 /hello rather than hello to activate bot 2024-06-27 18:01:14 +08:00
rustdesk
9e851542ec telegram bot works now 2024-06-27 17:36:50 +08:00
rustdesk
e79946b4e4 telegram bot ui settings and code sending 2024-06-27 16:18:41 +08:00
solokot
aed212d8f8 Update ru.rs (#8497) 2024-06-27 15:04:53 +08:00
fufesou
c5d3c7f390 Feat/android more actions (#8496)
* feat: android volume and power actions

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

* Add translations and refact action menus

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

* Remove divider

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

* fix: recover deleted translations

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-27 13:28:05 +08:00
fufesou
b047730830 Refact/android input changed notify clients (#8494)
* refact: android, input control changed, notify clients

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

* fix: android init input perm

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-27 13:05:45 +08:00
21pages
9c7d4ef1f7 not use nwg default features (#8492)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-27 12:11:08 +08:00
21pages
12d3c59172 windows portable loading ui (#8490)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-27 11:04:55 +08:00
Stas Solovey
ef06b7d5d0 add Belarusian locale (#8480)
* Update ru.rs

* Update ru.rs

* Update ru.rs

* Update ru.rs

* Update ru.rs

* Update ru.rs

* Update ru.rs

* Create be.rs (Belarussian locale)

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update lang.rs

* Update lang.rs

* Update be.rs

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-06-26 20:32:05 +08:00
fufesou
f17e17a6b9 fix: mobile cursor, check null (#8481)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-26 19:49:52 +08:00
rustdesk
faf363cfd2 add TelegramBot 2024-06-26 18:49:41 +08:00
fufesou
dbbd9179b7 fix: android cursor scale (#8478)
* fix: android cursor scale

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

* Min scale restriction for mobile cursor

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-26 18:42:08 +08:00
rustdesk
49f848a453 refactor 2024-06-26 16:35:29 +08:00
rustdesk
ef56aea74f fix https://github.com/rustdesk/rustdesk/issues/8479 2024-06-26 16:28:31 +08:00
rustdesk
cb5fa85ac2 build 45 2024-06-26 09:59:35 +08:00
fufesou
11bdd3cfcd fix: ios keyboard, composing input (#8471)
* fix: ios keyboard, composing input

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

* Incorrect changes

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-26 00:24:57 +08:00
fufesou
f0dcc91907 fix: wrong use of Instant sub, just after booting (#8470)
* fix: wrong use of Instant sub, just after booting

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

* fix: ThrottledInterval, first next tick

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-25 20:33:38 +08:00
21pages
c1c2d26ec7 fix, check video_queue len rather than len when refresh (#8469)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-25 18:30:32 +08:00
21pages
93133b9a6c if fps is adjusted below 30, not set fps to 30 when changing image quality (#8468)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-25 17:25:50 +08:00
fufesou
245f08055f fix: mobile, chat menu, hide after tapping (#8465)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-25 00:31:41 +08:00
fufesou
00ddd63372 fix: ios, chat window position, wrong member value (#8464)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-24 22:56:13 +08:00
21pages
1765c7bbf4 fix multi display fps control (#8455)
* Calculate fps without distinguish displays, use one fps control
  because the controlled side control fps of all displays with one FPS
  variable.
* Because all displays decode frame in one thread, when there are N
  displays, the video frames received in one second is `fps * N`, so the
  calculated decode fps should be divided by N. Because the actual
  display count is not obvious in rust, when no data frame is received for 5 seconds, the display is considered inactive, and only the active display is used as the dividend.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-24 19:41:15 +08:00
Yevhen Popok
65edd55516 Update Ukrainian translation (#8453) 2024-06-24 17:32:00 +08:00
rustdesk
4947cf8718 fix https://github.com/rustdesk/rustdesk/issues/8452 2024-06-23 22:50:54 +08:00
jxdv
65dd2b8993 update sk.rs (#8449) 2024-06-23 18:05:28 +08:00
jxdv
ef82cfa034 update cs.rs (#8450) 2024-06-23 18:05:06 +08:00
rustdesk
1a69d525af fix tile type droplist and change to build 44 2024-06-23 11:39:44 +08:00
fufesou
307827be3c fix: mobile actions hide and mobile theme (#8447)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-23 11:26:15 +08:00
fufesou
40cb59336f fix: mobile actions, position (#8446)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-23 11:06:47 +08:00
XLion
a9e0ea8520 Update tw.rs (#8444) 2024-06-23 09:09:08 +08:00
rustdesk
baeee642dd build 43 2024-06-22 20:51:41 +08:00
Mr-Update
416efe9fd3 Update de.rs (#8443) 2024-06-22 20:51:33 +08:00
solokot
8b5ac390d1 Update ru.rs (#8442) 2024-06-22 20:05:56 +08:00
rustdesk
212e8e7559 fix one missing file 2024-06-22 12:45:32 +08:00
rustdesk
41a20b50ea split web js to v1 and v2 2024-06-22 12:29:20 +08:00
21pages
3742b51d58 quality monitor, delay displays as 0 when fps is 0 (#8441)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-22 09:39:02 +08:00
bovirus
1a21dff5d4 Update Italian language (#8439) 2024-06-22 08:11:09 +08:00
fufesou
bbf7d9e08a fix: android, no voice call under android 11 (#8440)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-22 08:10:54 +08:00
21pages
ffed29e632 fix typo (#8436)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-21 23:58:00 +08:00
21pages
0f6538c1a7 add enable directx option, android software encoding half resolution option (#8435)
* add option enable directx capture screen, default true

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

* option android software encoding half scale, check isStart flag

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-21 18:54:32 +08:00
rustdesk
ff2e055a5a use flutter 3.13 for android because its video super slow on my phone 2024-06-21 18:14:58 +08:00
rustdesk
cdf97f8717 try out 3.13.9 with master 2024-06-21 17:42:58 +08:00
rustdesk
b2af79a3c5 try out 3.16.0 2024-06-21 17:05:51 +08:00
fufesou
74cc5abd09 fix: android prompt "Failed to stop voice call" on conn ended (#8434)
* fix: android prompt "Failed to stop voice call" on conn ended

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

* Remove invalid comment

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

* Better control of voice call status

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

* Better voice call status control

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

* better end conn for voice call

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-21 16:43:54 +08:00
rustdesk
32c4712d5e fix ci 2024-06-21 12:42:27 +08:00
rustdesk
3244395bfb try 3.22.2 playground 2024-06-21 12:34:00 +08:00
rustdesk
1cb0e1ce7b try out 3.22.2 2024-06-21 12:33:24 +08:00
fufesou
42394fcbdd fix: android, two finger pan, scale (#8429)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-21 09:07:32 +08:00
rustdesk
0b32e741f7 test 3.13.9 on master for android 2024-06-21 01:29:00 +08:00
rustdesk
80c5d59916 fix ci 2024-06-21 01:13:05 +08:00
rustdesk
e95823f543 for try out flutter 3.22.2 2024-06-21 01:00:46 +08:00
rustdesk
06fe972683 try out 3.22.2 for android 2024-06-21 00:45:45 +08:00
rustdesk
3057396c02 try latest 2024-06-21 00:19:05 +08:00
rustdesk
7db9543fee change back to old settings, only use flutter 3.13.9 2024-06-21 00:15:21 +08:00
rustdesk
58d86acf0d change back to 3.16.9 2024-06-20 23:51:58 +08:00
rustdesk
859020583d fix ci 2024-06-20 23:21:08 +08:00
rustdesk
0cab620ba5 try old flutter and vcpkg 2024-06-20 23:09:38 +08:00
rustdesk
4338fcc51a check if vpckg overrided 2024-06-20 23:06:42 +08:00
rustdesk
5f6f1e8d36 use prebuilt vcpkg to overwrite 2024-06-20 22:45:07 +08:00
fufesou
a91f244f35 fix: android, touch mode, one finger pan, start pos (#8427)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-20 22:21:14 +08:00
fufesou
82bf04da81 fix: android, touch mode, correct cursor input, on soft keyboard shows (#8426)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-20 22:18:57 +08:00
rustdesk
a679e4a5e3 vcpkg_root 2024-06-20 22:16:04 +08:00
rustdesk
3c79404534 use my vcpkg 2024-06-20 21:53:43 +08:00
rustdesk
ba707d1149 try out different ndk 2024-06-20 21:28:27 +08:00
rustdesk
93d88f30b4 test more commits 2024-06-20 21:10:43 +08:00
rustdesk
f5bc136b07 fix ci 2024-06-20 20:32:00 +08:00
rustdesk
ae69cbb207 fix ci 2024-06-20 20:30:55 +08:00
fufesou
39e3da1eb0 android, secure keyboard on remote input (#8425)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-20 20:20:32 +08:00
rustdesk
c1322b47c3 more commits 2024-06-20 20:13:57 +08:00
rustdesk
67f83bd5dd more commits 2024-06-20 19:48:40 +08:00
rustdesk
e424d01f3d publish missed 2024-06-20 19:41:43 +08:00
rustdesk
a424830893 fix ci 2024-06-20 19:23:17 +08:00
rustdesk
3c5810cc01 prepare android old version test 2024-06-20 19:16:51 +08:00
21pages
30bd4e1cef update hwcodec, use ms as pts like vpx (#8422)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-20 13:57:56 +08:00
RustDesk
7956953669 Revert "fix: android, touch mode, move cursor (#8419)" (#8421)
This reverts commit dcba4615a2.
2024-06-20 12:22:36 +08:00
fufesou
dcba4615a2 fix: android, touch mode, move cursor (#8419)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-20 08:29:07 +08:00
rustdesk
0bf9de8256 also check --server in loop 2024-06-19 21:29:54 +08:00
rustdesk
77f1c7e74c add crate::platform::quit_gui(); for double sure 2024-06-19 21:21:51 +08:00
rustdesk
27478946ea open new window not always work, so give it a little time before exit 2024-06-19 21:17:26 +08:00
rustdesk
1f25a8af86 fix macos stop service on gui not restart 2024-06-19 20:23:05 +08:00
rustdesk
d75caad71f move --server check into daemon.plist 2024-06-19 19:54:30 +08:00
rustdesk
adf0226641 fix ci and make macos service time check more aggressive 2024-06-19 19:10:44 +08:00
rustdesk
137f58a84a refactor macos service for delegate again, remove runme in install service of linux 2024-06-19 18:49:49 +08:00
21pages
7c45a68870 linux install service, stop service before start (#8414)
If the stop-service option before installation is "", after
installation --sever is also started up. When clicking to
start service, restart --server to make it read the config file,
otherwise the service can't be started util --server is restarted.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-19 16:29:40 +08:00
rustdesk
99edab4b61 hide docker from tao, this may fix https://github.com/rustdesk/rustdesk/issues/8399 2024-06-19 16:25:48 +08:00
fufesou
e50b72622c fix: android, touch mode, soft keyboard, no pointer events (#8409)
* fix: android, touch mode, soft keyboard, no pointer events

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

* Reset lastIsBlocked on touch mode toggled

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

* refact: reset lastIsBlocked when updating keyHelpToolsRect

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-19 15:58:23 +08:00
rustdesk
60dc40f47f try hide docker in tao delegate because hide in rustdesk side a bit late so that still seeing it sometimes
refactor service to make it restart after login to avoid delegate caught for seconds after login
also make main windows Close event restart itself for above case
2024-06-19 15:42:53 +08:00
21pages
841c331981 fix sleep duration when receive ipc close (#8410)
unit is second

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-19 14:02:13 +08:00
21pages
4eafa5a585 fix ci (#8407)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-19 10:12:10 +08:00
rustdesk
5a740e891e make main window can be reopen if killed by --server for creating ipc 2024-06-19 09:27:29 +08:00
rustdesk
1fcc7001bd use exit(-1) in Data::Close to make sure --server can restart 2024-06-18 22:42:42 +08:00
rustdesk
e57854422a fix kill main window in --server 2024-06-18 22:04:34 +08:00
21pages
8c39979848 fix get mac display scale, find screen from display id (#8401) 2024-06-18 19:37:15 +08:00
Stas Solovey
2c38648e39 update ru.rs (#8400)
* Update ru.rs

* Update ru.rs

* Update ru.rs

* Update ru.rs

* Update ru.rs

* Update ru.rs

* Update ru.rs

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-06-18 19:36:32 +08:00
rustdesk
97aa739d69 revert https://github.com/rustdesk/rustdesk/pull/8368 2024-06-18 18:21:29 +08:00
Stas Solovey
b0042f29fb Update ru.rs (#8398)
* Update ru.rs

* Update ru.rs

* Update ru.rs
2024-06-18 17:08:44 +08:00
rustdesk
e3ca82945f fix https://github.com/rustdesk/rustdesk/issues/2680 2024-06-18 16:30:56 +08:00
rustdesk
bf6a3a7067 fix stupid flutter 2024-06-18 15:06:43 +08:00
rustdesk
d25670c79a fix https://github.com/rustdesk/rustdesk/issues/2680 2024-06-18 14:39:56 +08:00
XLion
32b26e4ad3 Update translation (#8394)
* Update tw.rs

* Update cn.rs

Add spacing
2024-06-18 09:54:03 +08:00
rustdesk
818439db48 fix ci 2024-06-18 08:43:19 +08:00
21pages
e23a9da1a8 sync get option in android setting (#8393)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-18 08:29:10 +08:00
21pages
37ebac2a9e update hwcodec, remove AVCodecParserContext (#8389)
It was used to decode different resolution with same decoder, but may
cause crash.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-17 23:53:43 +08:00
Kleofass
46bf552afc Update lv.rs (#8383) 2024-06-17 17:31:38 +08:00
rustdesk
70151e3dd8 add Push Notifications capability though we do not use it explictly, but our dep used it, have to enable it, otherwise review will refuse us 2024-06-17 12:50:29 +08:00
rustdesk
e933f0baf2 build 41 2024-06-17 11:16:33 +08:00
rustdesk
f2a612c3d9 add voice_call start_pa 2024-06-17 10:57:25 +08:00
flusheDData
4a648f0068 New terms added (#8377)
* Update es.rs

New term and tip added

* Update es.rs

change representación por renderizado (render)

* Update es.rs

New terms added
2024-06-17 10:36:10 +08:00
21pages
5b52742cf7 fix mobile ab/group not update when login with 2fa/email (#8378)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-17 10:35:57 +08:00
rustdesk
237d234277 add rustc 1.78 ABI change comment 2024-06-17 09:44:36 +08:00
rustdesk
ed0cba281f start dbus only for main 2024-06-16 23:59:09 +08:00
rustdesk
2e0eaed322 call _ffiBind.mainStartPa only for --cm 2024-06-16 23:51:00 +08:00
21pages
e2a6d66805 make mobile ab dropdown button text vertical center (#8376)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-16 23:41:40 +08:00
21pages
8d6de9ca59 opt android ab ui (#8374)
* multiline error banner
* mobile remove ab permission icons due to hard to press
* center ab dropdown button text

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-16 23:13:46 +08:00
fufesou
db108d964b fix: build 09f452b055 (#8373)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-06-16 22:37:14 +08:00
solokot
f016d453fa Update ru.rs (#8370) 2024-06-16 15:14:45 +08:00
21pages
60ea8d2c2b mac scale factor of each screen (#8368)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-16 12:01:41 +08:00
jxdv
12ff1319f1 update sk.rs (#8364) 2024-06-15 16:03:56 +08:00
jxdv
f224d8872e update cs.rs (#8365) 2024-06-15 16:03:42 +08:00
rustdesk
5cf2d5f062 change back to 1.75 since sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html 2024-06-15 14:03:33 +08:00
fufesou
92dd0ee1dd fix: non texture, multi window, switch display (#8353)
* fix: non texture, multi window, switch display

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

* fix build

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-14 17:55:03 +08:00
bovirus
70c20fc76f Update Italian language (#8352) 2024-06-14 17:39:24 +08:00
fufesou
07e0b5ac10 fix: desktop, remote toolbar, remember collapse (#8349)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-14 00:28:59 +08:00
fufesou
12f7fc3d33 fix: push rgba only on desktop (#8348)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-13 23:22:03 +08:00
fufesou
60f47cb549 fix: desktop, remote toolbar autohide (#8347)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-13 21:04:00 +08:00
Mr-Update
d33fa3f073 Update de.rs (#8346) 2024-06-13 21:01:24 +08:00
21pages
2e4fafcf46 add missing call of androidUpdatekeepScreenOn (#8345)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-13 19:24:38 +08:00
21pages
ab451b9056 android keep screen on option (#8344)
* android keep screen on option

Keep screen on option relays on floating window.

Three options: Never, During controlled(default), During service is on

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

* When rustdesk is in forground, be consistent with the settings

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-13 18:30:29 +08:00
fufesou
bc875a35b0 Refact/multi window soft rendering (#8343)
* refact: multi_window_soft_rendering

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

* fix: window pos, potential wait for image

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

* comments

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

* remove debug print

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

* explicitly set rgba_data.size_got to false after init

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

* refact: multi window, merge images, render with texture

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

* revert, flutter.rs, rgba valid

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

* Add displays index before sending capture msg

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

* refact: multi window, soft rendering

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

* fix: build

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-13 18:03:41 +08:00
rustdesk
8e12a34634 upgrade our ipc-parity and tokio-socks crate to tokio 1.38 2024-06-13 13:17:34 +08:00
rustdesk
77204127f2 use latest rust for non-windows, and upgrade tokio to 3.18 which fix a mpsc channel bug 2024-06-13 13:05:35 +08:00
fufesou
65c2ccdc93 fix: try fix, macos, flutter, focus changed, no reponse (#8338)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-13 10:42:50 +08:00
fufesou
964d4f1f87 try fix cursor id, int.parse, exceeds limit (#8333)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-13 00:34:23 +08:00
21pages
f559e9c74a disable hardware encoding if encoding fails too many times (#8327)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-12 23:37:51 +08:00
21pages
610009528b hwcodec, only process that start ipc server start check process (#8325)
check process send config to ipc server, other process get config from ipc server. Process will save config to toml, and the toml will be used if the config is none.

when start check process: ipc server process start or option changed
from disable to enable

when get config: main window start or option changed from disable to
enable, start_video_audio_threads.

Only windows implements signature, which is used to mark whether the gpu software and hardware information changes. After reboot, the signature doesn't change. https://asawicki.info/news_1773_how_to_programmatically_check_graphics_driver_version, use dxgi way to get software version, it's not consistent with the visible driver version, after updating intel driver with small version change, the signature doesn't change. Linux doesn't use toml file.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-12 20:40:35 +08:00
rustdesk
0f10a88b23 remove elevation/installation requirement for --get-id 2024-06-12 20:35:04 +08:00
Kleofass
60049c8cc5 Update lv.rs (#8323) 2024-06-12 17:20:29 +08:00
rustdesk
50aa5880de always call platformFFI.nextRgba no matter what to avoid dead lock because of unknown reason 2024-06-12 02:34:15 +08:00
rustdesk
47143318ba ensure nextRgba called no matter if image created 2024-06-12 01:40:54 +08:00
fufesou
c27791a9ac comments (#8316)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-12 00:53:54 +08:00
fufesou
b19d732a3a fix: audio rechannel len (#8315)
* fix: audio rechannel len

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

* comments

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-12 00:32:10 +08:00
rustdesk
cd3db3a686 try to fix https://github.com/rustdesk/rustdesk-server-pro/issues/266 2024-06-11 19:51:33 +08:00
fufesou
35fb9f8897 fix: peer option, individual_windows, use 'N' instead of '' (#8307)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-10 23:01:55 +08:00
21pages
ec042434be use sihost.exe as fallback for run_as_user if no explorer.exe (#8305)
* There is no relevant information, but I found that each session has a unique sihost.exe, and the user name of the process is consistent with the user name of the session, and after using the task manager to kill this process, it will automatically restart. Checking sessionUserName=siHost UserName may be unnecessary, but since there is no evidence, check it anyway.
* GetFallbackUserPid is called only when explorer.exe does not exist.
* ProcessHacker shows that the tokens of explorer.exe and sihost.exe are the same, I know little about it.

Signed-off-by: 21pages <pages21@163.com>
2024-06-10 20:29:53 +08:00
rustdesk
f8041a3de5 fix merge problem of last commit 2024-06-10 19:53:02 +08:00
rustdesk
dd90096e13 remove useless stop-rendezvous-service 2024-06-10 16:12:08 +08:00
fufesou
9ab5512bfa fix: custom client, option to bool (#8303)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-10 11:01:39 +08:00
fufesou
32ab56f864 fix: custom client, options, option2bool() (#8302)
* fix: custom client, options, option2bool()

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

* format

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-10 00:11:59 +08:00
rustdesk
78d7bfac01 fix https://github.com/rustdesk/rustdesk/discussions/8031 2024-06-09 19:48:42 +08:00
21pages
57570c3ba6 is_ipc_file_exist quote Config::ipc_path (#8295)
Signed-off-by: 21pages <pages21@163.com>
2024-06-08 21:56:47 +08:00
21pages
ffac670f95 fix nt_terminate_process missing CloseHandle (#8294)
Signed-off-by: 21pages <pages21@163.com>
2024-06-08 21:15:01 +08:00
Yevhen Popok
be16f1be44 Update Ukrainian translation (#8293) 2024-06-08 20:51:06 +08:00
21pages
fd0f85d565 no explorer.exe, judge by pid retrived from cpp (#8291)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-08 16:50:35 +08:00
21pages
8de5f3f0d3 not close connection if failed to start cm due to no explorer.exe (#8290)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-08 16:11:51 +08:00
21pages
0bb537b872 fix kill occupied ipc process, find with enumerate, kill with NtTerminateProcess (#8289)
* I reproduced the issue, that process did't have title, couldn't be connected
  to and taskkill not work
* Test whether ipc is opccupied with enumerating named pipe
* With NtTerminateProcess, it was killed successfully.
* There is a way to find the exact process which occupy the ipc, I have
  not check it, it's from https://github.com/winsiderss/systeminformer

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-08 14:09:16 +08:00
Mr-Update
987da00be0 Update de.rs (#8286) 2024-06-08 09:43:50 +08:00
jxdv
e9e2214d29 update cs.rs (#8285) 2024-06-08 09:43:32 +08:00
jxdv
ac9f3317f1 update sk.rs (#8284) 2024-06-08 09:43:17 +08:00
bovirus
7da85d277e Update Italian language (#8282) 2024-06-07 20:48:19 +08:00
solokot
274244b055 Update ru.rs (#8281) 2024-06-07 15:49:52 +08:00
fufesou
8fa611daed refact: Wayland, not support multiple displays (#8280)
* refact: Wayland, not support multiple displays

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact: Wayland disable multiple for RemoteDesktop

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-07 12:59:42 +08:00
21pages
64d0fb17f7 add floating window setting (#8279)
* Set `disable-floating-window` in client ui, it shows enabled when
  option is enabled and has floating window permission.
* Remove ignore battery setting because not work on every device.
* When the phone orientation changes, make the Y coordinate change
  proportionally, when changing back, the floating window position is still the original one.
* Add custom client option `floating-window-untouchable` to make the
  click event pass through the floating window automically. Set it untouchable automically when transparency is 0.
* On my phone, floating window size 16 no works and 32 works, so keep
  the size range [32, 320]

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-07 11:04:18 +08:00
fufesou
6d1d844b14 refact: Wayland, do not show multi displays (#8277)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-07 09:54:50 +08:00
fufesou
686dd11d8e fix: peer menu, hidden by wrong check (#8275)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-07 00:20:55 +08:00
21pages
9d42ee9df8 vram avoid always fallback to gdi (#8272)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-06 22:52:31 +08:00
21pages
9562768a04 android floating window (#8268)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-05 23:11:44 +08:00
Kleofass
54b8daede4 Update lv.rs (#8262) 2024-06-05 18:44:47 +08:00
21pages
bd51afe86c fix rustPointerInput jni parameter declaration, call new_string in with_local_frame (#8266)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-05 18:09:01 +08:00
fufesou
a84b9bd2c8 fix: setMovable only on macos (#8261)
* fix: setMovable only on macos

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

* Refact and comments

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

* comments

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

* Refact comments

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-06-05 14:52:56 +08:00
rustdesk
ce1dac3b86 attempt to fix local reference table overflow, https://github.com/rustdesk/rustdesk/issues/4118 2024-06-05 00:38:54 +08:00
rustdesk
2dcd9f02cd bump to 1.2.6 2024-06-03 21:01:41 +08:00
rustdesk
416d57bec6 https://github.com/rustdesk/rustdesk/pull/7971 2024-06-03 13:40:14 +08:00
256 changed files with 12692 additions and 6599 deletions

View File

@@ -1,230 +0,0 @@
name: Flutter Nightly MacOS Arm64 Build
on:
#schedule:
# schedule build every night
# - cron: "0/6 * * * *"
workflow_dispatch:
env:
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
CARGO_NDK_VERSION: "3.1.2"
LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.19.6"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.03.25
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
VERSION: "1.2.5"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
# To make a custom build with your own servers set the below secret values
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
API_SERVER: "${{ secrets.API_SERVER }}"
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs:
build-appimage:
name: Build image ${{ matrix.job.target }}
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
job:
- {
target: x86_64-unknown-linux-gnu,
arch: x86_64,
}
- {
target: aarch64-unknown-linux-gnu,
arch: aarch64,
}
steps:
- name: Checkout source code
uses: actions/checkout@v3
- name: Rename Binary
run: |
sudo apt-get update -y
sudo apt-get install -y wget libarchive-tools
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb rustdesk-${{ env.VERSION }}.deb
- name: Patch archlinux PKGBUILD
if: matrix.job.arch == 'x86_64'
run: |
sed -i "s/x86_64/${{ matrix.job.arch }}/g" res/PKGBUILD
if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then
sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD
fi
bsdtar -zxvf rustdesk-${{ env.VERSION }}.deb
tar -xvf ./data.tar.xz
case ${{ matrix.job.arch }} in
aarch64)
mkdir -p flutter/build/linux/arm64/release/bundle
cp -rf usr/lib/rustdesk/* flutter/build/linux/arm64/release/bundle/
;;
x86_64)
mkdir -p flutter/build/linux/x64/release/bundle
cp -rf usr/lib/rustdesk/* flutter/build/linux/x64/release/bundle/
;;
esac
- name: Build archlinux package
if: matrix.job.arch == 'x86_64'
uses: rustdesk-org/arch-makepkg-action@master
with:
packages: >
llvm
clang
libva
libvdpau
rust
gstreamer
unzip
git
cmake
gcc
curl
wget
nasm
zip
make
pkg-config
clang
gtk3
xdotool
libxcb
libxfixes
alsa-lib
pipewire
python
ttf-arphic-uming
libappindicator-gtk3
pam
gst-plugins-base
gst-plugin-pipewire
scripts: |
cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f
- name: Publish archlinux package
if: matrix.job.arch == 'x86_64'
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
res/rustdesk-${{ env.VERSION }}*.zst
- name: Build appimage package
shell: bash
run: |
# set-up appimage-builder
pushd /tmp
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
chmod +x appimage-builder-x86_64.AppImage
sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
popd
# run appimage-builder
pushd appimage
sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml
- name: Publish appimage package
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
build-flatpak:
name: Build Flatpak ${{ matrix.job.target }}
runs-on: ${{ matrix.job.on }}
strategy:
fail-fast: false
matrix:
job:
- {
target: x86_64-unknown-linux-gnu,
distro: ubuntu18.04,
on: ubuntu-20.04,
arch: x86_64,
}
- {
target: aarch64-unknown-linux-gnu,
# try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub"
distro: ubuntu22.04,
on: [self-hosted, Linux, ARM64],
arch: aarch64,
}
steps:
- name: Checkout source code
uses: actions/checkout@v3
- name: Rename Binary
run: |
sudo apt-get update -y
sudo apt-get install -y wget
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb rustdesk-${{ env.VERSION }}.deb
- uses: rustdesk-org/run-on-arch-action@amd64-support
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
id: rpm
with:
arch: ${{ matrix.job.arch }}
distro: ${{ matrix.job.distro }}
githubToken: ${{ github.token }}
setup: |
ls -l "${PWD}"
dockerRunArgs: |
--volume "${PWD}:/workspace"
shell: /bin/bash
install: |
apt-get update -y
apt-get install -y \
curl \
git \
rpm \
wget
run: |
# disable git safe.directory
git config --global --add safe.directory "*"
pushd /workspace
# install
apt-get update -y
apt-get install -y \
cmake \
curl \
flatpak \
flatpak-builder \
gcc \
git \
g++ \
libgtk-3-dev \
nasm \
wget
# flatpak deps
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08
flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08
# package
pushd flatpak
git clone https://github.com/flathub/shared-modules.git --depth=1
flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak com.rustdesk.RustDesk
- name: Publish flatpak package
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak

34
.github/workflows/fdroid.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Fdroid version file generation
on:
workflow_dispatch:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-[0-9]+'
- '[0-9]+.[0-9]+.[0-9]+-[0-9]+'
jobs:
# https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.carriez.flutter_hbb.yml
# Finds latest release and transforms F-Droid version code from version as follows:
# X.Y.Z-A => X * 1e6 + Y * 1e4 + Z * 1e2 + A
update-fdroid-version-file:
name: Publish RustDesk version file for F-Droid updater
runs-on: ubuntu-latest
steps:
- name: Generate RustDesk version file
run: |
UPSTREAM_VERNAME="$(curl https://api.github.com/repos/rustdesk/rustdesk/releases/latest | jq -r .tag_name | sed 's/^v//')"
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr '.' ' ' | tr '-' ' ' | while read -r MAJOR MINOR PATCH REV; do [ -z "$MAJOR" ] && MAJOR=0; [ -z "$MINOR" ] && MINOR=0; [ -z "$PATCH" ] && PATCH=0; [ -z "$REV" ] && REV=0; echo "$(( 1000000 * $MAJOR + 10000 * $MINOR + 100 * $PATCH + $REV ))"; done)"
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
shell: bash
- name: Publish RustDesk version file
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: "fdroid-version"
files: |
./rustdesk-version.txt

View File

@@ -10,19 +10,28 @@ on:
type: string
default: "nightly"
# NOTE: F-Droid builder script 'flutter/build_fdroid.sh' reads environment
# variables from this workflow!
#
# It does NOT read build steps, however, so please fix 'flutter/build_fdroid.sh
# whenever you add changes to Android CI build action ('build-rustdesk-android')
# in this file!
env:
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
WIN_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
CARGO_NDK_VERSION: "3.1.2"
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"
# 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.03.25
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
VERSION: "1.2.5"
# vcpkg version: 2024.06.15
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
VERSION: "1.2.7"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -56,7 +65,7 @@ jobs:
job:
# - { target: i686-pc-windows-msvc , os: windows-2022 }
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64 }
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, vcpkg-triplet: x64-windows-static }
# - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 }
steps:
- name: Export GitHub Actions cache environment variables
@@ -84,7 +93,7 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
toolchain: ${{ env.WIN_RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
@@ -107,7 +116,7 @@ jobs:
- name: Install vcpkg dependencies
run: |
$VCPKG_ROOT/vcpkg install --triplet x64-windows-static --x-install-root="$VCPKG_ROOT/installed"
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
shell: bash
- name: Build rustdesk
@@ -161,6 +170,7 @@ jobs:
shell: bash
if: env.UPLOAD_ARTIFACT == 'true'
run: |
sed -i '/dpiAware/d' res/manifest.xml
pushd ./libs/portable
pip3 install -r requirements.txt
python3 ./generate.py -f ../../rustdesk/ -o . -e ../../rustdesk/rustdesk.exe
@@ -209,7 +219,7 @@ jobs:
job:
# - { target: i686-pc-windows-msvc , os: windows-2022 }
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
- { target: i686-pc-windows-msvc, os: windows-2022, arch: x86 }
- { target: i686-pc-windows-msvc, os: windows-2022, arch: x86, vcpkg-triplet: x86-windows-static }
# - { target: aarch64-pc-windows-msvc, os: windows-2022 }
steps:
- name: Export GitHub Actions cache environment variables
@@ -246,7 +256,7 @@ jobs:
- name: Install vcpkg dependencies
run: |
$VCPKG_ROOT/vcpkg install --triplet x86-windows-static --x-install-root="$VCPKG_ROOT/installed"
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
shell: bash
- name: Build rustdesk
@@ -295,14 +305,13 @@ jobs:
- name: Build self-extracted executable
shell: bash
run: |
sed -i '/dpiAware/d' res/manifest.xml
pushd ./libs/portable
pip3 install -r requirements.txt
python3 ./generate.py -f ../../Release/ -o . -e ../../Release/rustdesk.exe
popd
mkdir -p ./SignOutput
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.exe
mv ./Release ./rustdesk
tar czf rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.tar.gz rustdesk
- name: Sign rustdesk self-extracted file
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
@@ -318,7 +327,6 @@ jobs:
tag_name: ${{ env.TAG_NAME }}
files: |
./SignOutput/rustdesk-*.exe
./rustdesk-*.tar.gz
build-for-macOS-arm64-selfhost:
# use build-for-macOS instead
@@ -404,6 +412,7 @@ jobs:
arch: aarch64,
target: aarch64-apple-ios,
os: macos-13,
vcpkg-triplet: arm64-ios,
}
steps:
- name: Export GitHub Actions cache environment variables
@@ -431,7 +440,7 @@ jobs:
- name: Install vcpkg dependencies
run: |
$VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
shell: bash
- name: Install Rust toolchain
@@ -612,6 +621,14 @@ jobs:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Workaround for flutter issue
shell: bash
run: |
cd "$(dirname "$(which flutter)")"
# https://github.com/flutter/flutter/issues/133533
sed -i -e 's/_setFramesEnabledState(false);/\/\/_setFramesEnabledState(false);/g' ../packages/flutter/lib/src/scheduler/binding.dart
grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
@@ -810,7 +827,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
@@ -852,6 +869,13 @@ jobs:
prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated
key: ${{ matrix.job.target }}
- name: fix android for flutter 3.13
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
run: |
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
cd flutter/lib
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
- name: Build rustdesk lib
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
@@ -957,12 +981,16 @@ jobs:
target: x86_64-unknown-linux-gnu,
distro: ubuntu18.04,
on: ubuntu-20.04,
deb_arch: amd64,
vcpkg-triplet: x64-linux,
}
- {
arch: aarch64,
target: aarch64-unknown-linux-gnu,
distro: ubuntu18.04,
on: [self-hosted, Linux, ARM64],
deb_arch: arm64,
vcpkg-triplet: arm64-linux,
}
steps:
- name: Export GitHub Actions cache environment variables
@@ -997,6 +1025,7 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
@@ -1013,31 +1042,27 @@ jobs:
sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml
- name: Restore bridge files
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Setup vcpkg with Github Actions binary cache
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
if: env.UPLOAD_ARTIFACT == 'true'
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
run: |
case ${{ matrix.job.target }} in
aarch64-unknown-linux-gnu)
$VCPKG_ROOT/vcpkg install --triplet arm64-linux --x-install-root="$VCPKG_ROOT/installed"
;;
x86_64-unknown-linux-gnu)
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
;;
esac
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
shell: bash
- name: Restore bridge files
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
uses: actions/download-artifact@master
with:
name: bridge-artifact
@@ -1046,7 +1071,7 @@ jobs:
- uses: rustdesk-org/run-on-arch-action@amd64-support
name: Build rustdesk
id: vcpkg
if: env.UPLOAD_ARTIFACT == 'true'
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
with:
arch: ${{ matrix.job.arch }}
distro: ${{ matrix.job.distro }}
@@ -1137,7 +1162,6 @@ jobs:
aarch64)
export PATH=/opt/flutter-elinux/bin:$PATH
sed -i "s/flutter build linux --release/flutter-elinux build linux --verbose/g" ./build.py
export ARCH=arm64
sed -i "s/x64\/release/arm64\/release/g" ./build.py
;;
x86_64)
@@ -1169,6 +1193,7 @@ jobs:
# build flutter
pushd /workspace
export CARGO_INCREMENTAL=0
export DEB_ARCH=${{ matrix.job.deb_arch }}
python3 ./build.py --flutter --skip-cargo
for name in rustdesk*??.deb; do
mv "$name" "${name%%.deb}-${{ matrix.job.arch }}.deb"
@@ -1247,21 +1272,33 @@ jobs:
files: |
res/rustdesk-${{ env.VERSION }}*.zst
build-rustdesk-sciter-arm:
build-rustdesk-linux-sciter:
if: ${{ inputs.upload-artifact }}
needs: build-rustdesk-linux # not for dep, just make it run later for parallelism
runs-on: [self-hosted, Linux, ARM64]
name: build-rustdesk-sciter-arm ${{ matrix.job.target }}
runs-on: ${{ matrix.job.on }}
name: build-rustdesk-linux-sciter ${{ matrix.job.target }}
strategy:
fail-fast: false
matrix:
# use a high level qemu-user-static
job:
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
on: ubuntu-20.04,
distro: ubuntu18.04,
deb_arch: amd64,
sciter_arch: x64,
vcpkg-triplet: x64-linux,
}
- {
arch: armv7,
target: armv7-unknown-linux-gnueabihf,
deb-arch: armhf,
use-cross: true,
on: [self-hosted, Linux, ARM64],
distro: ubuntu18.04-rustdesk,
deb_arch: armhf,
sciter_arch: arm32,
vcpkg-triplet: arm-linux,
}
steps:
- name: Export GitHub Actions cache environment variables
@@ -1317,7 +1354,7 @@ jobs:
- name: Install vcpkg dependencies
run: |
cp $PWD/res/vcpkg/linux.cmake $VCPKG_ROOT/scripts/toolchains/linux.cmake
$VCPKG_ROOT/vcpkg install --triplet arm-linux --x-install-root="$VCPKG_ROOT/installed"
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
shell: bash
- uses: rustdesk-org/run-on-arch-action@amd64-support
@@ -1325,7 +1362,7 @@ jobs:
id: vcpkg
with:
arch: ${{ matrix.job.arch }}
distro: ubuntu18.04-rustdesk
distro: ${{ matrix.job.distro }}
githubToken: ${{ github.token }}
setup: |
ls -l "${PWD}"
@@ -1391,13 +1428,13 @@ jobs:
pushd /workspace
python3 ./res/inline-sciter.py
export VCPKG_ROOT=/opt/artifacts/vcpkg
export ARCH=armhf
export CARGO_INCREMENTAL=0
cargo build --features inline --release --bins --jobs 1
# package
mkdir -p ./Release
mv ./target/release/rustdesk ./Release/rustdesk
wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/arm32/libsciter-gtk.so
wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/${{ matrix.job.sciter_arch }}/libsciter-gtk.so
export DEB_ARCH=${{ matrix.job.deb_arch }}
./build.py --package ./Release
- name: Rename rustdesk
@@ -1417,6 +1454,13 @@ jobs:
files: |
rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
- name: Upload deb
uses: actions/upload-artifact@master
if: env.UPLOAD_ARTIFACT == 'true'
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
build-appimage:
name: Build appimage ${{ matrix.job.target }}
needs: [build-rustdesk-linux]
@@ -1474,8 +1518,10 @@ jobs:
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
build-flatpak:
name: Build flatpak ${{ matrix.job.target }}
needs: [build-rustdesk-linux]
name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }}
needs:
- build-rustdesk-linux
- build-rustdesk-linux-sciter
runs-on: ${{ matrix.job.on }}
if: ${{ inputs.upload-artifact }}
strategy:
@@ -1487,6 +1533,14 @@ jobs:
distro: ubuntu18.04,
on: ubuntu-20.04,
arch: x86_64,
suffix: "",
}
- {
target: x86_64-unknown-linux-gnu,
distro: ubuntu18.04,
on: ubuntu-20.04,
arch: x86_64,
suffix: "-sciter",
}
- {
target: aarch64-unknown-linux-gnu,
@@ -1494,6 +1548,7 @@ jobs:
distro: ubuntu22.04,
on: [self-hosted, Linux, ARM64],
arch: aarch64,
suffix: "",
}
steps:
- name: Checkout source code
@@ -1502,12 +1557,12 @@ jobs:
- name: Download Binary
uses: actions/download-artifact@master
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb
path: .
- name: Rename Binary
run: |
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb flatpak/rustdesk.deb
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb flatpak/rustdesk.deb
- uses: rustdesk-org/run-on-arch-action@amd64-support
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
@@ -1553,7 +1608,7 @@ jobs:
pushd flatpak
git clone https://github.com/flathub/shared-modules.git --depth=1
flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak com.rustdesk.RustDesk
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk
- name: Publish flatpak package
uses: softprops/action-gh-release@v1
@@ -1561,9 +1616,10 @@ jobs:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak
build-rustdesk-web:
if: False
name: build-rustdesk-web
runs-on: ubuntu-20.04
strategy:

View File

@@ -1,84 +0,0 @@
name: Flutter Windows History Build
on: [workflow_dispatch]
env:
LLVM_VERSION: "10.0"
FLUTTER_VERSION: "3.16.9"
TAG_NAME: "tmp"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VERSION: "1.2.5"
jobs:
build-for-history-windows:
name: ${{ matrix.job.date }}
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
ref: ${{ matrix.job.ref }}
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v1
with:
version: ${{ env.LLVM_VERSION }}
- name: Install flutter
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.job.target }}
override: true
components: rustfmt
profile: minimal # minimal component installation (ie, no documentation)
- name: Install flutter rust bridge deps
run: |
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:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
run: |
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
shell: bash
- name: Build rustdesk
run: python3 .\build.py --portable --hwcodec --flutter
- name: Build self-extracted executable
shell: bash
run: |
pushd ./libs/portable
python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe
popd
mkdir -p ./SignOutput
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ matrix.job.date }}-${{ matrix.job.target }}.exe
- name: Publish Release
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
./SignOutput/rustdesk-*.exe

416
.github/workflows/playground.yml vendored Normal file
View File

@@ -0,0 +1,416 @@
name: playground
on:
#schedule:
# schedule build every night
# - cron: "0/6 * * * *"
workflow_dispatch:
env:
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
CARGO_NDK_VERSION: "3.1.2"
LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.22.2"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.06.15
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
VERSION: "1.2.7"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
# To make a custom build with your own servers set the below secret values
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
API_SERVER: "${{ secrets.API_SERVER }}"
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs:
build-for-macOS:
name: ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.13.9",
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
bridge: "1.80.1",
date: "20231219"
}
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.10.6",
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
bridge: "1.80.1",
date: "20231219"
}
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.10.6",
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
bridge: "1.80.1",
date: "20231119"
}
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.13.9",
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
bridge: "1.80.1",
date: "20231119"
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
keychain: rustdesk
- name: Check sign and import sign key
if: env.MACOS_P12_BASE64 != null
run: |
security default-keychain -s rustdesk.keychain
security find-identity -v
- name: Import notarize key
if: env.MACOS_P12_BASE64 != null
uses: timheuer/base64-to-file@v1.2
with:
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
fileName: rustdesk.json
fileDir: ${{ github.workspace }}
encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }}
- name: Install rcodesign tool
if: env.MACOS_P12_BASE64 != null
shell: bash
run: |
pushd /tmp
wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz
tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz
mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin
popd
- name: Install build runtime
run: |
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ matrix.job.flutter }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.job.os }}
- name: Install flutter rust bridge deps
shell: bash
run: |
sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml;
cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid"
# below works for mac to make buildable on 3.13.9
# pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd;
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: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
run: |
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
- name: Restore from cache and install vcpkg
uses: lukka/run-vcpkg@v7
if: false
with:
setupOnly: true
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
if: false
run: |
$VCPKG_ROOT/vcpkg install libvpx libyuv opus aom
- name: Show version information (Rust, cargo, Clang)
shell: bash
run: |
clang --version || true
rustup -V
rustup toolchain list
rustup default
cargo -V
rustc -V
- name: Build rustdesk
run: |
./build.py --flutter ${{ matrix.job.extra-build-args }}
- name: create unsigned dmg
run: |
CREATE_DMG="$(command -v create-dmg)"
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
- name: Codesign app and create signed dmg
if: env.MACOS_P12_BASE64 != null
run: |
# Patch create-dmg to give more attempts to unmount image
CREATE_DMG="$(command -v create-dmg)"
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
# Unlock keychain
security default-keychain -s rustdesk.keychain
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
# start sign the rustdesk.app and dmg
rm -rf *.dmg || true
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
# notarize the rustdesk-${{ env.VERSION }}.dmg
rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg
- name: Rename rustdesk
run: |
for name in rustdesk*??.dmg; do
mv "$name" "${name%%.dmg}-${{ matrix.job.arch }}-flutter${{ matrix.job.flutter }}-flutter${{ matrix.job.date }}.dmg"
done
- name: Publish DMG package
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
rustdesk*-${{ matrix.job.arch }}*.dmg
build-rustdesk-android:
if: false
name: build rustdesk android apk ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- {
arch: aarch64,
target: aarch64-linux-android,
os: ubuntu-20.04,
openssl-arch: android-arm64,
ref: master, # latest
}
steps:
- name: Checkout source code
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
clang \
cmake \
curl \
gcc-multilib \
git \
g++ \
g++-multilib \
libappindicator3-dev \
libasound2-dev \
libc6-dev \
libclang-10-dev \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libvdpau-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libxdo-dev \
libxfixes-dev \
llvm-10-dev \
nasm \
yasm \
ninja-build \
openjdk-11-jdk-headless \
pkg-config \
tree \
wget
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
components: "rustfmt"
- 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"
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
pushd flutter/lib; find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'; popd;
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
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: ${{ env.NDK_VERSION }}
add-to-path: true
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
run: |
case ${{ matrix.job.target }} in
aarch64-linux-android)
./flutter/build_android_deps.sh arm64-v8a
;;
armv7-linux-androideabi)
./flutter/build_android_deps.sh armeabi-v7a
;;
esac
shell: bash
- name: Clone deps
shell: bash
run: |
pushd /opt
git clone https://github.com/rustdesk-org/rustdesk_thirdparty_lib.git --depth=1
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
# cp -rf /opt/rustdesk_thirdparty_lib/vcpkg/* /opt/artifacts/vcpkg/
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
- name: Build rustdesk lib
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
rustup target add ${{ matrix.job.target }}
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }}
case ${{ matrix.job.target }} in
aarch64-linux-android)
./flutter/ndk_arm64.sh
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
;;
armv7-linux-androideabi)
./flutter/ndk_arm.sh
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
;;
esac
- name: Build rustdesk
shell: bash
env:
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
run: |
export PATH=/usr/lib/jvm/java-11-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
aarch64-linux-android)
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
# build flutter
pushd flutter
flutter build apk --release --target-platform android-arm64 --split-per-abi
mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
;;
armv7-linux-androideabi)
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
# build flutter
pushd flutter
flutter build apk --release --target-platform android-arm --split-per-abi
mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
;;
esac
popd
mkdir -p signed-apk; pushd signed-apk
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk ./rustdesk-test-${{ matrix.job.ref }}-${{ matrix.job.ndk }}.apk
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
id: sign-rustdesk
with:
releaseDirectory: ./signed-apk
signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
alias: ${{ secrets.ANDROID_ALIAS }}
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"
- name: Publish signed apk package
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
${{steps.sign-rustdesk.outputs.signedReleaseFile}}

View File

@@ -4,9 +4,9 @@ on:
types: [released]
jobs:
publish:
runs-on: windows-latest # action can only be run on windows
runs-on: ubuntu-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@v1
- uses: vedantmgoyal9/winget-releaser@main
with:
identifier: RustDesk.RustDesk
version: ${{ github.event.release.tag_name }}

2965
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk"
version = "1.2.5"
version = "1.2.7"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"
@@ -66,7 +66,7 @@ default-net = "0.14"
wol-rs = "1.0"
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
errno = "0.3"
rdev = { git = "https://github.com/fufesou/rdev" }
rdev = { git = "https://github.com/rustdesk-org/rdev" }
url = { version = "2.3", features = ["serde"] }
crossbeam-queue = "0.3"
hex = "0.4"
@@ -89,7 +89,10 @@ sys-locale = "0.3"
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
clipboard = { path = "libs/clipboard" }
ctrlc = "3.2"
arboard = { git = "https://github.com/fufesou/arboard", branch = "feat/x11_set_conn_timeout", features = ["wayland-data-control"] }
# arboard = { version = "3.4.0", features = ["wayland-data-control"] }
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control", "image-data"] }
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
system_shutdown = "4.0"
qrcode-generator = "4.1"
@@ -149,11 +152,11 @@ psimple = { package = "libpulse-simple-binding", version = "2.27" }
pulse = { package = "libpulse-binding", version = "2.27" }
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
async-process = "1.7"
mouce = { git="https://github.com/fufesou/mouce.git" }
evdev = { git="https://github.com/fufesou/evdev" }
mouce = { git="https://github.com/rustdesk-org/mouce.git" }
evdev = { git="https://github.com/rustdesk-org/evdev" }
dbus = "0.9"
dbus-crossroads = "0.5"
pam = { git="https://github.com/fufesou/pam" }
pam = { git="https://github.com/rustdesk-org/pam" }
users = { version = "0.11" }
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}

View File

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

View File

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

View File

@@ -25,8 +25,8 @@ flutter_build_dir_2 = f'flutter/{flutter_build_dir}'
skip_cargo = False
def get_arch() -> str:
custom_arch = os.environ.get("ARCH")
def get_deb_arch() -> str:
custom_arch = os.environ.get("DEB_ARCH")
if custom_arch is None:
return "amd64"
return custom_arch
@@ -48,15 +48,7 @@ def get_version():
def parse_rc_features(feature):
available_features = {
'PrivacyMode': {
'platform': ['windows'],
'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3'
'/TempTopMostWindow_x64.zip',
'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3/checksum_md5',
'include': ['WindowInjection.dll'],
}
}
available_features = {}
apply_features = {}
if not feature:
feature = []
@@ -81,7 +73,6 @@ def parse_rc_features(feature):
elif isinstance(feature, list):
if windows:
# download third party is deprecated, we use github ci instead.
# force add PrivacyMode
# feature.append('PrivacyMode')
pass
for feat in feature:
@@ -108,7 +99,7 @@ def make_parser():
nargs='+',
default='',
help='Integrate features, windows only.'
'Available: PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
'Available: [Not used for now]. Special value is "ALL" and empty "". Default is empty.')
parser.add_argument('--flutter', action='store_true',
help='Build flutter package', default=False)
parser.add_argument(
@@ -294,7 +285,7 @@ Homepage: https://rustdesk.com
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire
Description: A remote control software.
""" % (version, get_arch())
""" % (version, get_deb_arch())
file = open(control_file_path, "w")
file.write(content)
file.close()

View File

@@ -1,60 +1,73 @@
<p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<img src="../res/logo-header.svg" alt="RustDesk - あなたのためのリモートデスクトップ"><br>
<a href="#free-public-servers">Servers</a> •
<a href="#raw-steps-to-build">Build</a> •
<a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Structure</a> •
<a href="#snapshot">Snapshot</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
<b>このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。</b>
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
<b>READMEや<a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>、 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a>の翻訳者を歓迎します!</b>
</p>
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
私たちと話す: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます](https://github.com/rustdesk/rustdesk-server-demo)。
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分でサーバーをセットアップする](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを作成する](https://github.com/rustdesk/rustdesk-server-demo)こともできます
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDeskは誰からの貢献歓迎します。 貢献するには [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) を参照してください。
RustDeskは皆さんの貢献歓迎します。
貢献の方法については[CONTRIBUTING.md](docs/CONTRIBUTING.md)をご確認ください。
[**RustDeskはどの様に動くのか?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
[**よくある質問**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
[**パッケージのダウンロード**](https://github.com/rustdesk/rustdesk/releases)
[**ナイトリービルド**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="F-Droidで入手する"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## 依存関係
デスクトップ版ではGUIに [sciter](https://sciter.com/) が使われています。 sciter dynamic library をダウンロードしてください。
デスクトップ版ではGUIにFlutterまたはSciter(非推奨)を使用しますが、チュートリアルでは分かりやすく、簡単なSciterのみを対象に解説しています。Flutterでのビルド方法については[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)をご覧ください。
Sciter dynamic libraryを事前にダウンロードしてください。
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
モバイル版はFlutterを利用します。デスクトップ版もSciterからFlutterへマイグレーション予定です。
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## ビルド手順
- Rust開発環境とC ++ビルド環境を準備します
- Rust開発環境とC++ビルド環境を準備します
- [vcpkg](https://github.com/microsoft/vcpkg), をインストールし、 `VCPKG_ROOT` 環境変数を正しく設定します。
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
- run `cargo run`
- [vcpkg](https://github.com/microsoft/vcpkg)をインストールし、環境変数に`VCPKG_ROOT`設定します。
その後、以下のコマンドを実行します。
- Windowsの場合: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/macOSの場合: vcpkg install libvpx libyuv opus aom
- `cargo run`を実行します。
## [ビルド](https://rustdesk.com/docs/en/dev/build/)
## Linuxでのビルド手順
## Linuxでのビルド方法
### Ubuntu 18 (Debian 10)
```sh
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
```
### Fedora 28 (CentOS 8)
@@ -69,7 +82,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
```
### Install vcpkg
### vcpkgのインストール
```sh
git clone https://github.com/microsoft/vcpkg
@@ -81,7 +94,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom
```
### Fix libvpx (For Fedora)
### libvpxの修正 (Fedoraのみ)
```sh
cd vcpkg/buildtrees/libvpx/src
@@ -107,9 +120,9 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run
```
## Dockerでビルドする方法
## Dockerでビルド方法
リポジトリクローンを作成し、Dockerコンテナを構築することから始めます
リポジトリクローンし、Dockerコンテナを構築ます:
```sh
git clone https://github.com/rustdesk/rustdesk
@@ -117,44 +130,50 @@ cd rustdesk
docker build -t "rustdesk-builder" .
```
その後、アプリケーションをビルドする必要があるたびに、以下のコマンドを実行します
以下のコマンドを実行します:
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
```
このコマンドはRustDeskをビルドする度に実行する必要があります。
なお、最初のビルドでは、依存関係がキャッシュされるまで時間がかかることがありますが、その後のビルドではより速くなります。さらに、ビルドコマンドに別の引数を指定する必要がある場合は、コマンドの最後にある `<OPTIONAL-ARGS>` の位置で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に
`--release` を実行します。できあがった実行ファイルはシステムのターゲットフォルダに格納され、のコマンドで実行できます。
初回ビルドは時間がかかるかもしれませんが、2回目以降は依存関係がキャッシュされるため、ビルドにかかる時間が短くなります。
ビルドコマンドに追加の引数を指定する必要がある場合は、コマンドの最後(`<OPTIONAL-ARGS>`の位置)で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に `--release`追記し実行します。ビルドされた実行ファイルはあなたのシステムのターゲットフォルダに保存され、下記のコマンドで実行することができます。
デバッグビルドを起動する場合:
```sh
target/debug/rustdesk
```
あるいは、リリース用の実行ファイルを実行している場合:
リリースビルドを起動する場合:
```sh
target/release/rustdesk
```
これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install``run` などの他の cargo サブコマンドは、ホストではなくコンテナ内プログラムをインストールまたは実行するため、現在の方法ではサポートされていないことに注意してください
コマンドをRustDeskリポジトリのルートから実行していることを確認してください。また、`install``run` などの他のcargoサブコマンドは、ホストではなくコンテナ内プログラムをインストール実行するため、現在の方法ではサポートされていません
## ファイル構造
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数その他のユーティリティ関数
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、設定、tcp/udpラッパー、protobuf、ファイル転送に利用されるfs関数その他のユーティリティ関数
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウスコントロール
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: オーディオ/クリップボード/入力/ビデオサービス、ネットワーク接続
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウス操作
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS向けのファイルのコピーと貼り付けの実装
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 廃止された Sciter UI (非推奨)
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**:
オーディオ/クリップボード/入力/ビデオ サービスとネットワーク接続
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: ピア接続の開始
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)と通信し、リモートの直接接続(TCPホールパンチング)や中継接続を担う
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアント向けのJavaScript
## スナップショット
## スクリーンショット
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)
![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png)
![Connected to a Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png)
![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)
![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)

View File

@@ -106,5 +106,6 @@ 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 'com.caverock:androidsvg-aar:1.4'
}

View File

@@ -81,6 +81,11 @@
android:name=".MainService"
android:enabled="true"
android:foregroundServiceType="mediaProjection" />
<service
android:name=".FloatingWindowService"
android:enabled="true" />
<!--
Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java

View File

@@ -116,24 +116,20 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
}
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (!isSupportVoiceCall()) {
return false
}
if (isVideoStart() || isAudioStart()) {
if (!switchToVoiceCall(mediaProjection)) {
return false
}
} else {
if (!switchToVoiceCall(mediaProjection)) {
return false
}
// No need to check if video or audio is started here.
if (!switchToVoiceCall(mediaProjection)) {
return false
}
return true
}
fun onVoiceCallClosed(mediaProjection: MediaProjection?): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return false
// Return true if not supported, because is was not started.
if (!isSupportVoiceCall()) {
return true
}
if (isVideoStart()) {
switchOutVoiceCall(mediaProjection)
@@ -180,9 +176,6 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
}
fun tryReleaseAudio() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return
}
if (isAudioStart() || isVideoStart()) {
return
}

View File

@@ -0,0 +1,378 @@
package com.carriez.flutter_hbb
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.PixelFormat
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
import android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
import android.widget.ImageView
import android.widget.PopupMenu
import com.caverock.androidsvg.SVG
import ffi.FFI
import kotlin.math.abs
class FloatingWindowService : Service(), View.OnTouchListener {
private lateinit var windowManager: WindowManager
private lateinit var layoutParams: WindowManager.LayoutParams
private lateinit var floatingView: ImageView
private lateinit var originalDrawable: Drawable
private lateinit var leftHalfDrawable: Drawable
private lateinit var rightHalfDrawable: Drawable
private var dragging = false
private var lastDownX = 0f
private var lastDownY = 0f
private var viewCreated = false;
private var keepScreenOn = KeepScreenOn.DURING_CONTROLLED
companion object {
private val logTag = "floatingService"
private var firstCreate = true
private var viewWidth = 120
private var viewHeight = 120
private const val MIN_VIEW_SIZE = 32 // size 0 does not help prevent the service from being killed
private const val MAX_VIEW_SIZE = 320
private var viewUntouchable = false
private var viewTransparency = 1f // 0 means invisible but can help prevent the service from being killed
private var customSvg = ""
private var lastLayoutX = 0
private var lastLayoutY = 0
private var lastOrientation = Configuration.ORIENTATION_UNDEFINED
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
try {
if (firstCreate) {
firstCreate = false
onFirstCreate(windowManager)
}
Log.d(logTag, "floating window size: $viewWidth x $viewHeight, transparency: $viewTransparency, lastLayoutX: $lastLayoutX, lastLayoutY: $lastLayoutY, customSvg: $customSvg")
createView(windowManager)
handler.postDelayed(runnable, 1000)
Log.d(logTag, "onCreate success")
} catch (e: Exception) {
Log.d(logTag, "onCreate failed: $e")
}
}
override fun onDestroy() {
super.onDestroy()
if (viewCreated) {
windowManager.removeView(floatingView)
}
handler.removeCallbacks(runnable)
}
@SuppressLint("ClickableViewAccessibility")
private fun createView(windowManager: WindowManager) {
floatingView = ImageView(this)
viewCreated = true
originalDrawable = resources.getDrawable(R.drawable.floating_window, null)
if (customSvg.isNotEmpty()) {
try {
val svg = SVG.getFromString(customSvg)
Log.d(logTag, "custom svg info: ${svg.documentWidth} x ${svg.documentHeight}");
// This make the svg render clear
svg.documentWidth = viewWidth * 1f
svg.documentHeight = viewHeight * 1f
originalDrawable = svg.renderToPicture().let {
BitmapDrawable(
resources,
Bitmap.createBitmap(it.width, it.height, Bitmap.Config.ARGB_8888)
.also { bitmap ->
it.draw(Canvas(bitmap))
})
}
floatingView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Log.d(logTag, "custom svg loaded")
} catch (e: Exception) {
e.printStackTrace()
}
}
val originalBitmap = Bitmap.createBitmap(
originalDrawable.intrinsicWidth,
originalDrawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(originalBitmap)
originalDrawable.setBounds(
0,
0,
originalDrawable.intrinsicWidth,
originalDrawable.intrinsicHeight
)
originalDrawable.draw(canvas)
val leftHalfBitmap = Bitmap.createBitmap(
originalBitmap,
0,
0,
originalDrawable.intrinsicWidth / 2,
originalDrawable.intrinsicHeight
)
val rightHalfBitmap = Bitmap.createBitmap(
originalBitmap,
originalDrawable.intrinsicWidth / 2,
0,
originalDrawable.intrinsicWidth / 2,
originalDrawable.intrinsicHeight
)
leftHalfDrawable = BitmapDrawable(resources, leftHalfBitmap)
rightHalfDrawable = BitmapDrawable(resources, rightHalfBitmap)
floatingView.setImageDrawable(rightHalfDrawable)
floatingView.setOnTouchListener(this)
floatingView.alpha = viewTransparency * 1f
var flags = FLAG_LAYOUT_IN_SCREEN or FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE
if (viewUntouchable || viewTransparency == 0f) {
flags = flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
}
layoutParams = WindowManager.LayoutParams(
viewWidth / 2,
viewHeight,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE,
flags,
PixelFormat.TRANSLUCENT
)
layoutParams.gravity = Gravity.TOP or Gravity.START
layoutParams.x = lastLayoutX
layoutParams.y = lastLayoutY
val keepScreenOnOption = FFI.getLocalOption("keep-screen-on").lowercase()
keepScreenOn = when (keepScreenOnOption) {
"never" -> KeepScreenOn.NEVER
"service-on" -> KeepScreenOn.SERVICE_ON
else -> KeepScreenOn.DURING_CONTROLLED
}
Log.d(logTag, "keepScreenOn option: $keepScreenOnOption, value: $keepScreenOn")
updateKeepScreenOnLayoutParams()
windowManager.addView(floatingView, layoutParams)
moveToScreenSide()
}
private fun onFirstCreate(windowManager: WindowManager) {
val wh = getScreenSize(windowManager)
val w = wh.first
val h = wh.second
// size
FFI.getLocalOption("floating-window-size").let {
if (it.isNotEmpty()) {
try {
val size = it.toInt()
if (size in MIN_VIEW_SIZE..MAX_VIEW_SIZE && size <= w / 2 && size <= h / 2) {
viewWidth = size
viewHeight = size
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// untouchable
viewUntouchable = FFI.getLocalOption("floating-window-untouchable") == "Y"
// transparency
FFI.getLocalOption("floating-window-transparency").let {
if (it.isNotEmpty()) {
try {
val transparency = it.toInt()
if (transparency in 0..10) {
viewTransparency = transparency * 1f / 10
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// custom svg
FFI.getLocalOption("floating-window-svg").let {
if (it.isNotEmpty()) {
customSvg = it
}
}
// position
lastLayoutX = 0
lastLayoutY = (wh.second - viewHeight) / 2
lastOrientation = resources.configuration.orientation
}
private fun performClick() {
showPopupMenu()
}
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
dragging = false
lastDownX = event.rawX
lastDownY = event.rawY
}
MotionEvent.ACTION_UP -> {
val clickDragTolerance = 10f
if (abs(event.rawX - lastDownX) < clickDragTolerance && abs(event.rawY - lastDownY) < clickDragTolerance) {
performClick()
} else {
moveToScreenSide()
}
}
MotionEvent.ACTION_MOVE -> {
val dx = event.rawX - lastDownX
val dy = event.rawY - lastDownY
// ignore too small fist start moving(some time is click)
if (!dragging && dx*dx+dy*dy < 25) {
return false
}
dragging = true
layoutParams.x = event.rawX.toInt()
layoutParams.y = event.rawY.toInt()
layoutParams.width = viewWidth
floatingView.setImageDrawable(originalDrawable)
windowManager.updateViewLayout(view, layoutParams)
lastLayoutX = layoutParams.x
lastLayoutY = layoutParams.y
}
}
return false
}
private fun moveToScreenSide(center: Boolean = false) {
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
val wh = getScreenSize(windowManager)
val w = wh.first
if (layoutParams.x < w / 2) {
layoutParams.x = 0
floatingView.setImageDrawable(rightHalfDrawable)
} else {
layoutParams.x = w - viewWidth / 2
floatingView.setImageDrawable(leftHalfDrawable)
}
if (center) {
layoutParams.y = (wh.second - viewHeight) / 2
}
layoutParams.width = viewWidth / 2
windowManager.updateViewLayout(floatingView, layoutParams)
lastLayoutX = layoutParams.x
lastLayoutY = layoutParams.y
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (newConfig.orientation != lastOrientation) {
lastOrientation = newConfig.orientation
val wh = getScreenSize(windowManager)
Log.d(logTag, "orientation: $lastOrientation, screen size: ${wh.first} x ${wh.second}")
val newW = wh.first
val newH = wh.second
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE || newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
// Proportional change
layoutParams.x = (layoutParams.x.toFloat() / newH.toFloat() * newW.toFloat()).toInt()
layoutParams.y = (layoutParams.y.toFloat() / newW.toFloat() * newH.toFloat()).toInt()
}
moveToScreenSide()
}
}
private fun showPopupMenu() {
val popupMenu = PopupMenu(this, floatingView)
val idShowRustDesk = 0
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
val idStopService = 1
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
idShowRustDesk -> {
openMainActivity()
true
}
idStopService -> {
stopMainService()
true
}
else -> false
}
}
popupMenu.setOnDismissListener {
moveToScreenSide()
}
popupMenu.show()
}
private fun openMainActivity() {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT
)
try {
pendingIntent.send()
} catch (e: PendingIntent.CanceledException) {
e.printStackTrace()
}
}
private fun stopMainService() {
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null)
}
enum class KeepScreenOn {
NEVER,
DURING_CONTROLLED,
SERVICE_ON,
}
private val handler = Handler(Looper.getMainLooper())
private val runnable = object : Runnable {
override fun run() {
if (updateKeepScreenOnLayoutParams()) {
windowManager.updateViewLayout(floatingView, layoutParams)
}
handler.postDelayed(this, 1000) // 1000 milliseconds = 1 second
}
}
private fun updateKeepScreenOnLayoutParams(): Boolean {
val oldOn = layoutParams.flags and FLAG_KEEP_SCREEN_ON != 0
val newOn = keepScreenOn == KeepScreenOn.SERVICE_ON || (keepScreenOn == KeepScreenOn.DURING_CONTROLLED && MainService.isStart)
if (oldOn != newOn) {
Log.d(logTag, "change keep screen on to $newOn")
if (newOn) {
layoutParams.flags = layoutParams.flags or FLAG_KEEP_SCREEN_ON
} else {
layoutParams.flags = layoutParams.flags and FLAG_KEEP_SCREEN_ON.inv()
}
return true
}
return false
}
}

View File

@@ -18,7 +18,9 @@ import android.widget.EditText
import android.view.accessibility.AccessibilityEvent
import android.view.ViewGroup.LayoutParams
import android.view.accessibility.AccessibilityNodeInfo
import android.view.KeyEvent as KeyEventAndroid
import android.graphics.Rect
import android.media.AudioManager
import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
@@ -75,6 +77,8 @@ class InputService : AccessibilityService() {
private var fakeEditTextForTextStateCalculation: EditText? = null
private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) }
@RequiresApi(Build.VERSION_CODES.N)
fun onMouseInput(mask: Int, _x: Int, _y: Int) {
val x = max(0, _x)
@@ -294,6 +298,18 @@ class InputService : AccessibilityService() {
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
var ke: KeyEventAndroid? = null
if (Build.VERSION.SDK_INT < 33 || textToCommit == null) {
ke = KeyEventConverter.toAndroidKeyEvent(keyEvent)
}
ke?.let { event ->
if (tryHandleVolumeKeyEvent(event)) {
return
} else if (tryHandlePowerKeyEvent(event)) {
return
}
}
if (Build.VERSION.SDK_INT >= 33) {
getInputMethod()?.let { inputMethod ->
inputMethod.getCurrentInputConnection()?.let { inputConnection ->
@@ -302,7 +318,7 @@ class InputService : AccessibilityService() {
inputConnection.commitText(text, 1, null)
}
} else {
KeyEventConverter.toAndroidKeyEvent(keyEvent).let { event ->
ke?.let { event ->
inputConnection.sendKeyEvent(event)
}
}
@@ -311,7 +327,7 @@ class InputService : AccessibilityService() {
} else {
val handler = Handler(Looper.getMainLooper())
handler.post {
KeyEventConverter.toAndroidKeyEvent(keyEvent)?.let { event ->
ke?.let { event ->
val possibleNodes = possibleAccessibiltyNodes()
Log.d(logTag, "possibleNodes:$possibleNodes")
for (item in possibleNodes) {
@@ -325,6 +341,43 @@ class InputService : AccessibilityService() {
}
}
private fun tryHandleVolumeKeyEvent(event: KeyEventAndroid): Boolean {
when (event.keyCode) {
KeyEventAndroid.KEYCODE_VOLUME_UP -> {
if (event.action == KeyEventAndroid.ACTION_DOWN) {
volumeController.raiseVolume(null, true, AudioManager.STREAM_SYSTEM)
}
return true
}
KeyEventAndroid.KEYCODE_VOLUME_DOWN -> {
if (event.action == KeyEventAndroid.ACTION_DOWN) {
volumeController.lowerVolume(null, true, AudioManager.STREAM_SYSTEM)
}
return true
}
KeyEventAndroid.KEYCODE_VOLUME_MUTE -> {
if (event.action == KeyEventAndroid.ACTION_DOWN) {
volumeController.toggleMute(true, AudioManager.STREAM_SYSTEM)
}
return true
}
else -> {
return false
}
}
}
private fun tryHandlePowerKeyEvent(event: KeyEventAndroid): Boolean {
if (event.keyCode == KeyEventAndroid.KEYCODE_POWER) {
// Perform power dialog action when action is up
if (event.action == KeyEventAndroid.ACTION_UP) {
performGlobalAction(GLOBAL_ACTION_POWER_DIALOG);
}
return true
}
return false
}
private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) {
if (node == null) {
return
@@ -422,7 +475,7 @@ class InputService : AccessibilityService() {
return linkedList
}
private fun trySendKeyEvent(event: android.view.KeyEvent, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
private fun trySendKeyEvent(event: KeyEventAndroid, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
node.refresh()
this.fakeEditTextForTextStateCalculation?.setSelection(0,0)
this.fakeEditTextForTextStateCalculation?.setText(null)
@@ -487,10 +540,10 @@ class InputService : AccessibilityService() {
it.layout(rect.left, rect.top, rect.right, rect.bottom)
it.onPreDraw()
if (event.action == android.view.KeyEvent.ACTION_DOWN) {
if (event.action == KeyEventAndroid.ACTION_DOWN) {
val succ = it.onKeyDown(event.getKeyCode(), event)
Log.d(logTag, "onKeyDown $succ")
} else if (event.action == android.view.KeyEvent.ACTION_UP) {
} else if (event.action == KeyEventAndroid.ACTION_UP) {
val success = it.onKeyUp(event.getKeyCode(), event)
Log.d(logTag, "keyup $success")
} else {}

View File

@@ -37,6 +37,8 @@ object KeyEventConverter {
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)
}
@@ -112,6 +114,10 @@ object KeyEventConverter {
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
ControlKey.VolumeMute -> KeyEvent.KEYCODE_VOLUME_MUTE
ControlKey.VolumeUp -> KeyEvent.KEYCODE_VOLUME_UP
ControlKey.VolumeDown -> KeyEvent.KEYCODE_VOLUME_DOWN
ControlKey.Power -> KeyEvent.KEYCODE_POWER
else -> 0 // Default to unknown.
}
}

View File

@@ -233,6 +233,17 @@ class MainActivity : FlutterActivity() {
result.success(false)
}
}
GET_VALUE -> {
if (call.arguments is String) {
if (call.arguments == KEY_IS_SUPPORT_VOICE_CALL) {
result.success(isSupportVoiceCall())
} else {
result.error("-1", "No such key", null)
}
} else {
result.success(null)
}
}
"on_voice_call_started" -> {
onVoiceCallStarted()
}
@@ -252,19 +263,9 @@ class MainActivity : FlutterActivity() {
val codecArray = JSONArray()
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
var w = 0
var h = 0
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val m = windowManager.maximumWindowMetrics
w = m.bounds.width()
h = m.bounds.height()
} else {
val dm = DisplayMetrics()
windowManager.defaultDisplay.getRealMetrics(dm)
w = dm.widthPixels
h = dm.heightPixels
}
val wh = getScreenSize(windowManager)
var w = wh.first
var h = wh.second
val align = 64
w = (w + align - 1) / align * align
h = (h + align - 1) / align * align
@@ -374,4 +375,17 @@ class MainActivity : FlutterActivity() {
Log.d(logTag, "onVoiceCallClosed success")
}
}
override fun onStop() {
super.onStop()
val disableFloatingWindow = FFI.getLocalOption("disable-floating-window") == "Y"
if (!disableFloatingWindow && MainService.isReady) {
startService(Intent(this, FloatingWindowService::class.java))
}
}
override fun onStart() {
super.onStart()
stopService(Intent(this, FloatingWindowService::class.java))
}
}

View File

@@ -64,9 +64,9 @@ class MainService : Service() {
@Keep
@RequiresApi(Build.VERSION_CODES.N)
fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) {
// turn on screen with LIFT_DOWN when screen off
if (!powerManager.isInteractive && (kind == "touch" || mask == LIFT_DOWN)) {
if (!powerManager.isInteractive && (kind == 0 || mask == LIFT_DOWN)) {
if (wakeLock.isHeld) {
Log.d(logTag, "Turn on Screen, WakeLock release")
wakeLock.release()
@@ -75,10 +75,10 @@ class MainService : Service() {
wakeLock.acquire(5000)
} else {
when (kind) {
"touch" -> {
0 -> { // touch
InputService.ctx?.onTouchInput(mask, x, y)
}
"mouse" -> {
1 -> { // mouse
InputService.ctx?.onMouseInput(mask, x, y)
}
else -> {
@@ -103,6 +103,9 @@ class MainService : Service() {
put("scale",SCREEN_INFO.scale)
}.toString()
}
"is_start" -> {
isStart.toString()
}
else -> ""
}
}
@@ -172,10 +175,10 @@ class MainService : Service() {
Log.d(logTag, "from rust:stop_capture")
stopCapture()
}
"is_hardware_codec" -> {
val isHwCodec = arg1.toBoolean()
if (isHardwareCodec != isHwCodec) {
isHardwareCodec = isHwCodec
"half_scale" -> {
val halfScale = arg1.toBoolean()
if (isHalfScale != halfScale) {
isHalfScale = halfScale
updateScreenInfo(resources.configuration.orientation)
}
@@ -191,11 +194,6 @@ class MainService : Service() {
private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager }
private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")}
private fun translate(input: String): String {
Log.d(logTag, "translate:$LOCAL_NAME")
return FFI.translateLocale(LOCAL_NAME, input)
}
companion object {
private var _isReady = false // media permission ready status
private var _isStart = false // screen capture start status
@@ -252,10 +250,11 @@ class MainService : Service() {
override fun onDestroy() {
checkMediaPermission()
stopService(Intent(this, FloatingWindowService::class.java))
super.onDestroy()
}
private var isHardwareCodec: Boolean? = null;
private var isHalfScale: Boolean? = null;
private fun updateScreenInfo(orientation: Int) {
var w: Int
var h: Int
@@ -288,7 +287,7 @@ class MainService : Service() {
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
var scale = 1
if (w != 0 && h != 0) {
if (isHardwareCodec == false && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) {
if (isHalfScale == true && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) {
scale = 2
w /= scale
h /= scale
@@ -486,6 +485,7 @@ class MainService : Service() {
mediaProjection = null
checkMediaPermission()
stopForeground(true)
stopService(Intent(this, FloatingWindowService::class.java))
stopSelf()
}

View File

@@ -0,0 +1,78 @@
package com.carriez.flutter_hbb
// Inspired by https://github.com/yosemiteyss/flutter_volume_controller/blob/main/android/src/main/kotlin/com/yosemiteyss/flutter_volume_controller/VolumeController.kt
import android.media.AudioManager
import android.os.Build
import android.util.Log
class VolumeController(private val audioManager: AudioManager) {
private val logTag = "volume controller"
fun getVolume(streamType: Int): Double {
val current = audioManager.getStreamVolume(streamType)
val max = audioManager.getStreamMaxVolume(streamType)
return current.toDouble() / max
}
fun setVolume(volume: Double, showSystemUI: Boolean, streamType: Int) {
val max = audioManager.getStreamMaxVolume(streamType)
audioManager.setStreamVolume(
streamType,
(max * volume).toInt(),
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
)
}
fun raiseVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
if (step == null) {
audioManager.adjustStreamVolume(
streamType,
AudioManager.ADJUST_RAISE,
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
)
} else {
val target = getVolume(streamType) + step
setVolume(target, showSystemUI, streamType)
}
}
fun lowerVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
if (step == null) {
audioManager.adjustStreamVolume(
streamType,
AudioManager.ADJUST_LOWER,
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
)
} else {
val target = getVolume(streamType) - step
setVolume(target, showSystemUI, streamType)
}
}
fun getMute(streamType: Int): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioManager.isStreamMute(streamType)
} else {
audioManager.getStreamVolume(streamType) == 0
}
}
private fun setMute(isMuted: Boolean, showSystemUI: Boolean, streamType: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioManager.adjustStreamVolume(
streamType,
if (isMuted) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE,
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
)
} else {
audioManager.setStreamMute(streamType, isMuted)
}
}
fun toggleMute(showSystemUI: Boolean, streamType: Int) {
val isMuted = getMute(streamType)
setMute(!isMuted, showSystemUI, streamType)
}
}

View File

@@ -15,10 +15,14 @@ import android.os.Looper
import android.os.PowerManager
import android.provider.Settings
import android.provider.Settings.*
import android.util.DisplayMetrics
import android.util.Log
import android.view.WindowManager
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat.getSystemService
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import ffi.FFI
import java.nio.ByteBuffer
import java.util.*
@@ -43,6 +47,9 @@ const val START_ACTION = "start_action"
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
const val GET_VALUE = "get_value"
const val KEY_IS_SUPPORT_VOICE_CALL = "KEY_IS_SUPPORT_VOICE_CALL"
const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
@@ -56,6 +63,11 @@ data class Info(
var width: Int, var height: Int, var scale: Int, var dpi: Int
)
fun isSupportVoiceCall(): Boolean {
// https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
}
fun requestPermission(context: Context, type: String) {
XXPermissions.with(context)
.permission(type)
@@ -120,3 +132,26 @@ class AudioReader(val bufSize: Int, private val maxFrames: Int) {
}
}
}
fun getScreenSize(windowManager: WindowManager) : Pair<Int, Int>{
var w = 0
var h = 0
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val m = windowManager.maximumWindowMetrics
w = m.bounds.width()
h = m.bounds.height()
} else {
val dm = DisplayMetrics()
windowManager.defaultDisplay.getRealMetrics(dm)
w = dm.widthPixels
h = dm.heightPixels
}
return Pair(w, h)
}
fun translate(input: String): String {
Log.d("common", "translate:$LOCAL_NAME")
return FFI.translateLocale(LOCAL_NAME, input)
}

View File

@@ -19,4 +19,5 @@ object FFI {
external fun refreshScreen()
external fun setFrameRawEnable(name: String, value: Boolean)
external fun setCodecInfo(info: String)
external fun getLocalOption(key: String): String
}

View File

@@ -0,0 +1,7 @@
<vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android" android:height="320dp" android:viewportHeight="32" android:viewportWidth="32" android:width="320dp">
<path android:fillColor="#ffffff" android:pathData="M16,0L16,0A16,16 0,0 1,32 16L32,16A16,16 0,0 1,16 32L16,32A16,16 0,0 1,0 16L0,16A16,16 0,0 1,16 0z" android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#1a1a1a" android:pathData="m23.89,10.135 l-1.807,1.795c-0.318,0.285 -0.472,0.744 -0.293,1.131 1.204,2.518 0.747,5.52 -1.228,7.494 -1.976,1.973 -4.981,2.429 -7.502,1.226 -0.371,-0.166 -0.807,-0.025 -1.093,0.265l-1.836,1.833c-0.216,0.211 -0.322,0.51 -0.288,0.809 0.034,0.3 0.206,0.567 0.463,0.723 4.326,2.618 9.882,1.951 13.463,-1.618 3.581,-3.568 4.264,-9.115 1.655,-13.443 -0.15,-0.263 -0.414,-0.442 -0.714,-0.484 -0.3,-0.043 -0.603,0.058 -0.819,0.269zM8.265,8.184c-3.599,3.554 -4.304,9.103 -1.709,13.441 0.15,0.264 0.413,0.443 0.714,0.485 0.3,0.042 0.603,-0.058 0.82,-0.27l1.797,-1.785c0.325,-0.285 0.484,-0.749 0.303,-1.141 -1.204,-2.518 -0.748,-5.52 1.228,-7.493 1.975,-1.973 4.981,-2.429 7.502,-1.227 0.367,0.165 0.797,0.028 1.084,-0.254l1.846,-1.844c0.216,-0.211 0.322,-0.509 0.288,-0.809 -0.035,-0.299 -0.206,-0.566 -0.463,-0.723 -4.334,-2.596 -9.881,-1.908 -13.448,1.668z" android:strokeWidth="0.987992"/>
</vector>

565
flutter/build_fdroid.sh Executable file
View File

@@ -0,0 +1,565 @@
#!/bin/bash
set -x
#
# Script to build F-Droid release of RustDesk
#
# Copyright (C) 2024, The RustDesk Authors
# 2024, Vasyl Gello <vasek.gello@gmail.com>
#
# The script is invoked by F-Droid builder system ste-by-step.
#
# It accepts the following arguments:
#
# - versionName from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
# - versionCode from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64
# - The build step to execute:
#
# + sudo-deps: as root, install needed Debian packages into builder VM
# + prebuild: patch sources and do other stuff before the build
# + build: perform actual build of APK file
#
# Parse command-line arguments
VERNAME="${1}"
VERCODE="${2}"
ANDROID_ABI="${3}"
BUILDSTEP="${4}"
if [ -z "${VERNAME}" ] || [ -z "${VERCODE}" ] || [ -z "${ANDROID_ABI}" ] ||
[ -z "${BUILDSTEP}" ]; then
echo "ERROR: Command-line arguments are all required to be non-empty!" >&2
exit 1
fi
# Set various architecture-specific identifiers
case "${ANDROID_ABI}" in
arm64-v8a)
FLUTTER_TARGET=android-arm64
NDK_TARGET=aarch64-linux-android
RUST_TARGET=aarch64-linux-android
# RUSTDESK_FEATURES='flutter,hwcodec'
RUSTDESK_FEATURES='flutter'
;;
armeabi-v7a)
FLUTTER_TARGET=android-arm
NDK_TARGET=arm-linux-androideabi
RUST_TARGET=armv7-linux-androideabi
# RUSTDESK_FEATURES='flutter,hwcodec'
RUSTDESK_FEATURES='flutter'
;;
x86_64)
FLUTTER_TARGET=android-x64
NDK_TARGET=x86_64-linux-android
RUST_TARGET=x86_64-linux-android
RUSTDESK_FEATURES='flutter'
;;
x86)
FLUTTER_TARGET=android-x86
NDK_TARGET=i686-linux-android
RUST_TARGET=i686-linux-android
RUSTDESK_FEATURES='flutter'
;;
*)
echo "ERROR: Unknown Android ABI '${ANDROID_ABI}'!" >&2
exit 1
;;
esac
# Check ANDROID_SDK_ROOT and sdkmanager present on PATH
if [ ! -d "${ANDROID_SDK_ROOT}" ] || ! command -v sdkmanager 1>/dev/null; then
echo "ERROR: Can not find Android SDK!" >&2
exit 1
fi
# Export necessary variables
export PATH="${PATH}:${HOME}/flutter/bin:${HOME}/depot_tools"
export VCPKG_ROOT="${HOME}/vcpkg"
# Now act depending on build step
# NOTE: F-Droid maintainers require explicit declaration of dependencies
# as root via `Builds.sudo` F-Droid metadata directive:
# https://gitlab.com/fdroid/fdroiddata/-/merge_requests/15343#note_1988918695
case "${BUILDSTEP}" in
prebuild)
# prebuild: patch sources and do other stuff before the build
#
# Extract required versions for NDK, Rust, Flutter from
# '.github/workflows/flutter-build.yml'
#
CARGO_NDK_VERSION="$(yq -r \
.env.CARGO_NDK_VERSION \
.github/workflows/flutter-build.yml)"
FLUTTER_VERSION="$(yq -r \
.env.ANDROID_FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
if [ -z "${FLUTTER_VERSION}" ]; then
FLUTTER_VERSION="$(yq -r \
.env.FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
fi
FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \
.env.FLUTTER_RUST_BRIDGE_VERSION \
.github/workflows/flutter-build.yml)"
NDK_VERSION="$(yq -r \
.env.NDK_VERSION \
.github/workflows/flutter-build.yml)"
RUST_VERSION="$(yq -r \
.env.RUST_VERSION \
.github/workflows/flutter-build.yml)"
VCPKG_COMMIT_ID="$(yq -r \
.env.VCPKG_COMMIT_ID \
.github/workflows/flutter-build.yml)"
if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] ||
[ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] ||
[ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] ||
[ -z "${VCPKG_COMMIT_ID}" ]; then
echo "ERROR: Can not identify all required versions!" >&2
exit 1
fi
# Map NDK version to revision
NDK_VERSION="$(wget \
-qO- \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
'https://api.github.com/repos/android/ndk/releases' |
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
if [ -z "${NDK_VERSION}" ]; then
echo "ERROR: Can not map Android NDK codename to revision!" >&2
exit 1
fi
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
#
# Install the components
#
set -e
# Install Android NDK
if [ ! -d "${ANDROID_NDK_ROOT}" ]; then
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
pushd "${HOME}"
git clone --depth 1 https://github.com/rust-lang/rustup
popd # ${HOME}
fi
pushd "${HOME}/rustup"
bash rustup-init.sh -y \
--target "${RUST_TARGET}" \
--default-toolchain "${RUST_VERSION}"
popd
if ! command -v cargo 1>/dev/null 2>&1; then
. "${HOME}/.cargo/env"
fi
# Install cargo-ndk
cargo install \
cargo-ndk \
--version "${CARGO_NDK_VERSION}"
# Install rust bridge generator
cargo install cargo-expand
cargo install flutter_rust_bridge_codegen \
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
--features "uuid"
# Populate native vcpkg dependencies
if [ ! -d "${VCPKG_ROOT}" ]; then
pushd "${HOME}"
git clone \
https://github.com/Microsoft/vcpkg.git
git clone \
https://github.com/Microsoft/vcpkg-tool.git
pushd vcpkg-tool
mkdir build
pushd build
cmake \
-DCMAKE_BUILD_TYPE=Release \
-G 'Ninja' \
-DVCPKG_DEVELOPMENT_WARNINGS=OFF \
..
cmake --build .
popd # build
popd # vcpkg-tool
pushd vcpkg
git reset --hard "${VCPKG_COMMIT_ID}"
cp -a ../vcpkg-tool/build/vcpkg vcpkg
# disable telemetry
touch "vcpkg.disable-metrics"
popd # vcpkg
popd # ${HOME}
fi
# Install depot-tools for x86
if [ "${ANDROID_ABI}" = "x86" ]; then
if [ ! -d "${HOME}/depot_tools" ]; then
pushd "${HOME}"
git clone \
--depth 1 \
https://chromium.googlesource.com/chromium/tools/depot_tools.git
popd # ${HOME}
fi
fi
# Patch the RustDesk sources
git apply res/fdroid/patches/*.patch
sed \
-i \
-e '/gms/d' \
flutter/android/build.gradle \
flutter/android/app/build.gradle
sed \
-i \
-e '/firebase_analytics/d' \
flutter/pubspec.yaml
sed \
-i \
-e '/ firebase/,/ version/d' \
flutter/pubspec.lock
sed \
-i \
-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/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
set -e
#
# Extract required versions for NDK, Rust, Flutter from
# '.github/workflows/flutter-build.yml'
#
FLUTTER_VERSION="$(yq -r \
.env.ANDROID_FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
if [ -z "${FLUTTER_VERSION}" ]; then
FLUTTER_VERSION="$(yq -r \
.env.FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
fi
NDK_VERSION="$(yq -r \
.env.NDK_VERSION \
.github/workflows/flutter-build.yml)"
# Map NDK version to revision
NDK_VERSION="$(wget \
-qO- \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
'https://api.github.com/repos/android/ndk/releases' |
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
if [ -z "${NDK_VERSION}" ]; then
echo "ERROR: Can not map Android NDK codename to revision!" >&2
exit 1
fi
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
if ! command -v cargo 1>/dev/null 2>&1; then
. "${HOME}/.cargo/env"
fi
# Download Flutter dependencies
pushd flutter
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}"
# Build rustdesk lib
cargo ndk \
--platform 21 \
--target "${RUST_TARGET}" \
--bindgen \
build \
--release \
--features "${RUSTDESK_FEATURES}"
mkdir -p "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"
cp "target/${RUST_TARGET}/release/liblibrustdesk.so" \
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/librustdesk.so"
cp "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/${NDK_TARGET}/libc++_shared.so" \
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/"
"${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" \
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"/*
# Build flutter-jit-release for x86
if [ "${ANDROID_ABI}" = "x86" ]; then
pushd flutter-sdk
echo "## Sync flutter engine sources"
echo "### We need fakeroot because chromium base image is unpacked with weird uid/gid ownership"
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" .gclient
export FAKEROOTDONTTRYCHOWN=1
fakeroot gclient sync
unset FAKEROOTDONTTRYCHOWN
pushd src
echo "## Patch away Google Play dependencies"
rm \
flutter/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java \
flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java flutter/shell/platform/android/io/flutter/embedding/android/FlutterPlayStoreSplitApplication.java
sed \
-i \
-e '/PlayStore/d' \
flutter/tools/android_lint/project.xml \
flutter/shell/platform/android/BUILD.gn
sed \
-i \
-e '/com.google.android.play/d' \
flutter/tools/androidx/files.json
echo "## Configure android engine build"
flutter/tools/gn \
--android --android-cpu x86 --runtime-mode=jit_release \
--no-goma --no-enable-unittests
echo "## Perform android engine build"
ninja -C out/android_jit_release_x86
echo "## Configure host engine build"
flutter/tools/gn \
--android-cpu x86 --runtime-mode=jit_release \
--no-goma --no-enable-unittests
echo "## Perform android engine build"
ninja -C out/host_jit_release_x86
echo "## Rename host engine"
mv out/host_jit_release_x86 out/host_jit_release
echo "## Mimic jit_release engine to debug to use with flutter build apk"
pushd out/android_jit_release_x86
sed \
-e 's/jit_release/debug/' \
flutter_embedding_jit_release.maven-metadata.xml \
1>flutter_embedding_debug.maven-metadata.xml
sed \
-e 's/jit_release/debug/' \
flutter_embedding_jit_release.pom \
1>flutter_embedding_debug.pom
sed \
-e 's/jit_release/debug/' \
x86_jit_release.maven-metadata.xml \
1>x86_debug.maven-metadata.xml
sed \
-e 's/jit_release/debug/' \
x86_jit_release.pom \
1>x86_debug.pom
cp -a \
flutter_embedding_jit_release-sources.jar \
flutter_embedding_debug-sources.jar
cp -a \
flutter_embedding_jit_release.jar \
flutter_embedding_debug.jar
cp -a \
x86_jit_release.jar \
x86_debug.jar
popd # out/android_jit_release_x86
popd # src
popd # flutter-sdk
echo "# Clean up intermediate engine files and show free space"
rm -rf \
flutter-sdk/src/out/android_jit_release_x86/obj \
flutter-sdk/src/out/host_jit_release/obj
mv flutter-sdk/src/out flutter-out
rm -rf flutter-sdk
mkdir -p flutter-sdk/src/
mv flutter-out flutter-sdk/src/out
fi
# Build the apk
pushd flutter
if [ "${ANDROID_ABI}" = "x86" ]; then
flutter build apk \
--local-engine-src-path="$(readlink -mf "../flutter-sdk/src")" \
--local-engine=android_jit_release_x86 \
--debug \
--build-number="${VERCODE}" \
--build-name="${VERNAME}" \
--target-platform "${FLUTTER_TARGET}"
else
flutter build apk \
--release \
--build-number="${VERCODE}" \
--build-name="${VERNAME}" \
--target-platform "${FLUTTER_TARGET}"
fi
popd # flutter
rm -rf flutter-sdk
# Special step for fdroiddata CI builds to remove .gitconfig
rm -f /home/vagrant/.gitconfig
;;
*)
echo "ERROR: Unknown build step '${BUILDSTEP}'!" >&2
exit 1
;;
esac
# Report success
echo "All done!"

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.networking.wifi-info</key>
<true/>
</dict>

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo build --features flutter --release --target aarch64-apple-ios --lib
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib

View File

@@ -718,7 +718,21 @@ class OverlayDialogManager {
int _tagCount = 0;
OverlayEntry? _mobileActionsOverlayEntry;
RxBool mobileActionsOverlayVisible = false.obs;
RxBool mobileActionsOverlayVisible = true.obs;
setMobileActionsOverlayVisible(bool v, {store = true}) {
if (store) {
bind.setLocalFlutterOption(k: kOptionShowMobileAction, v: v ? 'Y' : 'N');
}
// No need to read the value from local storage after setting it.
// It better to toggle the value directly.
mobileActionsOverlayVisible.value = v;
}
loadMobileActionsOverlayVisible() {
mobileActionsOverlayVisible.value =
bind.getLocalFlutterOption(k: kOptionShowMobileAction) != 'N';
}
void setOverlayState(OverlayKeyState overlayKeyState) {
_overlayKeyState = overlayKeyState;
@@ -865,14 +879,14 @@ class OverlayDialogManager {
);
overlayState.insert(overlay);
_mobileActionsOverlayEntry = overlay;
mobileActionsOverlayVisible.value = true;
setMobileActionsOverlayVisible(true);
}
void hideMobileActionsOverlay() {
void hideMobileActionsOverlay({store = true}) {
if (_mobileActionsOverlayEntry != null) {
_mobileActionsOverlayEntry!.remove();
_mobileActionsOverlayEntry = null;
mobileActionsOverlayVisible.value = false;
setMobileActionsOverlayVisible(false, store: store);
return;
}
}
@@ -891,30 +905,32 @@ class OverlayDialogManager {
}
makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
final position = SimpleWrapper(Offset(0, 0));
makeMobileActions(BuildContext context, double s) {
final scale = s < 0.85 ? 0.85 : s;
final session = ffi ?? gFFI;
// compute overlay position
final screenW = MediaQuery.of(context).size.width;
final screenH = MediaQuery.of(context).size.height;
const double overlayW = 200;
const double overlayH = 45;
final left = (screenW - overlayW * scale) / 2;
final top = screenH - (overlayH + 80) * scale;
position.value = Offset(left, top);
computeOverlayPosition() {
final screenW = MediaQuery.of(context).size.width;
final screenH = MediaQuery.of(context).size.height;
final left = (screenW - overlayW * scale) / 2;
final top = screenH - (overlayH + 80) * scale;
return Offset(left, top);
}
if (draggablePositions.mobileActions.isInvalid()) {
draggablePositions.mobileActions.update(computeOverlayPosition());
} else {
draggablePositions.mobileActions.tryAdjust(overlayW, overlayH, scale);
}
return DraggableMobileActions(
scale: scale,
position: position,
position: draggablePositions.mobileActions,
width: overlayW,
height: overlayH,
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
onRecentPressed: () async {
session.inputModel.sendMouse('down', MouseButtons.wheel);
await Future.delayed(const Duration(milliseconds: 500));
session.inputModel.sendMouse('up', MouseButtons.wheel);
},
onBackPressed: session.inputModel.onMobileBack,
onHomePressed: session.inputModel.onMobileHome,
onRecentPressed: session.inputModel.onMobileApps,
onHidePressed: onHide,
);
}
@@ -1008,7 +1024,8 @@ class CustomAlertDialog extends StatelessWidget {
return KeyEventResult.handled; // avoid TextField exception on escape
} else if (!tabTapped &&
onSubmit != null &&
(key.logicalKey == LogicalKeyboardKey.enter || key.logicalKey == LogicalKeyboardKey.numpadEnter)) {
(key.logicalKey == LogicalKeyboardKey.enter ||
key.logicalKey == LogicalKeyboardKey.numpadEnter)) {
if (key is RawKeyDownEvent) onSubmit?.call();
return KeyEventResult.handled;
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
@@ -1045,7 +1062,7 @@ void msgBox(SessionID sessionId, String type, String title, String text,
bool hasOk = false;
submit() {
dialogManager.dismissAll();
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
// https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
if (!type.contains("custom") && desktopType != DesktopType.portForward) {
closeConnection();
}
@@ -1079,21 +1096,33 @@ void msgBox(SessionID sessionId, String type, String title, String text,
dialogManager.dismissAll();
}));
}
if (reconnect != null &&
title == "Connection Error" &&
reconnectTimeout != null) {
if (reconnect != null && title == "Connection Error") {
// `enabled` is used to disable the dialog button once the button is clicked.
final enabled = true.obs;
final button = Obx(() => _ReconnectCountDownButton(
second: reconnectTimeout,
onPressed: enabled.isTrue
? () {
// Disable the button
enabled.value = false;
reconnect(dialogManager, sessionId, false);
}
: null,
));
final button = reconnectTimeout != null
? Obx(() => _ReconnectCountDownButton(
second: reconnectTimeout,
onPressed: enabled.isTrue
? () {
// Disable the button
enabled.value = false;
reconnect(dialogManager, sessionId, false);
}
: null,
))
: Obx(
() => dialogButton(
'Reconnect',
isOutline: true,
onPressed: enabled.isTrue
? () {
// Disable the button
enabled.value = false;
reconnect(dialogManager, sessionId, false);
}
: null,
),
);
buttons.insert(0, button);
}
if (link.isNotEmpty) {
@@ -1411,7 +1440,7 @@ Future<void> initGlobalFFI() async {
_globalFFI = FFI(null);
debugPrint("_globalFFI init end");
// after `put`, can also be globally found by Get.find<FFI>();
Get.put(_globalFFI, permanent: true);
Get.put<FFI>(_globalFFI, permanent: true);
}
String translate(String name) {
@@ -1421,14 +1450,16 @@ String translate(String name) {
return platformFFI.translate(name, localeName);
}
// This function must be kept the same as the one in rust and sciter code.
// rust: libs/hbb_common/src/config.rs -> option2bool()
// sciter: Does not have the function, but it should be kept the same.
bool option2bool(String option, String value) {
bool res;
if (option.startsWith("enable-")) {
res = value != "N";
} else if (option.startsWith("allow-") ||
option == "stop-service" ||
option == kOptionStopService ||
option == kOptionDirectServer ||
option == "stop-rendezvous-service" ||
option == kOptionForceAlwaysRelay) {
res = value == "Y";
} else {
@@ -1443,9 +1474,8 @@ String bool2option(String option, bool b) {
if (option.startsWith('enable-')) {
res = b ? defaultOptionYes : 'N';
} else if (option.startsWith('allow-') ||
option == "stop-service" ||
option == kOptionStopService ||
option == kOptionDirectServer ||
option == "stop-rendezvous-service" ||
option == kOptionForceAlwaysRelay) {
res = b ? 'Y' : defaultOptionNo;
} else {
@@ -1481,9 +1511,9 @@ bool mainGetPeerBoolOptionSync(String id, String key) {
return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key));
}
mainSetPeerBoolOptionSync(String id, String key, bool v) {
bind.mainSetPeerOptionSync(id: id, key: key, value: bool2option(key, v));
}
// Don't use `option2bool()` and `bool2option()` to convert the session option.
// Use `sessionGetToggleOption()` and `sessionToggleOption()` instead.
// Because all session options use `Y` and `<Empty>` as values.
Future<bool> matchPeer(String searchText, Peer peer) async {
if (searchText.isEmpty) {
@@ -2668,36 +2698,55 @@ Future<void> start_service(bool is_start) async {
!isMacOS ||
await callMainCheckSuperUserPermission();
if (checked) {
bind.mainSetOption(key: "stop-service", value: is_start ? "" : "Y");
mainSetBoolOption(kOptionStopService, !is_start);
}
}
Future<bool> canBeBlocked() async {
var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
var option = option2bool(kOptionAllowRemoteConfigModification,
await bind.mainGetOption(key: kOptionAllowRemoteConfigModification));
return access_mode == 'view' || (access_mode.isEmpty && !option);
}
Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
if (use != null && !await use()) {
block.value = false;
return;
}
var time0 = DateTime.now().millisecondsSinceEpoch;
await bind.mainCheckMouseTime();
Timer(const Duration(milliseconds: 120), () async {
var d = time0 - await bind.mainGetMouseTime();
if (d < 120) {
block.value = true;
} else {
block.value = false;
}
});
}
typedef WhetherUseRemoteBlock = Future<bool> Function();
Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) {
var block = false.obs;
Widget buildRemoteBlock(
{required Widget child,
required RxBool block,
required bool mask,
WhetherUseRemoteBlock? use}) {
return Obx(() => MouseRegion(
onEnter: (_) async {
if (use != null && !await use()) {
block.value = false;
return;
}
var time0 = DateTime.now().millisecondsSinceEpoch;
await bind.mainCheckMouseTime();
Timer(const Duration(milliseconds: 120), () async {
var d = time0 - await bind.mainGetMouseTime();
if (d < 120) {
block.value = true;
}
});
await shouldBeBlocked(block, use);
},
onExit: (event) => block.value = false,
child: Stack(children: [
child,
Offstage(
offstage: !block.value,
child: Container(
color: Colors.black.withOpacity(0.5),
)),
// scope block tab
FocusScope(child: child, canRequestFocus: !block.value),
// mask block click, cm not block click and still use check_click_time to avoid block local click
if (mask)
Offstage(
offstage: !block.value,
child: Container(
color: Colors.black.withOpacity(0.5),
)),
]),
));
}
@@ -2745,12 +2794,10 @@ Widget buildErrorBanner(BuildContext context,
required RxString err,
required Function? retry,
required Function close}) {
const double height = 25;
return Obx(() => Offstage(
offstage: !(!loading.value && err.value.isNotEmpty),
child: Center(
child: Container(
height: height,
color: MyTheme.color(context).errorBannerBg,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -2769,7 +2816,6 @@ Widget buildErrorBanner(BuildContext context,
message: translate(err.value),
child: Text(
translate(err.value),
overflow: TextOverflow.ellipsis,
),
)).marginSymmetric(vertical: 2),
),
@@ -2922,10 +2968,11 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
}
setNewConnectWindowFrame(int windowId, String peerId, Rect? screenRect) async {
setNewConnectWindowFrame(
int windowId, String peerId, int? display, Rect? screenRect) async {
if (screenRect == null) {
await restoreWindowPosition(WindowType.RemoteDesktop,
windowId: windowId, peerId: peerId);
windowId: windowId, display: display, peerId: peerId);
} else {
await tryMoveToScreenAndSetFullscreen(screenRect);
}
@@ -3051,9 +3098,16 @@ Future<bool> setServerConfig(
List<RxString>? errMsgs,
ServerConfig config,
) async {
config.idServer = config.idServer.trim();
config.relayServer = config.relayServer.trim();
config.apiServer = config.apiServer.trim();
String removeEndSlash(String input) {
if (input.endsWith('/')) {
return input.substring(0, input.length - 1);
}
return input;
}
config.idServer = removeEndSlash(config.idServer.trim());
config.relayServer = removeEndSlash(config.relayServer.trim());
config.apiServer = removeEndSlash(config.apiServer.trim());
config.key = config.key.trim();
if (controllers != null) {
controllers[0].text = config.idServer;
@@ -3272,6 +3326,11 @@ Widget buildPresetPasswordWarning() {
return Text(
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
} else if (snapshot.hasData && snapshot.data == true) {
if (bind.mainGetBuildinOption(
key: kOptionRemovePresetPasswordWarning) !=
'N') {
return SizedBox.shrink();
}
return Container(
color: Colors.yellow,
child: Column(
@@ -3372,3 +3431,33 @@ get defaultOptionNo => isCustomClient ? 'N' : '';
get defaultOptionWhitelist => isCustomClient ? ',' : '';
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
bool whitelistNotEmpty() {
// https://rustdesk.com/docs/en/self-host/client-configuration/advanced-settings/#whitelist
final v = bind.mainGetOptionSync(key: kOptionWhitelist);
return v != '' && v != ',';
}
// `setMovable()` is only supported on macOS.
//
// On macOS, the window can be dragged by the tab bar by default.
// We need to disable the movable feature to prevent the window from being dragged by the tabs in the tab bar.
//
// When we drag the blank tab bar (not the tab), the window will be dragged normally by adding the `onPanStart` handle.
//
// See the following code for more details:
// https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L385
// https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L399
//
// @platforms macos
disableWindowMovable(int? windowId) {
if (!isMacOS) {
return;
}
if (windowId == null) {
windowManager.setMovable(false);
} else {
WindowController.fromWindowId(windowId).setMovable(false);
}
}

View File

@@ -10,16 +10,16 @@ class PrivacyModeState {
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
if (!Get.isRegistered<RxString>(tag: key)) {
final RxString state = ''.obs;
Get.put(state, tag: key);
Get.put<RxString>(state, tag: key);
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
if (Get.isRegistered<RxString>(tag: key)) {
Get.delete<RxString>(tag: key);
} else {
Get.find<RxString>(tag: key).value = '';
}
@@ -33,9 +33,9 @@ class BlockInputState {
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs;
Get.put(state, tag: key);
Get.put<RxBool>(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = false;
}
@@ -43,8 +43,8 @@ class BlockInputState {
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete<RxBool>(tag: key);
}
}
@@ -56,9 +56,9 @@ class CurrentDisplayState {
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
if (!Get.isRegistered<RxInt>(tag: key)) {
final RxInt state = RxInt(0);
Get.put(state, tag: key);
Get.put<RxInt>(state, tag: key);
} else {
Get.find<RxInt>(tag: key).value = 0;
}
@@ -66,8 +66,8 @@ class CurrentDisplayState {
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
if (Get.isRegistered<RxInt>(tag: key)) {
Get.delete<RxInt>(tag: key);
}
}
@@ -105,16 +105,16 @@ class ConnectionTypeState {
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
if (!Get.isRegistered<ConnectionType>(tag: key)) {
final ConnectionType collectionType = ConnectionType();
Get.put(collectionType, tag: key);
Get.put<ConnectionType>(collectionType, tag: key);
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
if (Get.isRegistered<ConnectionType>(tag: key)) {
Get.delete<ConnectionType>(tag: key);
}
}
@@ -127,9 +127,9 @@ class FingerprintState {
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
if (!Get.isRegistered<RxString>(tag: key)) {
final RxString state = ''.obs;
Get.put(state, tag: key);
Get.put<RxString>(state, tag: key);
} else {
Get.find<RxString>(tag: key).value = '';
}
@@ -137,8 +137,8 @@ class FingerprintState {
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
if (Get.isRegistered<RxString>(tag: key)) {
Get.delete<RxString>(tag: key);
}
}
@@ -150,9 +150,9 @@ class ShowRemoteCursorState {
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs;
Get.put(state, tag: key);
Get.put<RxBool>(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = false;
}
@@ -160,8 +160,8 @@ class ShowRemoteCursorState {
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete<RxBool>(tag: key);
}
}
@@ -173,9 +173,9 @@ class ShowRemoteCursorLockState {
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs;
Get.put(state, tag: key);
Get.put<RxBool>(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = false;
}
@@ -183,8 +183,8 @@ class ShowRemoteCursorLockState {
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete<RxBool>(tag: key);
}
}
@@ -196,10 +196,10 @@ class KeyboardEnabledState {
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
if (!Get.isRegistered<RxBool>(tag: key)) {
// Server side, default true
final RxBool state = true.obs;
Get.put(state, tag: key);
Get.put<RxBool>(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = true;
}
@@ -207,8 +207,8 @@ class KeyboardEnabledState {
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete<RxBool>(tag: key);
}
}
@@ -220,9 +220,9 @@ class RemoteCursorMovedState {
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs;
Get.put(state, tag: key);
Get.put<RxBool>(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = false;
}
@@ -230,8 +230,8 @@ class RemoteCursorMovedState {
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete<RxBool>(tag: key);
}
}
@@ -243,9 +243,9 @@ class RemoteCountState {
static void init() {
final key = tag();
if (!Get.isRegistered(tag: key)) {
if (!Get.isRegistered<RxInt>(tag: key)) {
final RxInt state = 1.obs;
Get.put(state, tag: key);
Get.put<RxInt>(state, tag: key);
} else {
Get.find<RxInt>(tag: key).value = 1;
}
@@ -253,8 +253,8 @@ class RemoteCountState {
static void delete() {
final key = tag();
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
if (Get.isRegistered<RxInt>(tag: key)) {
Get.delete<RxInt>(tag: key);
}
}
@@ -266,9 +266,9 @@ class PeerBoolOption {
static void init(String id, String opt, bool Function() init_getter) {
final key = tag(id, opt);
if (!Get.isRegistered(tag: key)) {
if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool value = RxBool(init_getter());
Get.put(value, tag: key);
Get.put<RxBool>(value, tag: key);
} else {
Get.find<RxBool>(tag: key).value = init_getter();
}
@@ -276,8 +276,8 @@ class PeerBoolOption {
static void delete(String id, String opt) {
final key = tag(id, opt);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete<RxBool>(tag: key);
}
}
@@ -290,9 +290,9 @@ class PeerStringOption {
static void init(String id, String opt, String Function() init_getter) {
final key = tag(id, opt);
if (!Get.isRegistered(tag: key)) {
if (!Get.isRegistered<RxString>(tag: key)) {
final RxString value = RxString(init_getter());
Get.put(value, tag: key);
Get.put<RxString>(value, tag: key);
} else {
Get.find<RxString>(tag: key).value = init_getter();
}
@@ -300,8 +300,8 @@ class PeerStringOption {
static void delete(String id, String opt) {
final key = tag(id, opt);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
if (Get.isRegistered<RxString>(tag: key)) {
Get.delete<RxString>(tag: key);
}
}
@@ -314,9 +314,9 @@ class UnreadChatCountState {
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
if (!Get.isRegistered<RxInt>(tag: key)) {
final RxInt state = RxInt(0);
Get.put(state, tag: key);
Get.put<RxInt>(state, tag: key);
} else {
Get.find<RxInt>(tag: key).value = 0;
}
@@ -324,8 +324,8 @@ class UnreadChatCountState {
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
if (Get.isRegistered<RxInt>(tag: key)) {
Get.delete<RxInt>(tag: key);
}
}

View File

@@ -110,6 +110,7 @@ class _AddressBookState extends State<AddressBook> {
}
Widget _buildAddressBookMobile() {
const padding = 8.0;
return Column(
children: [
Offstage(
@@ -120,7 +121,8 @@ class _AddressBookState extends State<AddressBook> {
border: Border.all(
color: Theme.of(context).colorScheme.background)),
child: Container(
padding: const EdgeInsets.all(8.0),
padding:
const EdgeInsets.fromLTRB(padding, 0, padding, padding),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -130,7 +132,6 @@ class _AddressBookState extends State<AddressBook> {
width: double.infinity,
child: _buildTags(),
),
_buildAbPermission(),
],
),
),
@@ -198,24 +199,28 @@ class _AddressBookState extends State<AddressBook> {
if (contains) {
names.insert(0, personalAddressBookName);
}
Row buildItem(String e, {bool button = false}) {
return Row(
children: [
Expanded(
child: Tooltip(
waitDuration: Duration(milliseconds: 500),
message: gFFI.abModel.translatedName(e),
child: Text(
gFFI.abModel.translatedName(e),
style: button ? null : TextStyle(fontSize: 14.0),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: button ? TextAlign.center : null,
)),
),
],
);
}
final items = names
.map((e) => DropdownMenuItem(
value: e,
child: Row(
children: [
Expanded(
child: Tooltip(
waitDuration: Duration(milliseconds: 500),
message: gFFI.abModel.translatedName(e),
child: Text(
gFFI.abModel.translatedName(e),
style: TextStyle(fontSize: 14.0),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)),
),
],
)))
.map((e) => DropdownMenuItem(value: e, child: buildItem(e)))
.toList();
var menuItemStyleData = MenuItemStyleData(height: 36);
if (contains && items.length > 1) {
@@ -237,14 +242,22 @@ class _AddressBookState extends State<AddressBook> {
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
}
},
customButton: Container(
height: isDesktop ? 48 : 40,
child: Row(children: [
Expanded(
child: buildItem(gFFI.abModel.currentName.value, button: true)),
Icon(Icons.arrow_drop_down),
]),
),
underline: Container(
height: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
buttonStyleData: ButtonStyleData(height: 48),
menuItemStyleData: menuItemStyleData,
items: items,
isExpanded: true,
isDense: true,
dropdownSearchData: DropdownSearchData(
searchController: textEditingController,
searchInnerWidgetHeight: 50,

View File

@@ -2,22 +2,39 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
const _kWindowsSystemSound = 'System Sound';
typedef AudioINputSetDevice = void Function(String device);
typedef AudioInputBuilder = Widget Function(
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
class AudioInput extends StatelessWidget {
final AudioInputBuilder builder;
final bool isCm;
final bool isVoiceCall;
const AudioInput({Key? key, required this.builder}) : super(key: key);
const AudioInput(
{Key? key,
required this.builder,
required this.isCm,
required this.isVoiceCall})
: super(key: key);
static String getDefault() {
if (isWindows) return translate('System Sound');
return '';
}
static Future<String> getValue() async {
String device = await bind.mainGetOption(key: 'audio-input');
static Future<String> getAudioInput(bool isCm, bool isVoiceCall) {
if (isVoiceCall) {
return bind.getVoiceCallInputDevice(isCm: isCm);
} else {
return bind.mainGetOption(key: 'audio-input');
}
}
static Future<String> getValue(bool isCm, bool isVoiceCall) async {
String device = await getAudioInput(isCm, isVoiceCall);
if (device.isNotEmpty) {
return device;
} else {
@@ -25,31 +42,39 @@ class AudioInput extends StatelessWidget {
}
}
static Future<void> setDevice(String device) async {
static Future<void> setDevice(
String device, bool isCm, bool isVoiceCall) async {
if (device == getDefault()) device = '';
await bind.mainSetOption(key: 'audio-input', value: device);
if (isVoiceCall) {
await bind.setVoiceCallInputDevice(isCm: isCm, device: device);
} else {
await bind.mainSetOption(key: 'audio-input', value: device);
}
}
static Future<Map<String, Object>> getDevicesInfo() async {
static Future<Map<String, Object>> getDevicesInfo(
bool isCm, bool isVoiceCall) async {
List<String> devices = (await bind.mainGetSoundInputs()).toList();
if (isWindows) {
devices.insert(0, translate('System Sound'));
devices.insert(0, translate(_kWindowsSystemSound));
}
String current = await getValue();
String current = await getValue(isCm, isVoiceCall);
return {'devices': devices, 'current': current};
}
@override
Widget build(BuildContext context) {
return futureBuilder(
future: getDevicesInfo(),
future: getDevicesInfo(isCm, isVoiceCall),
hasData: (data) {
String currentDevice = data['current'];
List<String> devices = data['devices'] as List<String>;
if (devices.isEmpty) {
return const Offstage();
}
return builder(devices, currentDevice, setDevice);
return builder(devices, currentDevice, (devices) {
setDevice(devices, isCm, isVoiceCall);
});
},
);
}

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
@@ -218,50 +219,53 @@ void changeWhiteList({Function()? callback}) async {
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
if (!isOptFixed)dialogButton("Clear", onPressed: () async {
await bind.mainSetOption(
key: kOptionWhitelist, value: defaultOptionWhitelist);
callback?.call();
close();
}, isOutline: true),
if (!isOptFixed) dialogButton(
"OK",
onPressed: () async {
setState(() {
msg = "";
isInProgress = true;
});
newWhiteListField = controller.text.trim();
var newWhiteList = "";
if (newWhiteListField.isEmpty) {
// pass
} else {
final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
// test ip
final ipMatch = RegExp(
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
final ipv6Match = RegExp(
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
for (final ip in ips) {
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
msg = "${translate("Invalid IP")} $ip";
setState(() {
isInProgress = false;
});
return;
}
}
newWhiteList = ips.join(',');
}
if (newWhiteList.trim().isEmpty) {
newWhiteList = defaultOptionWhitelist;
}
if (!isOptFixed)
dialogButton("Clear", onPressed: () async {
await bind.mainSetOption(
key: kOptionWhitelist, value: newWhiteList);
key: kOptionWhitelist, value: defaultOptionWhitelist);
callback?.call();
close();
},
),
}, isOutline: true),
if (!isOptFixed)
dialogButton(
"OK",
onPressed: () async {
setState(() {
msg = "";
isInProgress = true;
});
newWhiteListField = controller.text.trim();
var newWhiteList = "";
if (newWhiteListField.isEmpty) {
// pass
} else {
final ips =
newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
// test ip
final ipMatch = RegExp(
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
final ipv6Match = RegExp(
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
for (final ip in ips) {
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
msg = "${translate("Invalid IP")} $ip";
setState(() {
isInProgress = false;
});
return;
}
}
newWhiteList = ips.join(',');
}
if (newWhiteList.trim().isEmpty) {
newWhiteList = defaultOptionWhitelist;
}
await bind.mainSetOption(
key: kOptionWhitelist, value: newWhiteList);
callback?.call();
close();
},
),
],
onCancel: close,
);
@@ -1762,6 +1766,66 @@ void renameDialog(
});
}
void changeBot({Function()? callback}) async {
if (bind.mainHasValidBotSync()) {
await bind.mainSetOption(key: "bot", value: "");
callback?.call();
return;
}
String errorText = '';
bool loading = false;
final controller = TextEditingController();
gFFI.dialogManager.show((setState, close, context) {
onVerify() async {
final token = controller.text.trim();
if (token == "") return;
loading = true;
errorText = '';
setState(() {});
final error = await bind.mainVerifyBot(token: token);
if (error == "") {
callback?.call();
close();
} else {
errorText = translate(error);
loading = false;
setState(() {});
}
}
final codeField = TextField(
autofocus: true,
controller: controller,
decoration: InputDecoration(
hintText: translate('Token'),
),
);
return CustomAlertDialog(
title: Text(translate("Telegram bot")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(translate("enable-bot-desc"),
style: TextStyle(fontSize: 12))
.marginOnly(bottom: 12),
Row(children: [Expanded(child: codeField)]),
if (errorText != '')
Text(errorText, style: TextStyle(color: Colors.red))
.marginOnly(top: 12),
],
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
loading
? CircularProgressIndicator()
: dialogButton("OK", onPressed: onVerify),
],
onCancel: close,
);
});
}
void change2fa({Function()? callback}) async {
if (bind.mainHasValid2FaSync()) {
await bind.mainSetOption(key: "2fa", value: "");
@@ -2124,3 +2188,31 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
);
});
}
void CommonConfirmDialog(OverlayDialogManager dialogManager, String content,
VoidCallback onConfirm) {
dialogManager.show((setState, close, context) {
submit() {
close();
onConfirm.call();
}
return CustomAlertDialog(
content: Row(
children: [
Expanded(
child: Text(content,
style: const TextStyle(fontSize: 15),
textAlign: TextAlign.start),
),
],
).marginOnly(bottom: 12),
actions: [
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
dialogButton(translate("OK"), onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}

View File

@@ -112,6 +112,8 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
};
}
// FIXME: This debounce logic is not working properly.
// If we move our finger very fast, we won't be able to detect the "oneFingerPan" event sometimes.
void onOneFingerStartDebounce(ScaleUpdateDetails d) {
start(ScaleUpdateDetails d) {
_currentState = GestureState.oneFingerPan;

View File

@@ -455,7 +455,7 @@ Future<bool?> loginDialog() async {
}
if (isEmailVerification != null) {
if (isMobile) {
if (close != null) close(false);
if (close != null) close(null);
verificationCodeDialog(
resp.user, resp.secret, isEmailVerification);
} else {
@@ -712,6 +712,11 @@ Future<bool?> verificationCodeDialog(
dialogButton("Verify", onPressed: getOnSubmit()),
]);
});
// For verification code, desktop update other models in login dialog, mobile need to close login dialog first,
// otherwise the soft keyboard will jump out on each key press, so mobile update in verification code dialog.
if (isMobile && res == true) {
await UserModel.updateOtherModels();
}
return res;
}

View File

@@ -1,6 +1,8 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
@@ -26,9 +28,12 @@ class DraggableChatWindow extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (draggablePositions.chatWindow.isInvalid()) {
draggablePositions.chatWindow.update(position);
}
return isIOS
? IOSDraggable(
position: position,
position: draggablePositions.chatWindow,
chatModel: chatModel,
width: width,
height: height,
@@ -45,7 +50,7 @@ class DraggableChatWindow extends StatelessWidget {
)
: Draggable(
checkKeyboard: true,
position: SimpleWrapper(position),
position: draggablePositions.chatWindow,
width: width,
height: height,
chatModel: chatModel,
@@ -176,7 +181,7 @@ class DraggableMobileActions extends StatelessWidget {
required this.scale});
final double scale;
final SimpleWrapper<Offset> position;
final DraggableKeyPosition position;
final double width;
final double height;
final VoidCallback? onBackPressed;
@@ -241,6 +246,92 @@ class DraggableMobileActions extends StatelessWidget {
}
}
class DraggableKeyPosition {
final String key;
Offset _pos;
late Debouncer<int> _debouncerStore;
DraggableKeyPosition(this.key)
: _pos = DraggablePositions.kInvalidDraggablePosition;
get pos => _pos;
_loadPosition(String k) {
final value = bind.getLocalFlutterOption(k: k);
if (value.isNotEmpty) {
final parts = value.split(',');
if (parts.length == 2) {
return Offset(double.parse(parts[0]), double.parse(parts[1]));
}
}
return DraggablePositions.kInvalidDraggablePosition;
}
load() {
_pos = _loadPosition(key);
_debouncerStore = Debouncer<int>(const Duration(milliseconds: 500),
onChanged: (v) => _store(), initialValue: 0);
}
update(Offset pos) {
_pos = pos;
_triggerStore();
}
// Adjust position to keep it in the screen
// Only used for desktop and web desktop
tryAdjust(double w, double h, double scale) {
final size = MediaQuery.of(Get.context!).size;
w = w * scale;
h = h * scale;
double x = _pos.dx;
double y = _pos.dy;
if (x + w > size.width) {
x = size.width - w;
}
final tabBarHeight = isDesktop ? kDesktopRemoteTabBarHeight : 0;
if (y + h > (size.height - tabBarHeight)) {
y = size.height - tabBarHeight - h;
}
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
if (x != _pos.dx || y != _pos.dy) {
update(Offset(x, y));
}
}
isInvalid() {
return _pos == DraggablePositions.kInvalidDraggablePosition;
}
_triggerStore() => _debouncerStore.value = _debouncerStore.value + 1;
_store() {
bind.setLocalFlutterOption(k: key, v: '${_pos.dx},${_pos.dy}');
}
}
class DraggablePositions {
static const kChatWindow = 'draggablePositionChat';
static const kMobileActions = 'draggablePositionMobile';
static const kIOSDraggable = 'draggablePositionIOS';
static const kInvalidDraggablePosition = Offset(-999999, -999999);
final chatWindow = DraggableKeyPosition(kChatWindow);
final mobileActions = DraggableKeyPosition(kMobileActions);
final iOSDraggable = DraggableKeyPosition(kIOSDraggable);
load() {
chatWindow.load();
mobileActions.load();
iOSDraggable.load();
}
}
DraggablePositions draggablePositions = DraggablePositions();
class Draggable extends StatefulWidget {
Draggable(
{Key? key,
@@ -255,7 +346,7 @@ class Draggable extends StatefulWidget {
final bool checkKeyboard;
final bool checkScreenSize;
final SimpleWrapper<Offset> position;
final DraggableKeyPosition position;
final double width;
final double height;
final ChatModel? chatModel;
@@ -277,7 +368,7 @@ class _DraggableState extends State<Draggable> {
_chatModel = widget.chatModel;
}
get position => widget.position.value;
get position => widget.position.pos;
void onPanUpdate(DragUpdateDetails d) {
final offset = d.delta;
@@ -301,7 +392,7 @@ class _DraggableState extends State<Draggable> {
y = position.dy + offset.dy;
}
setState(() {
widget.position.value = Offset(x, y);
widget.position.update(Offset(x, y));
});
_chatModel?.setChatWindowPosition(position);
}
@@ -320,7 +411,7 @@ class _DraggableState extends State<Draggable> {
// reset
if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() {
widget.position.value = Offset(position.dx, _saveHeight);
widget.position.update(Offset(position.dx, _saveHeight));
});
}
@@ -331,7 +422,7 @@ class _DraggableState extends State<Draggable> {
if (sumHeight + position.dy > contextHeight) {
final y = contextHeight - sumHeight;
setState(() {
widget.position.value = Offset(position.dx, y);
widget.position.update(Offset(position.dx, y));
});
}
}
@@ -362,14 +453,14 @@ class _DraggableState extends State<Draggable> {
class IOSDraggable extends StatefulWidget {
const IOSDraggable(
{Key? key,
this.position = Offset.zero,
this.chatModel,
required this.position,
required this.width,
required this.height,
required this.builder})
: super(key: key);
final Offset position;
final DraggableKeyPosition position;
final ChatModel? chatModel;
final double width;
final double height;
@@ -380,7 +471,6 @@ class IOSDraggable extends StatefulWidget {
}
class IOSDraggableState extends State<IOSDraggable> {
late Offset _position;
late ChatModel? _chatModel;
late double _width;
late double _height;
@@ -391,25 +481,26 @@ class IOSDraggableState extends State<IOSDraggable> {
@override
void initState() {
super.initState();
_position = widget.position;
_chatModel = widget.chatModel;
_width = widget.width;
_height = widget.height;
}
DraggableKeyPosition get position => widget.position;
checkKeyboard() {
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
final currentVisible = bottomHeight != 0;
// save
if (!_keyboardVisible && currentVisible) {
_saveHeight = _position.dy;
_saveHeight = position.pos.dy;
}
// reset
if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() {
_position = Offset(_position.dx, _saveHeight);
position.update(Offset(position.pos.dx, _saveHeight));
});
}
@@ -417,10 +508,10 @@ class IOSDraggableState extends State<IOSDraggable> {
if (_keyboardVisible && currentVisible) {
final sumHeight = bottomHeight + _height;
final contextHeight = MediaQuery.of(context).size.height;
if (sumHeight + _position.dy > contextHeight) {
if (sumHeight + position.pos.dy > contextHeight) {
final y = contextHeight - sumHeight;
setState(() {
_position = Offset(_position.dx, y);
position.update(Offset(position.pos.dx, y));
});
}
}
@@ -435,14 +526,14 @@ class IOSDraggableState extends State<IOSDraggable> {
return Stack(
children: [
Positioned(
left: _position.dx,
top: _position.dy,
left: position.pos.dx,
top: position.pos.dy,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
_position += details.delta;
position.update(position.pos + details.delta);
});
_chatModel?.setChatWindowPosition(_position);
_chatModel?.setChatWindowPosition(position.pos);
},
child: Material(
child: Container(
@@ -498,8 +589,10 @@ class QualityMonitor extends StatelessWidget {
children: [
_row("Speed", qualityMonitorModel.data.speed ?? '-'),
_row("FPS", qualityMonitorModel.data.fps ?? '-'),
// let delay be 0 if fps is 0
_row(
"Delay", "${qualityMonitorModel.data.delay ?? '-'}ms",
"Delay",
"${qualityMonitorModel.data.delay == null ? '-' : (qualityMonitorModel.data.fps ?? "").replaceAll(' ', '').replaceAll('0', '').isEmpty ? 0 : qualityMonitorModel.data.delay}ms",
rightColor: Colors.green),
_row("Target Bitrate",
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),

View File

@@ -22,6 +22,8 @@ enum PeerUiType { grid, tile, list }
final peerCardUiType = PeerUiType.grid.obs;
bool? hideUsernameOnCard;
class _PeerCard extends StatefulWidget {
final Peer peer;
final PeerTabIndex tab;
@@ -130,8 +132,11 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildPeerTile(
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
hideUsernameOnCard ??=
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
final name = hideUsernameOnCard == true
? peer.hostname
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final greyStyle = TextStyle(
fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
@@ -239,8 +244,11 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildPeerCard(
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
hideUsernameOnCard ??=
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
final name = hideUsernameOnCard == true
? peer.hostname
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final child = Card(
color: Colors.transparent,
elevation: 0,
@@ -887,7 +895,7 @@ class RecentPeerCard extends BasePeerCard {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
if (isDesktop || isWebDesktop) {
if (isMobile || isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id));
}
if (await bind.mainPeerHasPassword(id: peer.id)) {
@@ -943,7 +951,7 @@ class FavoritePeerCard extends BasePeerCard {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
if (isDesktop || isWebDesktop) {
if (isMobile || isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id));
}
if (await bind.mainPeerHasPassword(id: peer.id)) {
@@ -1048,7 +1056,7 @@ class AddressBookPeerCard extends BasePeerCard {
}
if (gFFI.abModel.current.canWrite()) {
menuItems.add(MenuEntryDivider());
if (isDesktop || isWebDesktop) {
if (isMobile || isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id));
}
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {

View File

@@ -399,9 +399,9 @@ class _PeerTabPageState extends State<PeerTabPage>
final peers = model.selectedPeers;
switch (model.currentTab) {
case 0:
peers.map((p) async {
for (var p in peers) {
await bind.mainRemovePeer(id: p.id);
}).toList();
}
await bind.mainLoadRecentPeers();
break;
case 1:
@@ -413,9 +413,9 @@ class _PeerTabPageState extends State<PeerTabPage>
await bind.mainLoadFavPeers();
break;
case 2:
peers.map((p) async {
for (var p in peers) {
await bind.mainRemoveDiscovered(id: p.id);
}).toList();
}
await bind.mainLoadLanPeers();
break;
case 3:
@@ -802,13 +802,20 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
child: SizedBox(
height: 36,
child: getRadio<PeerUiType>(
Text(
translate(types.indexOf(e) == 0
Tooltip(
message: translate(types.indexOf(e) == 0
? 'Big tiles'
: types.indexOf(e) == 1
? 'Small tiles'
: 'List'),
style: style),
child: Icon(
e == PeerUiType.grid
? Icons.grid_view_rounded
: e == PeerUiType.list
? Icons.view_list_rounded
: Icons.view_agenda_rounded,
size: 18,
)),
e,
peerCardUiType.value,
dense: true,
@@ -838,7 +845,7 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
child: Icon(
peerCardUiType.value == PeerUiType.grid
? Icons.grid_view_rounded
: peerCardUiType.value == PeerUiType.tile
: peerCardUiType.value == PeerUiType.list
? Icons.view_list_rounded
: Icons.view_agenda_rounded,
size: 18,

View File

@@ -82,7 +82,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
final _curPeers = <String>{};
var _lastChangeTime = DateTime.now();
var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now().add(const Duration(seconds: 30));
var _lastQueryTime = DateTime.now();
var _queryCount = 0;
var _exit = false;
@@ -253,10 +253,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
return body;
}
final _queryInterval = const Duration(seconds: 20);
var _queryInterval = const Duration(seconds: 20);
void _startCheckOnlines() {
() async {
final p = await bind.mainIsUsingPublicServer();
if (!p) {
_queryInterval = const Duration(seconds: 6);
}
while (!_exit) {
final now = DateTime.now();
if (!setEquals(_curPeers, _lastQueryPeers)) {
@@ -264,7 +268,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
_queryOnlines(false);
}
} else {
if (_queryCount < _maxQueryCount) {
if (_queryCount < _maxQueryCount || !p) {
if (now.difference(_lastQueryTime) >= _queryInterval) {
if (_curPeers.isNotEmpty) {
bind.queryOnlines(ids: _curPeers.toList(growable: false));

View File

@@ -69,6 +69,8 @@ class RawTouchGestureDetectorRegion extends StatefulWidget {
class _RawTouchGestureDetectorRegionState
extends State<RawTouchGestureDetectorRegion> {
Offset _cacheLongPressPosition = Offset(0, 0);
// Timestamp of the last long press event.
int _cacheLongPressPositionTs = 0;
double _mouseScrollIntegral = 0; // mouse scroll speed controller
double _scale = 1;
@@ -95,8 +97,9 @@ class _RawTouchGestureDetectorRegionState
}
if (handleTouch) {
// Desktop or mobile "Touch mode"
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
inputModel.tapDown(MouseButtons.left);
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
inputModel.tapDown(MouseButtons.left);
}
}
}
@@ -105,8 +108,9 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (handleTouch) {
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
inputModel.tapUp(MouseButtons.left);
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
inputModel.tapUp(MouseButtons.left);
}
}
}
@@ -134,6 +138,9 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
return;
}
inputModel.tap(MouseButtons.left);
inputModel.tap(MouseButtons.left);
}
@@ -146,6 +153,7 @@ class _RawTouchGestureDetectorRegionState
if (handleTouch) {
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
_cacheLongPressPosition = d.localPosition;
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
}
}
@@ -222,9 +230,22 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (handleTouch) {
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return;
}
if (isDesktop) {
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.
//
// 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
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
}
inputModel.sendMouse('down', MouseButtons.left);
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
} else {
@@ -244,6 +265,9 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return;
}
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
}
@@ -281,7 +305,7 @@ class _RawTouchGestureDetectorRegionState
}
} else {
// mobile
ffi.canvasModel.updateScale(d.scale / _scale);
ffi.canvasModel.updateScale(d.scale / _scale, d.focalPoint);
_scale = d.scale;
ffi.canvasModel.panX(d.focalPointDelta.dx);
ffi.canvasModel.panY(d.focalPointDelta.dy);

View File

@@ -234,12 +234,12 @@ List<(String, String)> otherDefaultSettings() {
('True color (4:4:4)', kOptionI444),
('Reverse mouse wheel', kKeyReverseMouseWheel),
('swap-left-right-mouse', kOptionSwapLeftRightMouse),
if (isDesktop && bind.mainGetUseTextureRender())
if (isDesktop)
(
'Show displays as individual windows',
kKeyShowDisplaysAsIndividualWindows
),
if (isDesktop && bind.mainGetUseTextureRender())
if (isDesktop)
(
'Use all my displays for the remote session',
kKeyUseAllMyDisplaysForTheRemoteSession

View File

@@ -6,6 +6,7 @@ import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
@@ -22,6 +23,20 @@ class TTextMenu {
required this.onPressed,
this.trailingIcon,
this.divider = false});
Widget getChild() {
if (trailingIcon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
child,
trailingIcon!,
],
);
} else {
return child;
}
}
}
class TRadioMenu<T> {
@@ -583,8 +598,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Lock after session end'))));
}
if (bind.mainGetUseTextureRender() &&
pi.isSupportMultiDisplay &&
if (pi.isSupportMultiDisplay &&
PrivacyModeState.find(id).isEmpty &&
pi.displaysCount.value > 1 &&
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
@@ -596,15 +610,13 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
onChanged: (value) {
if (value == null) return;
bind.sessionSetDisplaysAsIndividualWindows(
sessionId: sessionId, value: value ? 'Y' : '');
sessionId: sessionId, value: value ? 'Y' : 'N');
},
child: Text(translate('Show displays as individual windows'))));
}
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
if (bind.mainGetUseTextureRender() &&
pi.isSupportMultiDisplay &&
isMultiScreens) {
if (pi.isSupportMultiDisplay && isMultiScreens) {
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
sessionId: ffi.sessionId) ==
'Y';
@@ -613,7 +625,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
onChanged: (value) {
if (value == null) return;
bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
sessionId: sessionId, value: value ? 'Y' : '');
sessionId: sessionId, value: value ? 'Y' : 'N');
},
child: Text(translate('Use all my displays for the remote session'))));
}
@@ -639,6 +651,18 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
v.addAll(toolbarKeyboardToggles(ffi));
}
// view mode (mobile only, desktop is in keyboard menu)
if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
v.add(TToggleMenu(
value: ffiModel.viewOnly,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, value);
},
child: Text(translate('View Mode'))));
}
return v;
}
@@ -779,3 +803,106 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
}
return v;
}
bool showVirtualDisplayMenu(FFI ffi) {
if (ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
return false;
}
if (!ffi.ffiModel.pi.isInstalled) {
return false;
}
if (ffi.ffiModel.pi.isRustDeskIdd || ffi.ffiModel.pi.isAmyuniIdd) {
return true;
}
return false;
}
List<Widget> getVirtualDisplayMenuChildren(
FFI ffi, String id, VoidCallback? clickCallBack) {
if (!showVirtualDisplayMenu(ffi)) {
return [];
}
final pi = ffi.ffiModel.pi;
final privacyModeState = PrivacyModeState.find(id);
if (pi.isRustDeskIdd) {
final virtualDisplays = ffi.ffiModel.pi.RustDeskVirtualDisplays;
final children = <Widget>[];
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
children.add(Obx(() => CkbMenuButton(
value: virtualDisplays.contains(i + 1),
onChanged: privacyModeState.isNotEmpty
? null
: (bool? value) async {
if (value != null) {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId, index: i + 1, on: value);
clickCallBack?.call();
}
},
child: Text('${translate('Virtual display')} ${i + 1}'),
ffi: ffi,
)));
}
children.add(Divider());
children.add(Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
clickCallBack?.call();
},
ffi: ffi,
child: Text(translate('Plug out all')),
)));
return children;
}
if (pi.isAmyuniIdd) {
final count = ffi.ffiModel.pi.amyuniVirtualDisplayCount;
final children = <Widget>[
Obx(() => Row(
children: [
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId, index: 0, on: false);
clickCallBack?.call();
},
child: Icon(Icons.remove),
),
Text(count.toString()),
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 4
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId, index: 0, on: true);
clickCallBack?.call();
},
child: Icon(Icons.add),
),
],
)),
Divider(),
Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
clickCallBack?.call();
},
ffi: ffi,
child: Text(translate('Plug out all')),
)),
];
return children;
}
return [];
}

View File

@@ -133,9 +133,28 @@ const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
const String kOptionEnableCheckUpdate = "enable-check-update";
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
const String kOptionStopService = "stop-service";
const String kOptionDirectxCapture = "enable-directx-capture";
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
// buildin opitons
const String kOptionHideServerSetting = "hide-server-settings";
const String kOptionHideProxySetting = "hide-proxy-settings";
const String kOptionHideSecuritySetting = "hide-security-settings";
const String kOptionHideNetworkSetting = "hide-network-settings";
const String kOptionRemovePresetPasswordWarning =
"remove-preset-password-warning";
const kHideUsernameOnCard = "hide-username-on-card";
const String kOptionHideHelpCards = "hide-help-cards";
const String kOptionToggleViewOnly = "view-only";
const String kOptionDisableFloatingWindow = "disable-floating-window";
const String kOptionKeepScreenOn = "keep-screen-on";
const String kOptionShowMobileAction = "showMobileActions";
const String kUrlActionClose = "close";
const String kTabLabelHomePage = "Home";
@@ -147,6 +166,8 @@ const int kWindowMainId = 0;
const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse";
const String kKeyFlutterKey = "flutter_key";
const String kKeyShowDisplaysAsIndividualWindows =
'displays_as_individual_windows';
const String kKeyUseAllMyDisplaysForTheRemoteSession =

View File

@@ -340,7 +340,7 @@ class _ConnectionPageState extends State<ConnectionPage>
?.merge(TextStyle(height: 1)),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 0),
waitDuration: Duration(milliseconds: 300),
message: translate("id_input_tip"),
child: Icon(
Icons.help_outline_outlined,

View File

@@ -443,14 +443,14 @@ class _DesktopHomePageState extends State<DesktopHomePage>
});
}
} else if (isMacOS) {
if (!(bind.isOutgoingOnly() ||
bind.mainIsCanScreenRecording(prompt: false))) {
final isOutgoingOnly = bind.isOutgoingOnly();
if (!(isOutgoingOnly || bind.mainIsCanScreenRecording(prompt: false))) {
return buildInstallCard("Permissions", "config_screen", "Configure",
() async {
bind.mainIsCanScreenRecording(prompt: true);
watchIsCanScreenRecording = true;
}, help: 'Help', link: translate("doc_mac_permission"));
} else if (!bind.mainIsProcessTrusted(prompt: false)) {
} else if (!isOutgoingOnly && !bind.mainIsProcessTrusted(prompt: false)) {
return buildInstallCard("Permissions", "config_acc", "Configure",
() async {
bind.mainIsProcessTrusted(prompt: true);
@@ -462,7 +462,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
bind.mainIsCanInputMonitoring(prompt: true);
watchIsInputMonitoring = true;
}, help: 'Help', link: translate("doc_mac_permission"));
} else if (!svcStopped.value &&
} else if (!isOutgoingOnly &&
!svcStopped.value &&
bind.mainIsInstalled() &&
!bind.mainIsInstalledDaemon(prompt: false)) {
return buildInstallCard("", "install_daemon_tip", "Install", () async {
@@ -545,6 +546,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
String? link,
bool? closeButton,
String? closeOption}) {
if (bind.mainGetBuildinOption(key: kOptionHideHelpCards) == 'Y' &&
content != 'install_daemon_tip') {
return const SizedBox();
}
void closeCard() async {
if (closeOption != null) {
await bind.mainSetLocalOption(key: closeOption, value: 'N');
@@ -671,7 +676,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
systemError = error;
setState(() {});
}
final v = await bind.mainGetOption(key: "stop-service") == "Y";
final v = await mainGetBoolOption(kOptionStopService);
if (v != svcStopped.value) {
svcStopped.value = v;
setState(() {});
@@ -838,7 +843,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}
}
void setPasswordDialog() async {
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
final pw = await bind.mainGetPermanentPassword();
final p0 = TextEditingController(text: pw);
final p1 = TextEditingController(text: pw);
@@ -878,6 +883,9 @@ void setPasswordDialog() async {
return;
}
bind.mainSetPermanentPassword(password: pass);
if (pass.isNotEmpty) {
notEmptyCallback?.call();
}
close();
}

View File

@@ -61,9 +61,13 @@ class DesktopSettingPage extends StatefulWidget {
final SettingsTabKey initialTabkey;
static final List<SettingsTabKey> tabKeys = [
SettingsTabKey.general,
if (!bind.isOutgoingOnly() && !bind.isDisableSettings())
if (!bind.isOutgoingOnly() &&
!bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
SettingsTabKey.safety,
if (!bind.isDisableSettings()) SettingsTabKey.network,
if (!bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y')
SettingsTabKey.network,
if (!bind.isIncomingOnly()) SettingsTabKey.display,
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
SettingsTabKey.plugin,
@@ -84,8 +88,10 @@ class DesktopSettingPage extends StatefulWidget {
}
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
DesktopTabPage.onAddSetting(initialPage: page);
PageController controller = Get.find(tag: _kSettingPageControllerTag);
Rx<SettingsTabKey> selected = Get.find(tag: _kSettingPageTabKeyTag);
PageController controller =
Get.find<PageController>(tag: _kSettingPageControllerTag);
Rx<SettingsTabKey> selected =
Get.find<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag);
selected.value = page;
controller.jumpToPage(index);
} else {
@@ -171,16 +177,32 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
}
List<Widget> _children() {
final children = [
_General(),
if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) _Safety(),
if (!bind.isDisableSettings()) _Network(),
if (!bind.isIncomingOnly()) _Display(),
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
_Plugin(),
if (!bind.isDisableAccount()) _Account(),
_About(),
];
final children = List<Widget>.empty(growable: true);
for (final tab in DesktopSettingPage.tabKeys) {
switch (tab) {
case SettingsTabKey.general:
children.add(const _General());
break;
case SettingsTabKey.safety:
children.add(const _Safety());
break;
case SettingsTabKey.network:
children.add(const _Network());
break;
case SettingsTabKey.display:
children.add(const _Display());
break;
case SettingsTabKey.plugin:
children.add(const _Plugin());
break;
case SettingsTabKey.account:
children.add(const _Account());
break;
case SettingsTabKey.about:
children.add(const _About());
break;
}
}
return children;
}
@@ -315,11 +337,11 @@ class _GeneralState extends State<_General> {
children: [
service(),
theme(),
_Card(title: 'Language', children: [language()]),
hwcodec(),
audio(context),
record(context),
WaylandCard(),
_Card(title: 'Language', children: [language()]),
other()
],
).marginOnly(bottom: _kListViewBottomMargin));
@@ -413,6 +435,12 @@ class _GeneralState extends State<_General> {
'Check for software update on startup',
kOptionEnableCheckUpdate,
isServer: false,
),
if (isWindows && !bind.isOutgoingOnly())
_OptionCheckBox(
context,
'Capture screen using DirectX',
kOptionDirectxCapture,
)
],
];
@@ -487,7 +515,7 @@ class _GeneralState extends State<_General> {
return const Offstage();
}
return AudioInput(builder: (devices, currentDevice, setDevice) {
builder(devices, currentDevice, setDevice) {
return _Card(title: 'Audio Input Device', children: [
...devices.map((device) => _Radio<String>(context,
value: device,
@@ -498,15 +526,17 @@ class _GeneralState extends State<_General> {
setState(() {});
}))
]);
});
}
return AudioInput(builder: builder, isCm: false, isVoiceCall: false);
}
Widget record(BuildContext context) {
final showRootDir = isWindows && bind.mainIsInstalled();
return futureBuilder(future: () async {
String user_dir = await bind.mainVideoSaveDirectory(root: false);
String user_dir = bind.mainVideoSaveDirectory(root: false);
String root_dir =
showRootDir ? await bind.mainVideoSaveDirectory(root: true) : '';
showRootDir ? bind.mainVideoSaveDirectory(root: true) : '';
bool user_dir_exists = await Directory(user_dir).exists();
bool root_dir_exists =
showRootDir ? await Directory(root_dir).exists() : false;
@@ -673,15 +703,24 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
// Simple temp wrapper for PR check
tmpWrapper() {
RxBool has2fa = bind.mainHasValid2FaSync().obs;
RxBool hasBot = bind.mainHasValidBotSync().obs;
update() async {
has2fa.value = bind.mainHasValid2FaSync();
setState(() {});
}
onChanged(bool? checked) async {
change2fa(callback: update);
if (checked == false) {
CommonConfirmDialog(
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
change2fa(callback: update);
});
} else {
change2fa(callback: update);
}
}
return GestureDetector(
final tfa = GestureDetector(
child: InkWell(
child: Obx(() => Row(
children: [
@@ -702,6 +741,52 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
onChanged(!has2fa.value);
},
).marginOnly(left: _kCheckBoxLeftMargin);
if (!has2fa.value) {
return tfa;
}
updateBot() async {
hasBot.value = bind.mainHasValidBotSync();
setState(() {});
}
onChangedBot(bool? checked) async {
if (checked == false) {
CommonConfirmDialog(
gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
changeBot(callback: updateBot);
});
} else {
changeBot(callback: updateBot);
}
}
final bot = GestureDetector(
child: Tooltip(
waitDuration: Duration(milliseconds: 300),
message: translate("enable-bot-tip"),
child: InkWell(
child: Obx(() => Row(
children: [
Checkbox(
value: hasBot.value,
onChanged: enabled ? onChangedBot : null)
.marginOnly(right: 5),
Expanded(
child: Text(
translate('Telegram bot'),
style: TextStyle(
color: disabledTextColor(context, enabled)),
))
],
))),
),
onTap: () {
onChangedBot(!hasBot.value);
},
).marginOnly(left: _kCheckBoxLeftMargin + 30);
return Column(
children: [tfa, bot],
);
}
return tmpWrapper();
@@ -826,12 +911,22 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
label: value,
onChanged: locked
? null
: ((value) {
() async {
: ((value) async {
callback() async {
await model.setVerificationMethod(
passwordKeys[passwordValues.indexOf(value)]);
await model.updatePasswordModel();
}();
}
if (value ==
passwordValues[passwordKeys
.indexOf(kUsePermanentPassword)] &&
(await bind.mainGetPermanentPassword())
.isEmpty) {
setPasswordDialog(notEmptyCallback: callback);
} else {
await callback();
}
}),
))
.toList();
@@ -1024,12 +1119,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
bool enabled = !locked;
// Simple temp wrapper for PR check
tmpWrapper() {
RxBool hasWhitelist = (bind.mainGetOptionSync(key: kOptionWhitelist) !=
defaultOptionWhitelist)
.obs;
RxBool hasWhitelist = whitelistNotEmpty().obs;
update() async {
hasWhitelist.value = bind.mainGetOptionSync(key: kOptionWhitelist) !=
defaultOptionWhitelist;
hasWhitelist.value = whitelistNotEmpty();
}
onChanged(bool? checked) async {
@@ -1140,7 +1232,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
width: 95,
child: TextField(
controller: controller,
enabled: enabled && !locked && isOptFixed,
enabled: enabled && !locked && !isOptFixed,
onChanged: (_) => applyEnabled.value = true,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(
@@ -1193,6 +1285,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
super.build(context);
bool enabled = !locked;
final scrollController = ScrollController();
final hideServer =
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
final hideProxy =
bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
@@ -1206,11 +1302,12 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
AbsorbPointer(
absorbing: locked,
child: Column(children: [
server(enabled),
_Card(title: 'Proxy', children: [
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
enabled: enabled),
]),
if (!hideServer) server(enabled),
if (!hideProxy)
_Card(title: 'Proxy', children: [
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
enabled: enabled),
]),
]),
),
]).marginOnly(bottom: _kListViewBottomMargin));
@@ -1709,7 +1806,7 @@ class _AboutState extends State<_About> {
child: SingleChildScrollView(
controller: scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
child: _Card(title: '${translate('About')} RustDesk', children: [
child: _Card(title: translate('About RustDesk'), children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -2243,35 +2340,40 @@ void changeSocks5Proxy() async {
children: [
Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Align(
alignment: Alignment.centerRight,
child: Row(
children: [
Text(
translate('Server'),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 0),
message: translate("default_proxy_tip"),
child: Icon(
Icons.help_outline_outlined,
size: 16,
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5),
if (!isMobile)
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Align(
alignment: Alignment.centerRight,
child: Row(
children: [
Text(
translate('Server'),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 0),
message: translate("default_proxy_tip"),
child: Icon(
Icons.help_outline_outlined,
size: 16,
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5),
),
),
),
],
)).marginOnly(right: 10),
),
],
)).marginOnly(right: 10),
),
Expanded(
child: TextField(
decoration: InputDecoration(
errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
labelText: isMobile ? translate('Server') : null,
helperText:
isMobile ? translate("default_proxy_tip") : null,
helperMaxLines: isMobile ? 3 : null,
),
controller: proxyController,
autofocus: true,
@@ -2282,15 +2384,19 @@ void changeSocks5Proxy() async {
).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
'${translate("Username")}:',
textAlign: TextAlign.right,
).marginOnly(right: 10)),
if (!isMobile)
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
'${translate("Username")}:',
textAlign: TextAlign.right,
).marginOnly(right: 10)),
Expanded(
child: TextField(
controller: userController,
decoration: InputDecoration(
labelText: isMobile ? translate('Username') : null,
),
enabled: !isOptFixed,
),
),
@@ -2298,16 +2404,18 @@ void changeSocks5Proxy() async {
).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
'${translate("Password")}:',
textAlign: TextAlign.right,
).marginOnly(right: 10)),
if (!isMobile)
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
'${translate("Password")}:',
textAlign: TextAlign.right,
).marginOnly(right: 10)),
Expanded(
child: Obx(() => TextField(
obscureText: obscure.value,
decoration: InputDecoration(
labelText: isMobile ? translate('Password') : null,
suffixIcon: IconButton(
onPressed: () => obscure.value = !obscure.value,
icon: Icon(obscure.value

View File

@@ -8,6 +8,7 @@ import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
// import 'package:flutter/services.dart';
import '../../common/shared_state.dart';
@@ -20,7 +21,7 @@ class DesktopTabPage extends StatefulWidget {
static void onAddSetting(
{SettingsTabKey initialPage = SettingsTabKey.general}) {
try {
DesktopTabController tabController = Get.find();
DesktopTabController tabController = Get.find<DesktopTabController>();
tabController.add(TabInfo(
key: kTabLabelSettingPage,
label: kTabLabelSettingPage,
@@ -36,12 +37,26 @@ class DesktopTabPage extends StatefulWidget {
}
}
class _DesktopTabPageState extends State<DesktopTabPage> {
class _DesktopTabPageState extends State<DesktopTabPage>
with WidgetsBindingObserver {
final tabController = DesktopTabController(tabType: DesktopTabType.main);
final RxBool _block = false.obs;
// bool mouseIn = false;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
shouldBeBlocked(_block, canBeBlocked);
} else if (state == AppLifecycleState.inactive) {}
}
@override
void initState() {
super.initState();
// HardwareKeyboard.instance.addHandler(_handleKeyEvent);
WidgetsBinding.instance.addObserver(this);
Get.put<DesktopTabController>(tabController);
RemoteCountState.init();
tabController.add(TabInfo(
@@ -66,10 +81,23 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
}
}
/*
bool _handleKeyEvent(KeyEvent event) {
if (!mouseIn && event is KeyDownEvent) {
print('key down: ${event.logicalKey}');
shouldBeBlocked(_block, canBeBlocked);
}
return false; // allow it to propagate
}
*/
@override
void dispose() {
super.dispose();
// HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
WidgetsBinding.instance.removeObserver(this);
Get.delete<DesktopTabController>();
super.dispose();
}
@override
@@ -88,6 +116,7 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
isClose: false,
),
),
blockTab: _block,
)));
return isMacOS || kUseCompatibleUiMode
? tabWidget

View File

@@ -92,7 +92,7 @@ class _FileManagerPageState extends State<FileManagerPage>
_ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
});
Get.put(_ffi, tag: 'ft_${widget.id}');
Get.put<FFI>(_ffi, tag: 'ft_${widget.id}');
if (!isLinux) {
WakelockPlus.enable();
}

View File

@@ -63,7 +63,7 @@ class _PortForwardPageState extends State<PortForwardPage>
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay,
isRdp: widget.isRDP);
Get.put(_ffi, tag: 'pf_${widget.id}');
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
debugPrint("Port forward page init success with id ${widget.id}");
widget.tabController.onSelected?.call(widget.id);
}
@@ -141,8 +141,9 @@ class _PortForwardPageState extends State<PortForwardPage>
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
return Theme(
data: Theme.of(context)
.copyWith(backgroundColor: Theme.of(context).colorScheme.background),
data: Theme.of(context).copyWith(
colorScheme: Theme.of(context).colorScheme,
),
child: Obx(() => ListView.builder(
controller: ScrollController(),
itemCount: pfs.length + 2,
@@ -289,7 +290,7 @@ class _PortForwardPageState extends State<PortForwardPage>
).marginOnly(left: _kTextLeftMargin));
return Theme(
data: Theme.of(context)
.copyWith(backgroundColor: Theme.of(context).colorScheme.background),
.copyWith(colorScheme: Theme.of(context).colorScheme),
child: ListView.builder(
controller: ScrollController(),
itemCount: 2,

View File

@@ -85,6 +85,9 @@ class _RemotePageState extends State<RemotePage>
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
// to identify the toolbar instance and its callback function.
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
Function(bool)? _onEnterOrLeaveImage4Toolbar;
late FFI _ffi;
@@ -104,7 +107,7 @@ class _RemotePageState extends State<RemotePage>
super.initState();
_initStates(widget.id);
_ffi = FFI(widget.sessionId);
Get.put(_ffi, tag: widget.id);
Get.put<FFI>(_ffi, tag: widget.id);
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
@@ -131,6 +134,7 @@ class _RemotePageState extends State<RemotePage>
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
_ffi.dialogManager.loadMobileActionsOverlayVisible();
// Session option should be set after models.dart/FFI.start
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: 'show-remote-cursor');
@@ -268,9 +272,18 @@ class _RemotePageState extends State<RemotePage>
id: widget.id,
ffi: _ffi,
state: widget.toolbarState,
onEnterOrLeaveImageSetter: (func) =>
_onEnterOrLeaveImage4Toolbar = func,
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
onEnterOrLeaveImageSetter: (id, func) {
_instanceIdOnEnterOrLeaveImage4Toolbar = id;
_onEnterOrLeaveImage4Toolbar = func;
},
onEnterOrLeaveImageCleaner: (id) {
// If _instanceIdOnEnterOrLeaveImage4Toolbar != id
// it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
_instanceIdOnEnterOrLeaveImage4Toolbar = null;
_onEnterOrLeaveImage4Toolbar = null;
}
},
setRemoteState: setState,
);
@@ -310,22 +323,13 @@ class _RemotePageState extends State<RemotePage>
if (!_ffi.ffiModel.isPeerAndroid) {
return Offstage();
} else {
if (_ffi.connType == ConnType.defaultConn &&
_ffi.ffiModel.permissions['keyboard'] != false) {
Timer(
Duration(milliseconds: 10),
() => _ffi.dialogManager
.mobileActionsOverlayVisible.value = true);
}
return Obx(() => Offstage(
offstage: _ffi.dialogManager
.mobileActionsOverlayVisible.isFalse,
child: Overlay(initialEntries: [
makeMobileActionsOverlayEntry(
() => _ffi
.dialogManager
.mobileActionsOverlayVisible
.value = false,
() => _ffi.dialogManager
.setMobileActionsOverlayVisible(false),
ffi: _ffi,
)
]),
@@ -484,6 +488,7 @@ class _RemotePageState extends State<RemotePage>
() => _ffi.ffiModel.pi.isSet.isFalse
? Container(color: Colors.transparent)
: Obx(() {
widget.toolbarState.initShow(sessionId);
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
return ImagePaint(
id: widget.id,
@@ -501,12 +506,13 @@ class _RemotePageState extends State<RemotePage>
];
if (!_ffi.canvasModel.cursorEmbedded) {
paints.add(Obx(() => Offstage(
offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse,
child: CursorPaint(
id: widget.id,
zoomCursor: _zoomCursor,
))));
paints
.add(Obx(() => _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse
? Offstage()
: CursorPaint(
id: widget.id,
zoomCursor: _zoomCursor,
)));
}
paints.add(
Positioned(
@@ -617,10 +623,11 @@ class _ImagePaintState extends State<ImagePaint> {
final paintWidth = c.getDisplayWidth() * s;
final paintHeight = c.getDisplayHeight() * s;
final paintSize = Size(paintWidth, paintHeight);
final paintWidget = m.useTextureRender
? _BuildPaintTextureRender(
c, s, Offset.zero, paintSize, isViewOriginal())
: _buildScrollbarNonTextureRender(m, paintSize, s);
final paintWidget =
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
? _BuildPaintTextureRender(
c, s, Offset.zero, paintSize, isViewOriginal())
: _buildScrollbarNonTextureRender(m, paintSize, s);
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
c.updateScrollPercent();
@@ -638,17 +645,18 @@ class _ImagePaintState extends State<ImagePaint> {
));
} else {
if (c.size.width > 0 && c.size.height > 0) {
final paintWidget = m.useTextureRender
? _BuildPaintTextureRender(
c,
s,
Offset(
isLinux ? c.x.toInt().toDouble() : c.x,
isLinux ? c.y.toInt().toDouble() : c.y,
),
c.size,
isViewOriginal())
: _buildScrollAuthNonTextureRender(m, c, s);
final paintWidget =
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
? _BuildPaintTextureRender(
c,
s,
Offset(
isLinux ? c.x.toInt().toDouble() : c.x,
isLinux ? c.y.toInt().toDouble() : c.y,
),
c.size,
isViewOriginal())
: _buildScrollAutoNonTextureRender(m, c, s);
return mouseRegion(child: _buildListener(paintWidget));
} else {
return Container();
@@ -664,7 +672,7 @@ class _ImagePaintState extends State<ImagePaint> {
);
}
Widget _buildScrollAuthNonTextureRender(
Widget _buildScrollAutoNonTextureRender(
ImageModel m, CanvasModel c, double s) {
return CustomPaint(
size: Size(c.size.width, c.size.height),

View File

@@ -46,7 +46,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
static const IconData selectedIcon = Icons.desktop_windows_sharp;
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
late ToolbarState _toolbarState;
String? peerId;
bool _isScreenRectSet = false;
int? _display;
@@ -54,7 +53,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
var connectionMap = RxList<Widget>.empty(growable: true);
_ConnectionTabPageState(Map<String, dynamic> params) {
_toolbarState = ToolbarState();
RemoteCountState.init();
peerId = params['id'];
final sessionId = params['session_id'];
@@ -91,7 +89,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
display: display,
displays: displays?.cast<int>(),
password: params['password'],
toolbarState: _toolbarState,
toolbarState: ToolbarState(),
tabController: tabController,
switchUuid: params['switch_uuid'],
forceRelay: params['forceRelay'],
@@ -126,7 +124,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
@override
void dispose() {
super.dispose();
_toolbarState.save();
}
@override
@@ -251,15 +248,16 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final pi = ffi.ffiModel.pi;
final perms = ffi.ffiModel.permissions;
final sessionId = ffi.sessionId;
final toolbarState = remotePage.toolbarState;
menu.addAll([
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Obx(() => Text(
translate(
_toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
style: style,
)),
proc: () {
_toolbarState.switchShow();
toolbarState.switchShow(sessionId);
cancelFunc();
},
padding: padding,
@@ -420,14 +418,12 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
await WindowController.fromWindowId(windowId()).setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
}
await setNewConnectWindowFrame(windowId(), id!, screenRect);
await setNewConnectWindowFrame(windowId(), id!, display, screenRect);
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
await windowOnTop(windowId());
});
});
ConnectionTypeState.init(id);
_toolbarState.setShow(
bind.mainGetUserDefaultOption(key: kOptionCollapseToolbar) != 'Y');
tabController.add(TabInfo(
key: id,
label: id,
@@ -442,7 +438,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
display: display,
displays: displays?.cast<int>(),
password: args['password'],
toolbarState: _toolbarState,
toolbarState: ToolbarState(),
tabController: tabController,
switchUuid: switchUuid,
forceRelay: args['forceRelay'],

View File

@@ -36,7 +36,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
void initState() {
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
windowManager.addListener(this);
Get.put(tabController);
Get.put<DesktopTabController>(tabController);
tabController.onRemoved = (_, id) {
onRemoveId(id);
};
@@ -104,7 +104,20 @@ class ConnectionManager extends StatefulWidget {
State<StatefulWidget> createState() => ConnectionManagerState();
}
class ConnectionManagerState extends State<ConnectionManager> {
class ConnectionManagerState extends State<ConnectionManager>
with WidgetsBindingObserver {
final RxBool _block = false.obs;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
if (!allowRemoteCMModification()) {
shouldBeBlocked(_block, null);
}
}
}
@override
void initState() {
gFFI.serverModel.updateClientState();
@@ -127,9 +140,16 @@ class ConnectionManagerState extends State<ConnectionManager> {
}
};
gFFI.chatModel.isConnManager = true;
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
final serverModel = Provider.of<ServerModel>(context);
@@ -165,6 +185,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
selectedBorderColor: MyTheme.accent,
maxLabelWidth: 100,
tail: null, //buildScrollJumper(),
blockTab: allowRemoteCMModification() ? null : _block,
selectedTabBackgroundColor:
Theme.of(context).hintColor.withOpacity(0),
tabBuilder: (key, icon, label, themeConf) {
@@ -207,15 +228,12 @@ class ConnectionManagerState extends State<ConnectionManager> {
Consumer<ChatModel>(
builder: (_, model, child) => SizedBox(
width: realChatPageWidth,
child: buildRemoteBlock(
child: Container(
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Theme.of(context)
.dividerColor))),
child: buildSidePage()),
),
child: allowRemoteCMModification()
? buildSidePage()
: buildRemoteBlock(
child: buildSidePage(),
block: _block,
mask: true),
)),
SizedBox(
width: realClosedWidth,
@@ -714,7 +732,7 @@ class _CmControlPanel extends StatelessWidget {
child: buildButton(context,
color: MyTheme.accent,
onClick: null, onTapDown: (details) async {
final devicesInfo = await AudioInput.getDevicesInfo();
final devicesInfo = await AudioInput.getDevicesInfo(true, true);
List<String> devices = devicesInfo['devices'] as List<String>;
if (devices.isEmpty) {
msgBox(
@@ -740,13 +758,13 @@ class _CmControlPanel extends StatelessWidget {
value: d,
height: 18,
padding: EdgeInsets.zero,
onTap: () => AudioInput.setDevice(d),
onTap: () => AudioInput.setDevice(d, true, true),
child: IgnorePointer(
child: RadioMenuButton(
value: d,
groupValue: currentDevice,
onChanged: (v) {
if (v != null) AudioInput.setDevice(v);
if (v != null) AudioInput.setDevice(v, true, true);
},
child: Container(
child: Text(
@@ -1043,6 +1061,10 @@ class _CmControlPanel extends StatelessWidget {
}
void checkClickTime(int id, Function() callback) async {
if (allowRemoteCMModification()) {
callback();
return;
}
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
await bind.cmCheckClickTime(connId: id);
Timer(const Duration(milliseconds: 120), () async {
@@ -1051,6 +1073,11 @@ void checkClickTime(int id, Function() callback) async {
});
}
bool allowRemoteCMModification() {
return option2bool(kOptionAllowRemoteCmModification,
bind.mainGetLocalOption(key: kOptionAllowRemoteCmModification));
}
class _FileTransferLogPage extends StatefulWidget {
_FileTransferLogPage({Key? key}) : super(key: key);

View File

@@ -271,7 +271,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
/// The text style of the popup menu item.
///
/// If this property is null, then [PopupMenuThemeData.textStyle] is used.
/// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.subtitle1]
/// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.titleMedium]
/// of [ThemeData.textTheme] is used.
final TextStyle? textStyle;
@@ -352,7 +352,7 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
TextStyle style = widget.textStyle ??
popupMenuTheme.textStyle ??
theme.textTheme.subtitle1!;
theme.textTheme.titleMedium!;
if (!widget.enabled) style = style.copyWith(color: theme.disabledColor);

View File

@@ -65,7 +65,7 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
TextStyle style = widget.textStyle ??
popupMenuTheme.textStyle ??
theme.textTheme.subtitle1!;
theme.textTheme.titleMedium!;
return Obx(() => mod_menu.PopupMenuButton<T>(
enabled: enabled.value,
position: widget.position,

View File

@@ -26,45 +26,42 @@ import './popup_menu.dart';
import './kb_layout_type_chooser.dart';
class ToolbarState {
late RxBool show;
late RxBool _pin;
bool isShowInited = false;
RxBool show = false.obs;
ToolbarState() {
_pin = RxBool(false);
final s = bind.getLocalFlutterOption(k: kOptionRemoteMenubarState);
if (s.isEmpty) {
_initSet(false, false);
return;
}
try {
final m = jsonDecode(s);
if (m == null) {
_initSet(false, false);
} else {
_initSet(m['pin'] ?? false, m['pin'] ?? false);
if (m != null) {
_pin = RxBool(m['pin'] ?? false);
}
} catch (e) {
debugPrint('Failed to decode toolbar state ${e.toString()}');
_initSet(false, false);
}
}
_initSet(bool s, bool p) {
// Show remubar when connection is established.
show = RxBool(
bind.mainGetUserDefaultOption(key: kOptionCollapseToolbar) != 'Y');
_pin = RxBool(p);
}
bool get pin => _pin.value;
switchShow() async {
switchShow(SessionID sessionId) async {
bind.sessionToggleOption(
sessionId: sessionId, value: kOptionCollapseToolbar);
show.value = !show.value;
}
setShow(bool v) async {
if (show.value != v) {
show.value = v;
initShow(SessionID sessionId) async {
if (!isShowInited) {
show.value = !(await bind.sessionGetToggleOption(
sessionId: sessionId, arg: kOptionCollapseToolbar) ??
false);
isShowInited = true;
}
}
@@ -86,10 +83,6 @@ class ToolbarState {
bind.setLocalFlutterOption(
k: kOptionRemoteMenubarState, v: jsonEncode({'pin': _pin.value}));
}
save() async {
await _savePin();
}
}
class _ToolbarTheme {
@@ -332,8 +325,8 @@ class RemoteToolbar extends StatefulWidget {
final String id;
final FFI ffi;
final ToolbarState state;
final Function(Function(bool)) onEnterOrLeaveImageSetter;
final VoidCallback onEnterOrLeaveImageCleaner;
final Function(int, Function(bool)) onEnterOrLeaveImageSetter;
final Function(int) onEnterOrLeaveImageCleaner;
final Function(VoidCallback) setRemoteState;
RemoteToolbar({
@@ -393,7 +386,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
initialValue: 0,
);
widget.onEnterOrLeaveImageSetter((enter) {
widget.onEnterOrLeaveImageSetter(identityHashCode(this), (enter) {
if (enter) {
triggerAutoHide();
_isCursorOverImage = true;
@@ -413,12 +406,11 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
dispose() {
super.dispose();
widget.onEnterOrLeaveImageCleaner();
widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
}
@override
Widget build(BuildContext context) {
// No need to use future builder here.
return Align(
alignment: Alignment.topCenter,
child: Obx(() => show.value
@@ -447,7 +439,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
sessionId: widget.ffi.sessionId,
dragging: _dragging,
fractionX: _fractionX,
show: show,
toolbarState: widget.state,
setFullscreen: _setFullscreen,
setMinimize: _minimize,
borderRadius: borderRadius,
@@ -587,8 +579,8 @@ class _MobileActionMenu extends StatelessWidget {
return Obx(() => _IconMenuButton(
assetName: 'assets/actions_mobile.svg',
tooltip: 'Mobile Actions',
onPressed: () =>
ffi.dialogManager.mobileActionsOverlayVisible.toggle(),
onPressed: () => ffi.dialogManager.setMobileActionsOverlayVisible(
!ffi.dialogManager.mobileActionsOverlayVisible.value),
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
? _ToolbarTheme.blueColor
: _ToolbarTheme.inactiveColor,
@@ -643,15 +635,12 @@ class _MonitorMenu extends StatelessWidget {
}
Widget buildMonitorSubmenuWidget(BuildContext context) {
final m = Provider.of<ImageModel>(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(children: buildMonitorList(context, false)),
supportIndividualWindows && m.useTextureRender ? Divider() : Offstage(),
supportIndividualWindows && m.useTextureRender
? chooseDisplayBehavior()
: Offstage(),
supportIndividualWindows ? Divider() : Offstage(),
supportIndividualWindows ? chooseDisplayBehavior() : Offstage(),
],
);
}
@@ -665,7 +654,7 @@ class _MonitorMenu extends StatelessWidget {
onChanged: (value) async {
if (value == null) return;
await bind.sessionSetDisplaysAsIndividualWindows(
sessionId: ffi.sessionId, value: value ? 'Y' : '');
sessionId: ffi.sessionId, value: value ? 'Y' : 'N');
},
ffi: ffi,
child: Text(translate('Show displays as individual windows')));
@@ -737,10 +726,7 @@ class _MonitorMenu extends StatelessWidget {
for (int i = 0; i < pi.displays.length; i++) {
monitorList.add(buildMonitorButton(i));
}
final m = Provider.of<ImageModel>(context);
if (supportIndividualWindows &&
m.useTextureRender &&
pi.displays.length > 1) {
if (supportIndividualWindows && pi.displays.length > 1) {
monitorList.add(buildMonitorButton(kAllDisplayValue));
}
return monitorList;
@@ -824,7 +810,6 @@ class _MonitorMenu extends StatelessWidget {
RxInt display = CurrentDisplayState.find(id);
if (display.value != i) {
final isChooseDisplayToOpenInNewWindow = pi.isSupportMultiDisplay &&
bind.mainGetUseTextureRender() &&
bind.sessionGetDisplaysAsIndividualWindows(
sessionId: ffi.sessionId) ==
'Y';
@@ -1067,15 +1052,11 @@ class _DisplayMenuState extends State<_DisplayMenu> {
ffi: widget.ffi,
screenAdjustor: _screenAdjustor,
),
if (pi.isRustDeskIdd)
_RustDeskVirtualDisplayMenu(
id: widget.id,
ffi: widget.ffi,
),
if (pi.isAmyuniIdd)
_AmyuniVirtualDisplayMenu(
id: widget.id,
if (showVirtualDisplayMenu(ffi))
_SubmenuButton(
ffi: widget.ffi,
menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
child: Text(translate("Virtual display")),
),
cursorToggles(),
Divider(),
@@ -1574,155 +1555,6 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
}
}
class _RustDeskVirtualDisplayMenu extends StatefulWidget {
final String id;
final FFI ffi;
_RustDeskVirtualDisplayMenu({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
State<_RustDeskVirtualDisplayMenu> createState() =>
_RustDeskVirtualDisplayMenuState();
}
class _RustDeskVirtualDisplayMenuState
extends State<_RustDeskVirtualDisplayMenu> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
return Offstage();
}
if (!widget.ffi.ffiModel.pi.isInstalled) {
return Offstage();
}
final virtualDisplays = widget.ffi.ffiModel.pi.RustDeskVirtualDisplays;
final privacyModeState = PrivacyModeState.find(widget.id);
final children = <Widget>[];
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
children.add(Obx(() => CkbMenuButton(
value: virtualDisplays.contains(i + 1),
onChanged: privacyModeState.isNotEmpty
? null
: (bool? value) async {
if (value != null) {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: i + 1,
on: value);
}
},
child: Text('${translate('Virtual display')} ${i + 1}'),
ffi: widget.ffi,
)));
}
children.add(Divider());
children.add(Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
},
ffi: widget.ffi,
child: Text(translate('Plug out all')),
)));
return _SubmenuButton(
ffi: widget.ffi,
menuChildren: children,
child: Text(translate("Virtual display")),
);
}
}
class _AmyuniVirtualDisplayMenu extends StatefulWidget {
final String id;
final FFI ffi;
_AmyuniVirtualDisplayMenu({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
State<_AmyuniVirtualDisplayMenu> createState() =>
_AmiyuniVirtualDisplayMenuState();
}
class _AmiyuniVirtualDisplayMenuState extends State<_AmyuniVirtualDisplayMenu> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
return Offstage();
}
if (!widget.ffi.ffiModel.pi.isInstalled) {
return Offstage();
}
final count = widget.ffi.ffiModel.pi.amyuniVirtualDisplayCount;
final privacyModeState = PrivacyModeState.find(widget.id);
final children = <Widget>[
Obx(() => Row(
children: [
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () => bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId, index: 0, on: false),
child: Icon(Icons.remove),
),
Text(count.toString()),
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 4
? null
: () => bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId, index: 0, on: true),
child: Icon(Icons.add),
),
],
)),
Divider(),
Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
},
ffi: widget.ffi,
child: Text(translate('Plug out all')),
)),
];
return _SubmenuButton(
ffi: widget.ffi,
menuChildren: children,
child: Text(translate("Virtual display")),
);
}
}
class _KeyboardMenu extends StatelessWidget {
final String id;
final FFI ffi;
@@ -1756,6 +1588,7 @@ class _KeyboardMenu extends StatelessWidget {
viewMode(),
Divider(),
...toolbarToggles(),
...mobileActions(),
]);
}
@@ -1892,6 +1725,39 @@ class _KeyboardMenu extends StatelessWidget {
ffi: ffi,
child: Text(translate('View Mode')));
}
mobileActions() {
if (pi.platform != kPeerPlatformAndroid) return [];
final enabled = versionCmp(pi.version, '1.2.7') >= 0;
if (!enabled) return [];
return [
Divider(),
MenuButton(
child: Text(translate('Back')),
onPressed: () => ffi.inputModel.onMobileBack(),
ffi: ffi),
MenuButton(
child: Text(translate('Home')),
onPressed: () => ffi.inputModel.onMobileHome(),
ffi: ffi),
MenuButton(
child: Text(translate('Apps')),
onPressed: () => ffi.inputModel.onMobileApps(),
ffi: ffi),
MenuButton(
child: Text(translate('Volume up')),
onPressed: () => ffi.inputModel.onMobileVolumeUp(),
ffi: ffi),
MenuButton(
child: Text(translate('Volume down')),
onPressed: () => ffi.inputModel.onMobileVolumeDown(),
ffi: ffi),
MenuButton(
child: Text(translate('Power')),
onPressed: () => ffi.inputModel.onMobilePower(),
ffi: ffi),
];
}
}
class _ChatMenu extends StatefulWidget {
@@ -1965,28 +1831,31 @@ class _VoiceCallMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
menuChildrenGetter() {
final audioInput =
AudioInput(builder: (devices, currentDevice, setDevice) {
return Column(
children: devices
.map((d) => RdoMenuButton<String>(
child: Container(
child: Text(
d,
overflow: TextOverflow.ellipsis,
final audioInput = AudioInput(
builder: (devices, currentDevice, setDevice) {
return Column(
children: devices
.map((d) => RdoMenuButton<String>(
child: Container(
child: Text(
d,
overflow: TextOverflow.ellipsis,
),
constraints: BoxConstraints(maxWidth: 250),
),
constraints: BoxConstraints(maxWidth: 250),
),
value: d,
groupValue: currentDevice,
onChanged: (v) {
if (v != null) setDevice(v);
},
ffi: ffi,
))
.toList(),
);
});
value: d,
groupValue: currentDevice,
onChanged: (v) {
if (v != null) setDevice(v);
},
ffi: ffi,
))
.toList(),
);
},
isCm: false,
isVoiceCall: true,
);
return [
audioInput,
Divider(),
@@ -2351,7 +2220,7 @@ class _DraggableShowHide extends StatefulWidget {
final SessionID sessionId;
final RxDouble fractionX;
final RxBool dragging;
final RxBool show;
final ToolbarState toolbarState;
final BorderRadius borderRadius;
final Function(bool) setFullscreen;
@@ -2362,7 +2231,7 @@ class _DraggableShowHide extends StatefulWidget {
required this.sessionId,
required this.fractionX,
required this.dragging,
required this.show,
required this.toolbarState,
required this.setFullscreen,
required this.setMinimize,
required this.borderRadius,
@@ -2378,6 +2247,8 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
double left = 0.0;
double right = 1.0;
RxBool get show => widget.toolbarState.show;
@override
initState() {
super.initState();
@@ -2465,28 +2336,29 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
),
),
)),
if (!isMacOS) Obx(() => Offstage(
offstage: isFullscreen.isFalse,
child: TextButton(
onPressed: () => widget.setMinimize(),
child: Tooltip(
message: translate('Minimize'),
child: Icon(
Icons.remove,
size: iconSize,
if (!isMacOS)
Obx(() => Offstage(
offstage: isFullscreen.isFalse,
child: TextButton(
onPressed: () => widget.setMinimize(),
child: Tooltip(
message: translate('Minimize'),
child: Icon(
Icons.remove,
size: iconSize,
),
),
),
),
)),
)),
TextButton(
onPressed: () => setState(() {
widget.show.value = !widget.show.value;
widget.toolbarState.switchShow(widget.sessionId);
}),
child: Obx((() => Tooltip(
message: translate(
widget.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
message:
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
child: Icon(
widget.show.isTrue ? Icons.expand_less : Icons.expand_more,
show.isTrue ? Icons.expand_less : Icons.expand_more,
size: iconSize,
),
))),

View File

@@ -230,8 +230,7 @@ typedef LabelGetter = Rx<String> Function(String key);
int _lastClickTime =
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
// ignore: must_be_immutable
class DesktopTab extends StatelessWidget {
class DesktopTab extends StatefulWidget {
final bool showLogo;
final bool showTitle;
final bool showMinimize;
@@ -248,15 +247,12 @@ class DesktopTab extends StatelessWidget {
final Color? selectedTabBackgroundColor;
final Color? unSelectedTabBackgroundColor;
final Color? selectedBorderColor;
final RxBool? blockTab;
final DesktopTabController controller;
Rx<DesktopTabState> get state => controller.state;
final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
late final DesktopTabType tabType;
late final bool isMainWindow;
final RxList<String> invisibleTabKeys = RxList.empty();
DesktopTab({
@@ -277,25 +273,240 @@ class DesktopTab extends StatelessWidget {
this.selectedTabBackgroundColor,
this.unSelectedTabBackgroundColor,
this.selectedBorderColor,
}) : super(key: key) {
tabType = controller.tabType;
isMainWindow = tabType == DesktopTabType.main ||
tabType == DesktopTabType.cm ||
tabType == DesktopTabType.install;
}
this.blockTab,
}) : super(key: key);
static RxString tablabelGetter(String peerId) {
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
return RxString(getDesktopTabLabel(peerId, alias));
}
@override
State<DesktopTab> createState() {
return _DesktopTabState();
}
}
// ignore: must_be_immutable
class _DesktopTabState extends State<DesktopTab>
with MultiWindowListener, WindowListener {
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
Timer? _macOSCheckRestoreTimer;
int _macOSCheckRestoreCounter = 0;
bool get showLogo => widget.showLogo;
bool get showTitle => widget.showTitle;
bool get showMinimize => widget.showMinimize;
bool get showMaximize => widget.showMaximize;
bool get showClose => widget.showClose;
Widget Function(Widget pageView)? get pageViewBuilder =>
widget.pageViewBuilder;
TabMenuBuilder? get tabMenuBuilder => widget.tabMenuBuilder;
Widget? get tail => widget.tail;
Future<bool> Function()? get onWindowCloseButton =>
widget.onWindowCloseButton;
TabBuilder? get tabBuilder => widget.tabBuilder;
LabelGetter? get labelGetter => widget.labelGetter;
double? get maxLabelWidth => widget.maxLabelWidth;
Color? get selectedTabBackgroundColor => widget.selectedTabBackgroundColor;
Color? get unSelectedTabBackgroundColor =>
widget.unSelectedTabBackgroundColor;
Color? get selectedBorderColor => widget.selectedBorderColor;
RxBool? get blockTab => widget.blockTab;
DesktopTabController get controller => widget.controller;
RxList<String> get invisibleTabKeys => widget.invisibleTabKeys;
Debouncer get _scrollDebounce => widget._scrollDebounce;
Rx<DesktopTabState> get state => controller.state;
DesktopTabType get tabType => controller.tabType;
bool get isMainWindow =>
tabType == DesktopTabType.main ||
tabType == DesktopTabType.cm ||
tabType == DesktopTabType.install;
_DesktopTabState() : super();
static RxString tablabelGetter(String peerId) {
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
return RxString(getDesktopTabLabel(peerId, alias));
}
@override
void initState() {
super.initState();
DesktopMultiWindow.addListener(this);
windowManager.addListener(this);
Future.delayed(Duration(milliseconds: 500), () {
if (isMainWindow) {
windowManager.isMaximized().then((maximized) {
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
} else {
final wc = WindowController.fromWindowId(kWindowId!);
wc.isMaximized().then((maximized) {
debugPrint("isMaximized $maximized");
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
}
});
}
@override
void dispose() {
DesktopMultiWindow.removeListener(this);
windowManager.removeListener(this);
_macOSCheckRestoreTimer?.cancel();
super.dispose();
}
void _setMaximized(bool maximize) {
stateGlobal.setMaximized(maximize);
_saveFrameDebounce.call(_saveFrame);
setState(() {});
}
@override
void onWindowFocus() {
stateGlobal.isFocused.value = true;
}
@override
void onWindowBlur() {
stateGlobal.isFocused.value = false;
}
@override
void onWindowMinimize() {
stateGlobal.setMinimized(true);
stateGlobal.setMaximized(false);
super.onWindowMinimize();
}
@override
void onWindowMaximize() {
stateGlobal.setMinimized(false);
_setMaximized(true);
super.onWindowMaximize();
}
@override
void onWindowUnmaximize() {
stateGlobal.setMinimized(false);
_setMaximized(false);
super.onWindowUnmaximize();
}
_saveFrame() async {
if (tabType == DesktopTabType.main) {
await saveWindowPosition(WindowType.Main);
} else if (kWindowType != null && kWindowId != null) {
await saveWindowPosition(kWindowType!, windowId: kWindowId);
}
}
@override
void onWindowMoved() {
_saveFrameDebounce.call(_saveFrame);
super.onWindowMoved();
}
@override
void onWindowResized() {
_saveFrameDebounce.call(_saveFrame);
super.onWindowMoved();
}
@override
void onWindowClose() async {
mainWindowClose() async => await windowManager.hide();
notMainWindowClose(WindowController windowController) async {
if (controller.length != 0) {
debugPrint("close not empty multiwindow from taskbar");
if (isWindows) {
await windowController.show();
await windowController.focus();
final res = await onWindowCloseButton?.call() ?? true;
if (!res) return;
}
controller.clear();
}
await windowController.hide();
await rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
}
macOSWindowClose(
Future<bool> Function() checkFullscreen,
Future<void> Function() closeFunc,
) async {
_macOSCheckRestoreCounter = 0;
_macOSCheckRestoreTimer =
Timer.periodic(Duration(milliseconds: 30), (timer) async {
_macOSCheckRestoreCounter++;
if (!await checkFullscreen() || _macOSCheckRestoreCounter >= 30) {
_macOSCheckRestoreTimer?.cancel();
_macOSCheckRestoreTimer = null;
Timer(Duration(milliseconds: 700), () async => await closeFunc());
}
});
}
// hide window on close
if (isMainWindow) {
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
}
// macOS specific workaround, the window is not hiding when in fullscreen.
if (isMacOS && await windowManager.isFullScreen()) {
await windowManager.setFullScreen(false);
await macOSWindowClose(
() async => await windowManager.isFullScreen(),
mainWindowClose,
);
} else {
await mainWindowClose();
}
} else {
// it's safe to hide the subwindow
final controller = WindowController.fromWindowId(kWindowId!);
if (isMacOS) {
// onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
// use ??= to make sure the value is set on first call.
if (await onWindowCloseButton?.call() ?? true) {
if (await controller.isFullScreen()) {
await controller.setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
await macOSWindowClose(
() async => await controller.isFullScreen(),
() async => await notMainWindowClose(controller),
);
} else {
await notMainWindowClose(controller);
}
}
} else {
await notMainWindowClose(controller);
}
}
super.onWindowClose();
}
@override
Widget build(BuildContext context) {
return Column(children: [
Obx(() => Offstage(
offstage: !stateGlobal.showTabBar.isTrue ||
(kUseCompatibleUiMode && isHideSingleItem()),
child: SizedBox(
Obx(() {
if (stateGlobal.showTabBar.isTrue &&
!(kUseCompatibleUiMode && isHideSingleItem())) {
return SizedBox(
height: _kTabBarHeight,
child: Column(
children: [
@@ -308,7 +519,11 @@ class DesktopTab extends StatelessWidget {
),
],
),
))),
);
} else {
return Offstage();
}
}),
Expanded(
child: pageViewBuilder != null
? pageViewBuilder!(_buildPageView())
@@ -317,19 +532,15 @@ class DesktopTab extends StatelessWidget {
}
Widget _buildBlock({required Widget child}) {
if (tabType != DesktopTabType.main) {
if (blockTab != null) {
return buildRemoteBlock(
child: child,
block: blockTab!,
use: canBeBlocked,
mask: tabType == DesktopTabType.main);
} else {
return child;
}
return buildRemoteBlock(
child: child,
use: () async {
var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
var option = option2bool(
kOptionAllowRemoteConfigModification,
await bind.mainGetOption(
key: kOptionAllowRemoteConfigModification));
return access_mode == 'view' || (access_mode.isEmpty && !option);
});
}
List<Widget> _tabWidgets = [];
@@ -397,6 +608,8 @@ class DesktopTab extends StatelessWidget {
: null,
onPanStart: (_) => startDragging(isMainWindow),
onPanCancel: () {
// We want to disable dragging of the tab area in the tab bar.
// Disable dragging is needed because macOS handles dragging by default.
if (isMacOS) {
setMovable(isMainWindow, false);
}
@@ -464,7 +677,6 @@ class DesktopTab extends StatelessWidget {
// hide simulated action buttons when we in compatible ui mode, because of reusing system title bar.
WindowActionPanel(
isMainWindow: isMainWindow,
tabType: tabType,
state: state,
tabController: controller,
invisibleTabKeys: invisibleTabKeys,
@@ -482,7 +694,6 @@ class DesktopTab extends StatelessWidget {
class WindowActionPanel extends StatefulWidget {
final bool isMainWindow;
final DesktopTabType tabType;
final Rx<DesktopTabState> state;
final DesktopTabController tabController;
@@ -498,7 +709,6 @@ class WindowActionPanel extends StatefulWidget {
const WindowActionPanel(
{Key? key,
required this.isMainWindow,
required this.tabType,
required this.state,
required this.tabController,
required this.invisibleTabKeys,
@@ -516,180 +726,17 @@ class WindowActionPanel extends StatefulWidget {
}
}
class WindowActionPanelState extends State<WindowActionPanel>
with MultiWindowListener, WindowListener {
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
Timer? _macOSCheckRestoreTimer;
int _macOSCheckRestoreCounter = 0;
class WindowActionPanelState extends State<WindowActionPanel> {
@override
void initState() {
super.initState();
DesktopMultiWindow.addListener(this);
windowManager.addListener(this);
Future.delayed(Duration(milliseconds: 500), () {
if (widget.isMainWindow) {
windowManager.isMaximized().then((maximized) {
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
} else {
final wc = WindowController.fromWindowId(kWindowId!);
wc.isMaximized().then((maximized) {
debugPrint("isMaximized $maximized");
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
}
});
}
@override
void dispose() {
DesktopMultiWindow.removeListener(this);
windowManager.removeListener(this);
_macOSCheckRestoreTimer?.cancel();
super.dispose();
}
void _setMaximized(bool maximize) {
stateGlobal.setMaximized(maximize);
_saveFrameDebounce.call(_saveFrame);
setState(() {});
}
@override
void onWindowFocus() {
stateGlobal.isFocused.value = true;
}
@override
void onWindowBlur() {
stateGlobal.isFocused.value = false;
}
@override
void onWindowMinimize() {
stateGlobal.setMinimized(true);
stateGlobal.setMaximized(false);
super.onWindowMinimize();
}
@override
void onWindowMaximize() {
stateGlobal.setMinimized(false);
_setMaximized(true);
super.onWindowMaximize();
}
@override
void onWindowUnmaximize() {
stateGlobal.setMinimized(false);
_setMaximized(false);
super.onWindowUnmaximize();
}
_saveFrame() async {
if (widget.tabType == DesktopTabType.main) {
await saveWindowPosition(WindowType.Main);
} else if (kWindowType != null && kWindowId != null) {
await saveWindowPosition(kWindowType!, windowId: kWindowId);
}
}
@override
void onWindowMoved() {
_saveFrameDebounce.call(_saveFrame);
super.onWindowMoved();
}
@override
void onWindowResized() {
_saveFrameDebounce.call(_saveFrame);
super.onWindowMoved();
}
@override
void onWindowClose() async {
mainWindowClose() async => await windowManager.hide();
notMainWindowClose(WindowController controller) async {
if (widget.tabController.length != 0) {
debugPrint("close not empty multiwindow from taskbar");
if (isWindows) {
await controller.show();
await controller.focus();
final res = await widget.onClose?.call() ?? true;
if (!res) return;
}
widget.tabController.clear();
}
await controller.hide();
await rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
}
macOSWindowClose(
Future<bool> Function() checkFullscreen,
Future<void> Function() closeFunc,
) async {
_macOSCheckRestoreCounter = 0;
_macOSCheckRestoreTimer =
Timer.periodic(Duration(milliseconds: 30), (timer) async {
_macOSCheckRestoreCounter++;
if (!await checkFullscreen() || _macOSCheckRestoreCounter >= 30) {
_macOSCheckRestoreTimer?.cancel();
_macOSCheckRestoreTimer = null;
Timer(Duration(milliseconds: 700), () async => await closeFunc());
}
});
}
// hide window on close
if (widget.isMainWindow) {
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
}
// macOS specific workaround, the window is not hiding when in fullscreen.
if (isMacOS && await windowManager.isFullScreen()) {
await windowManager.setFullScreen(false);
await macOSWindowClose(
() async => await windowManager.isFullScreen(),
mainWindowClose,
);
} else {
await mainWindowClose();
}
} else {
// it's safe to hide the subwindow
final controller = WindowController.fromWindowId(kWindowId!);
if (isMacOS) {
// onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
// use ??= to make sure the value is set on first call.
if (await widget.onClose?.call() ?? true) {
if (await controller.isFullScreen()) {
await controller.setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
await macOSWindowClose(
() async => await controller.isFullScreen(),
() async => await notMainWindowClose(controller),
);
} else {
await notMainWindowClose(controller);
}
}
} else {
await notMainWindowClose(controller);
}
}
super.onWindowClose();
}
bool showTabDowndown() {
return widget.tabController.state.value.tabs.length > 1 &&
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
@@ -710,72 +757,69 @@ class WindowActionPanelState extends State<WindowActionPanel>
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Obx(() => Offstage(
offstage:
!(showTabDowndown() && existingInvisibleTab().isNotEmpty),
child: _TabDropDownButton(
controller: widget.tabController,
labelGetter: widget.labelGetter,
tabkeys: existingInvisibleTab()),
)),
Offstage(offstage: widget.tail == null, child: widget.tail),
Offstage(
offstage: kUseCompatibleUiMode,
child: Row(
Obx(() {
if (showTabDowndown() && existingInvisibleTab().isNotEmpty) {
return _TabDropDownButton(
controller: widget.tabController,
labelGetter: widget.labelGetter,
tabkeys: existingInvisibleTab());
} else {
return Offstage();
}
}),
if (widget.tail != null) widget.tail!,
if (!kUseCompatibleUiMode)
Row(
children: [
Offstage(
offstage: !widget.showMinimize || isMacOS,
child: ActionIcon(
message: 'Minimize',
icon: IconFont.min,
onTap: () {
if (widget.isMainWindow) {
windowManager.minimize();
} else {
WindowController.fromWindowId(kWindowId!).minimize();
}
},
isClose: false,
)),
Offstage(
offstage: !widget.showMaximize || isMacOS,
child: Obx(() => ActionIcon(
message: stateGlobal.isMaximized.isTrue
? 'Restore'
: 'Maximize',
icon: stateGlobal.isMaximized.isTrue
? IconFont.restore
: IconFont.max,
onTap: bind.isIncomingOnly() && isInHomePage()
? null
: _toggleMaximize,
isClose: false,
))),
Offstage(
offstage: !widget.showClose || isMacOS,
child: ActionIcon(
message: 'Close',
icon: IconFont.close,
onTap: () async {
final res = await widget.onClose?.call() ?? true;
if (res) {
// hide for all window
// note: the main window can be restored by tray icon
Future.delayed(Duration.zero, () async {
if (widget.isMainWindow) {
await windowManager.close();
} else {
await WindowController.fromWindowId(kWindowId!)
.close();
}
});
}
},
isClose: true,
))
if (widget.showMinimize && !isMacOS)
ActionIcon(
message: 'Minimize',
icon: IconFont.min,
onTap: () {
if (widget.isMainWindow) {
windowManager.minimize();
} else {
WindowController.fromWindowId(kWindowId!).minimize();
}
},
isClose: false,
),
if (widget.showMaximize && !isMacOS)
Obx(() => ActionIcon(
message: stateGlobal.isMaximized.isTrue
? 'Restore'
: 'Maximize',
icon: stateGlobal.isMaximized.isTrue
? IconFont.restore
: IconFont.max,
onTap: bind.isIncomingOnly() && isInHomePage()
? null
: _toggleMaximize,
isClose: false,
)),
if (widget.showClose && !isMacOS)
ActionIcon(
message: 'Close',
icon: IconFont.close,
onTap: () async {
final res = await widget.onClose?.call() ?? true;
if (res) {
// hide for all window
// note: the main window can be restored by tray icon
Future.delayed(Duration.zero, () async {
if (widget.isMainWindow) {
await windowManager.close();
} else {
await WindowController.fromWindowId(kWindowId!)
.close();
}
});
}
},
isClose: true,
)
],
),
),
],
);
}
@@ -1179,22 +1223,26 @@ class _CloseButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
width: _kIconSize,
child: Offstage(
offstage: !visible,
child: InkWell(
hoverColor: MyTheme.tabbar(context).closeHoverColor,
customBorder: const CircleBorder(),
onTap: () => onClose(),
child: Icon(
Icons.close,
size: _kIconSize,
color: tabSelected
? MyTheme.tabbar(context).selectedIconColor
: MyTheme.tabbar(context).unSelectedIconColor,
),
),
)).paddingOnly(left: 10);
width: _kIconSize,
child: () {
if (visible) {
return InkWell(
hoverColor: MyTheme.tabbar(context).closeHoverColor,
customBorder: const CircleBorder(),
onTap: () => onClose(),
child: Icon(
Icons.close,
size: _kIconSize,
color: tabSelected
? MyTheme.tabbar(context).selectedIconColor
: MyTheme.tabbar(context).unSelectedIconColor,
),
);
} else {
return Offstage();
}
}())
.paddingOnly(left: 10);
}
}
@@ -1348,27 +1396,30 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
child: InkWell(child: Text(label)),
),
Obx(
() => Offstage(
offstage: !(tabInfo?.onTabCloseButton != null &&
menuHover.value),
child: InkWell(
onTap: () {
tabInfo?.onTabCloseButton?.call();
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
onHover: (event) =>
setState(() => btnHover.value = true),
onExit: (event) =>
setState(() => btnHover.value = false),
child: Icon(Icons.close,
color:
btnHover.value ? Colors.red : null))),
),
)
() {
if (tabInfo?.onTabCloseButton != null &&
menuHover.value) {
return InkWell(
onTap: () {
tabInfo?.onTabCloseButton?.call();
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
onHover: (event) =>
setState(() => btnHover.value = true),
onExit: (event) =>
setState(() => btnHover.value = false),
child: Icon(Icons.close,
color:
btnHover.value ? Colors.red : null)));
} else {
return Offstage();
}
},
),
],
),
),

View File

@@ -6,6 +6,7 @@ import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/overlay.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/pages/install_page.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart';
@@ -95,7 +96,9 @@ Future<void> main(List<String> args) async {
desktopType = DesktopType.main;
await windowManager.ensureInitialized();
windowManager.setPreventClose(true);
windowManager.setMovable(false);
if (isMacOS) {
disableWindowMovable(kWindowId);
}
runMainApp(true);
}
}
@@ -154,6 +157,7 @@ void runMobileApp() async {
await initEnv(kAppTypeMain);
if (isAndroid) androidChannelInit();
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
draggablePositions.load();
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser();
runApp(App());
@@ -168,10 +172,13 @@ void runMultiWindow(
final title = getWindowName();
// set prevent close to true, we handle close event manually
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
WindowController.fromWindowId(kWindowId!).setMovable(false);
if (isMacOS) {
disableWindowMovable(kWindowId);
}
late Widget widget;
switch (appType) {
case kAppTypeDesktopRemote:
draggablePositions.load();
widget = DesktopRemoteScreen(
params: argument,
);

View File

@@ -395,7 +395,7 @@ class _WebMenuState extends State<WebMenu> {
[
PopupMenuItem(
value: "about",
child: Text('${translate('About')} RustDesk'),
child: Text(translate('About RustDesk')),
)
];
},

View File

@@ -55,6 +55,9 @@ class _RemotePageState extends State<RemotePage> {
InputModel get inputModel => gFFI.inputModel;
SessionID get sessionId => gFFI.sessionId;
final TextEditingController _textController =
TextEditingController(text: initText);
@override
void initState() {
super.initState();
@@ -82,13 +85,14 @@ class _RemotePageState extends State<RemotePage> {
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
_blockableOverlayState.applyFfi(gFFI);
gFFI.dialogManager.loadMobileActionsOverlayVisible();
}
@override
Future<void> dispose() async {
// https://github.com/flutter/flutter/issues/64935
super.dispose();
gFFI.dialogManager.hideMobileActionsOverlay();
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
gFFI.inputModel.listenToMouse(false);
gFFI.imageModel.disposeImage();
gFFI.cursorModel.disposeImages();
@@ -105,11 +109,10 @@ class _RemotePageState extends State<RemotePage> {
}
await keyboardSubscription.cancel();
removeSharedStates(widget.id);
if (isAndroid) {
// Only one client is considered here for now.
// TODO: take into account the case where there are multiple clients
gFFI.invokeMethod("on_voice_call_closed");
}
// `on_voice_call_closed` should be called when the connection is ended.
// The inner logic of `on_voice_call_closed` will check if the voice call is active.
// Only one client is considered here for now.
gFFI.chatModel.onVoiceCallClosed("End connetion");
}
// to-do: It should be better to use transparent color instead of the bgColor.
@@ -145,37 +148,59 @@ class _RemotePageState extends State<RemotePage> {
setState(() {});
}
// handle mobile virtual keyboard
void handleSoftKeyboardInput(String newValue) {
void _handleIOSSoftKeyboardInput(String newValue) {
var oldValue = _value;
_value = newValue;
if (isIOS) {
var i = newValue.length - 1;
for (; i >= 0 && newValue[i] != '\1'; --i) {}
var j = oldValue.length - 1;
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
if (i < j) j = i;
newValue = newValue.substring(j + 1);
oldValue = oldValue.substring(j + 1);
var common = 0;
for (;
common < oldValue.length &&
common < newValue.length &&
newValue[common] == oldValue[common];
++common) {}
for (i = 0; i < oldValue.length - common; ++i) {
inputModel.inputKey('VK_BACK');
}
if (newValue.length > common) {
var s = newValue.substring(common);
if (s.length > 1) {
bind.sessionInputString(sessionId: sessionId, value: s);
} else {
inputChar(s);
}
}
return;
var i = newValue.length - 1;
for (; i >= 0 && newValue[i] != '\1'; --i) {}
var j = oldValue.length - 1;
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
if (i < j) j = i;
var subNewValue = newValue.substring(j + 1);
var subOldValue = oldValue.substring(j + 1);
// get common prefix of subNewValue and subOldValue
var common = 0;
for (;
common < subOldValue.length &&
common < subNewValue.length &&
subNewValue[common] == subOldValue[common];
++common) {}
// get newStr from subNewValue
var newStr = "";
if (subNewValue.length > common) {
newStr = subNewValue.substring(common);
}
// Set the value to the old value and early return if is still composing. (1 && 2)
// 1. The composing range is valid
// 2. The new string is shorter than the composing range.
if (_textController.value.isComposingRangeValid) {
final composingLength = _textController.value.composing.end -
_textController.value.composing.start;
if (composingLength > newStr.length) {
_value = oldValue;
return;
}
}
// Delete the different part in the old value.
for (i = 0; i < subOldValue.length - common; ++i) {
inputModel.inputKey('VK_BACK');
}
// Input the new string.
if (newStr.length > 1) {
bind.sessionInputString(sessionId: sessionId, value: newStr);
} else {
inputChar(newStr);
}
}
void _handleNonIOSSoftKeyboardInput(String newValue) {
var oldValue = _value;
_value = newValue;
if (oldValue.isNotEmpty &&
newValue.isNotEmpty &&
oldValue[0] == '\1' &&
@@ -214,6 +239,15 @@ class _RemotePageState extends State<RemotePage> {
}
}
// handle mobile virtual keyboard
void handleSoftKeyboardInput(String newValue) {
if (isIOS) {
_handleIOSSoftKeyboardInput(newValue);
} else {
_handleNonIOSSoftKeyboardInput(newValue);
}
}
void inputChar(String char) {
if (char == '\n') {
char = 'VK_RETURN';
@@ -227,6 +261,7 @@ class _RemotePageState extends State<RemotePage> {
gFFI.invokeMethod("enable_soft_keyboard", true);
// destroy first, so that our _value trick can work
_value = initText;
_textController.text = _value;
setState(() => _showEdit = false);
_timer?.cancel();
_timer = Timer(kMobileDelaySoftKeyboard, () {
@@ -242,12 +277,10 @@ class _RemotePageState extends State<RemotePage> {
});
}
bool get keyboard => gFFI.ffiModel.permissions['keyboard'] != false;
Widget _bottomWidget() => _showGestureHelp
? getGestureHelp()
: (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
? getBottomAppBar(keyboard)
? getBottomAppBar()
: Offstage());
@override
@@ -314,7 +347,7 @@ class _RemotePageState extends State<RemotePage> {
return Container(
color: kColorCanvas,
child: isWebDesktop
? getBodyForDesktopWithListener(keyboard)
? getBodyForDesktopWithListener()
: SafeArea(
child:
OrientationBuilder(builder: (ctx, orientation) {
@@ -346,9 +379,9 @@ class _RemotePageState extends State<RemotePage> {
}
Widget getRawPointerAndKeyBody(Widget child) {
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
final ffiModel = Provider.of<FfiModel>(context);
return RawPointerMouseRegion(
cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer,
cursor: ffiModel.keyboard ? SystemMouseCursors.none : MouseCursor.defer,
inputModel: inputModel,
// Disable RawKeyFocusScope before the connecting is established.
// The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
@@ -361,7 +394,8 @@ class _RemotePageState extends State<RemotePage> {
);
}
Widget getBottomAppBar(bool keyboard) {
Widget getBottomAppBar() {
final ffiModel = Provider.of<FfiModel>(context);
return BottomAppBar(
elevation: 10,
color: MyTheme.accent,
@@ -387,7 +421,7 @@ class _RemotePageState extends State<RemotePage> {
},
)
] +
(isWebDesktop
(isWebDesktop || ffiModel.viewOnly || !ffiModel.keyboard
? []
: gFFI.ffiModel.isPeerAndroid
? [
@@ -419,17 +453,21 @@ class _RemotePageState extends State<RemotePage> {
(isWeb
? []
: <Widget>[
IconButton(
color: Colors.white,
icon: isAndroid
? SvgPicture.asset('assets/chat.svg',
colorFilter: ColorFilter.mode(
Colors.white, BlendMode.srcIn))
: Icon(Icons.message),
onPressed: () => isAndroid
? showChatOptions(widget.id)
: onPressedTextChat(widget.id),
)
futureBuilder(
future: gFFI.invokeMethod(
"get_value", "KEY_IS_SUPPORT_VOICE_CALL"),
hasData: (isSupportVoiceCall) => IconButton(
color: Colors.white,
icon: isAndroid && isSupportVoiceCall
? SvgPicture.asset('assets/chat.svg',
colorFilter: ColorFilter.mode(
Colors.white, BlendMode.srcIn))
: Icon(Icons.message),
onPressed: () =>
isAndroid && isSupportVoiceCall
? showChatOptions(widget.id)
: onPressedTextChat(widget.id),
))
]) +
[
IconButton(
@@ -479,11 +517,15 @@ class _RemotePageState extends State<RemotePage> {
: TextFormField(
textInputAction: TextInputAction.newline,
autocorrect: false,
enableSuggestions: false,
// Flutter 3.16.9 Android.
// `enableSuggestions` causes secure keyboard to be shown.
// https://github.com/flutter/flutter/issues/139143
// https://github.com/flutter/flutter/issues/146540
// enableSuggestions: false,
autofocus: true,
focusNode: _mobileFocusNode,
maxLines: null,
initialValue: _value,
controller: _textController,
// trick way to make backspace work always
keyboardType: TextInputType.multiline,
onChanged: handleSoftKeyboardInput,
@@ -491,48 +533,84 @@ class _RemotePageState extends State<RemotePage> {
),
];
if (showCursorPaint) {
paints.add(CursorPaint());
paints.add(CursorPaint(widget.id));
}
return paints;
}()));
}
Widget getBodyForDesktopWithListener(bool keyboard) {
Widget getBodyForDesktopWithListener() {
final ffiModel = Provider.of<FfiModel>(context);
var paints = <Widget>[ImagePaint()];
if (showCursorPaint) {
final cursor = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: 'show-remote-cursor');
if (keyboard || cursor) {
paints.add(CursorPaint());
if (ffiModel.keyboard || cursor) {
paints.add(CursorPaint(widget.id));
}
}
return Container(
color: MyTheme.canvasColor, child: Stack(children: paints));
}
List<TTextMenu> _getMobileActionMenus() {
if (gFFI.ffiModel.pi.platform != kPeerPlatformAndroid ||
!gFFI.ffiModel.keyboard) {
return [];
}
final enabled = versionCmp(gFFI.ffiModel.pi.version, '1.2.7') >= 0;
if (!enabled) return [];
return [
TTextMenu(
child: Text(translate('Back')),
onPressed: () => gFFI.inputModel.onMobileBack(),
),
TTextMenu(
child: Text(translate('Home')),
onPressed: () => gFFI.inputModel.onMobileHome(),
),
TTextMenu(
child: Text(translate('Apps')),
onPressed: () => gFFI.inputModel.onMobileApps(),
),
TTextMenu(
child: Text(translate('Volume up')),
onPressed: () => gFFI.inputModel.onMobileVolumeUp(),
),
TTextMenu(
child: Text(translate('Volume down')),
onPressed: () => gFFI.inputModel.onMobileVolumeDown(),
),
TTextMenu(
child: Text(translate('Power')),
onPressed: () => gFFI.inputModel.onMobilePower(),
),
];
}
void showActions(String id) async {
final size = MediaQuery.of(context).size;
final x = 120.0;
final y = size.height;
final mobileActionMenus = _getMobileActionMenus();
final menus = toolbarControls(context, id, gFFI);
getChild(TTextMenu menu) {
if (menu.trailingIcon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
menu.child,
menu.trailingIcon!,
]);
} else {
return menu.child;
}
}
final more = menus
.asMap()
.entries
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
.toList();
final List<PopupMenuEntry<int>> more = [
...mobileActionMenus
.asMap()
.entries
.map((e) =>
PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
.toList(),
if (mobileActionMenus.isNotEmpty) PopupMenuDivider(),
...menus
.asMap()
.entries
.map((e) => PopupMenuItem<int>(
child: e.value.getChild(),
value: e.key + mobileActionMenus.length))
.toList(),
];
() async {
var index = await showMenu(
context: context,
@@ -540,8 +618,12 @@ class _RemotePageState extends State<RemotePage> {
items: more,
elevation: 8,
);
if (index != null && index < menus.length) {
menus[index].onPressed.call();
if (index != null) {
if (index < mobileActionMenus.length) {
mobileActionMenus[index].onPressed.call();
} else if (index < mobileActionMenus.length + more.length) {
menus[index - mobileActionMenus.length].onPressed.call();
}
}
}();
}
@@ -561,9 +643,11 @@ class _RemotePageState extends State<RemotePage> {
child: Text(translate(label), style: labelStyle),
trailingIcon: Transform.scale(
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
child: IconButton(
onPressed: onPressed,
icon: icon,
child: IgnorePointer(
child: IconButton(
onPressed: null,
icon: icon,
),
),
),
onPressed: onPressed,
@@ -594,23 +678,11 @@ class _RemotePageState extends State<RemotePage> {
),
onPressVoiceCall),
];
getChild(TTextMenu menu) {
if (menu.trailingIcon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
menu.child,
menu.trailingIcon!,
]);
} else {
return menu.child;
}
}
final menuItems = menus
.asMap()
.entries
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
.map((e) => PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
.toList();
Future.delayed(Duration.zero, () async {
final size = MediaQuery.of(context).size;
@@ -679,6 +751,7 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
var _fn = false;
var _pin = false;
final _keyboardVisibilityController = KeyboardVisibilityController();
final _key = GlobalKey();
InputModel get inputModel => gFFI.inputModel;
@@ -703,6 +776,24 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
onPressed: onPressed);
}
@override
void initState() {
super.initState();
}
_updateRect() {
RenderObject? renderObject = _key.currentContext?.findRenderObject();
if (renderObject == null) {
return;
}
if (renderObject is RenderBox) {
final size = renderObject.size;
Offset pos = renderObject.localToGlobal(Offset.zero);
gFFI.cursorModel.keyHelpToolsVisibilityChanged(
Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height));
}
}
@override
Widget build(BuildContext context) {
final hasModifierOn = inputModel.ctrl ||
@@ -711,6 +802,7 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
inputModel.command;
if (!_pin && !hasModifierOn && !widget.requestShow) {
gFFI.cursorModel.keyHelpToolsVisibilityChanged(null);
return Offstage();
}
final size = MediaQuery.of(context).size;
@@ -821,7 +913,12 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
}),
];
final space = size.width > 320 ? 4.0 : 2.0;
// 500 ms is long enough for this widget to be built!
Future.delayed(Duration(milliseconds: 500), () {
_updateRect();
});
return Container(
key: _key,
color: Color(0xAA000000),
padding: EdgeInsets.only(
top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8),
@@ -852,26 +949,52 @@ class ImagePaint extends StatelessWidget {
}
class CursorPaint extends StatelessWidget {
late final String id;
CursorPaint(this.id);
@override
Widget build(BuildContext context) {
final m = Provider.of<CursorModel>(context);
final c = Provider.of<CanvasModel>(context);
final ffiModel = Provider.of<FfiModel>(context);
final adjust = gFFI.cursorModel.adjustForKeyboard();
var s = c.scale;
final s = c.scale;
double hotx = m.hotx;
double hoty = m.hoty;
if (m.image == null) {
var image = m.image;
if (image == null) {
if (preDefaultCursor.image != null) {
image = preDefaultCursor.image;
hotx = preDefaultCursor.image!.width / 2;
hoty = preDefaultCursor.image!.height / 2;
}
}
if (preForbiddenCursor.image != null &&
!ffiModel.viewOnly &&
!ffiModel.keyboard &&
!ShowRemoteCursorState.find(id).value) {
image = preForbiddenCursor.image;
hotx = preForbiddenCursor.image!.width / 2;
hoty = preForbiddenCursor.image!.height / 2;
}
if (image == null) {
return Offstage();
}
final minSize = 12.0;
double mins =
minSize / (image.width > image.height ? image.width : image.height);
double factor = 1.0;
if (s < mins) {
factor = s / mins;
}
final s2 = s < mins ? mins : s;
return CustomPaint(
painter: ImagePainter(
image: m.image ?? preDefaultCursor.image,
x: m.x * s - hotx + c.x,
y: m.y * s - hoty + c.y - adjust,
scale: 1),
image: image,
x: (m.x - hotx) * factor + c.x / s2,
y: (m.y - hoty) * factor + (c.y - adjust) / s2,
scale: s2),
);
}
}
@@ -901,7 +1024,7 @@ void showOptions(
border: Border.all(color: Theme.of(context).hintColor),
borderRadius: BorderRadius.circular(2),
color: i == cur
? Theme.of(context).toggleableActiveColor.withOpacity(0.6)
? Theme.of(context).primaryColor.withOpacity(0.6)
: null),
child: Center(
child: Text((i + 1).toString(),
@@ -949,22 +1072,40 @@ void showOptions(
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
final radios = [
for (var e in viewStyleRadios)
Obx(() => getRadio<String>(e.child, e.value, viewStyle.value, (v) {
e.onChanged?.call(v);
if (v != null) viewStyle.value = v;
})),
Obx(() => getRadio<String>(
e.child,
e.value,
viewStyle.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) viewStyle.value = v;
}
: null)),
const Divider(color: MyTheme.border),
for (var e in imageQualityRadios)
Obx(() => getRadio<String>(e.child, e.value, imageQuality.value, (v) {
e.onChanged?.call(v);
if (v != null) imageQuality.value = v;
})),
Obx(() => getRadio<String>(
e.child,
e.value,
imageQuality.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) imageQuality.value = v;
}
: null)),
const Divider(color: MyTheme.border),
for (var e in codecRadios)
Obx(() => getRadio<String>(e.child, e.value, codec.value, (v) {
e.onChanged?.call(v);
if (v != null) codec.value = v;
})),
Obx(() => getRadio<String>(
e.child,
e.value,
codec.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) codec.value = v;
}
: null)),
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
];
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
@@ -975,10 +1116,12 @@ void showOptions(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
value: rxCursorToggleValues[e.key].value,
onChanged: (v) {
e.value.onChanged?.call(v);
if (v != null) rxCursorToggleValues[e.key].value = v;
},
onChanged: e.value.onChanged != null
? (v) {
e.value.onChanged?.call(v);
if (v != null) rxCursorToggleValues[e.key].value = v;
}
: null,
title: e.value.child)))
.toList();
@@ -990,10 +1133,12 @@ void showOptions(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
value: rxToggleValues[e.key].value,
onChanged: (v) {
e.value.onChanged?.call(v);
if (v != null) rxToggleValues[e.key].value = v;
},
onChanged: e.value.onChanged != null
? (v) {
e.value.onChanged?.call(v);
if (v != null) rxToggleValues[e.key].value = v;
}
: null,
title: e.value.child)))
.toList();
final toggles = [
@@ -1013,14 +1158,110 @@ void showOptions(
);
}
var popupDialogMenus = List<Widget>.empty(growable: true);
final resolution = getResolutionMenu(gFFI, id);
if (resolution != null) {
popupDialogMenus.add(ListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
title: resolution.child,
onTap: () {
close();
resolution.onPressed();
},
));
}
final virtualDisplayMenu = getVirtualDisplayMenu(gFFI, id);
if (virtualDisplayMenu != null) {
popupDialogMenus.add(ListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
title: virtualDisplayMenu.child,
onTap: () {
close();
virtualDisplayMenu.onPressed();
},
));
}
if (popupDialogMenus.isNotEmpty) {
popupDialogMenus.add(const Divider(color: MyTheme.border));
}
return CustomAlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: displays + radios + toggles + [privacyModeWidget]),
children: displays +
radios +
popupDialogMenus +
toggles +
[privacyModeWidget]),
);
}, clickMaskDismiss: true, backDismiss: true);
}
TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
if (!showVirtualDisplayMenu(ffi)) {
return null;
}
return TTextMenu(
child: Text(translate("Virtual display")),
onPressed: () {
ffi.dialogManager.show((setState, close, context) {
final children = getVirtualDisplayMenuChildren(ffi, id, close);
return CustomAlertDialog(
title: Text(translate('Virtual display')),
content: Column(
mainAxisSize: MainAxisSize.min,
children: children,
),
);
}, clickMaskDismiss: true, backDismiss: true);
},
);
}
TTextMenu? getResolutionMenu(FFI ffi, String id) {
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final resolutions = pi.resolutions;
final display = pi.tryGetDisplayIfNotAllDisplay(display: pi.currentDisplay);
final visible =
ffiModel.keyboard && (resolutions.length > 1) && display != null;
if (!visible) return null;
return TTextMenu(
child: Text(translate("Resolution")),
onPressed: () {
ffi.dialogManager.show((setState, close, context) {
final children = resolutions
.map((e) => getRadio<String>(
Text('${e.width}x${e.height}'),
'${e.width}x${e.height}',
'${display.width}x${display.height}',
(value) {
close();
bind.sessionChangeResolution(
sessionId: ffi.sessionId,
display: pi.currentDisplay,
width: e.width,
height: e.height,
);
},
))
.toList();
return CustomAlertDialog(
title: Text(translate('Resolution')),
content: Column(
mainAxisSize: MainAxisSize.min,
children: children,
),
);
}, clickMaskDismiss: true, backDismiss: true);
},
);
}
void sendPrompt(bool isMac, String key) {
final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl;
if (isMac) {

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:get/get.dart';
@@ -22,7 +23,22 @@ class ServerPage extends StatefulWidget implements PageShape {
final icon = const Icon(Icons.mobile_screen_share);
@override
final appBarActions = [
final appBarActions = (!bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
? [_DropDownAction()]
: [];
ServerPage({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ServerPageState();
}
class _DropDownAction extends StatelessWidget {
_DropDownAction();
// should only have one action
final actions = [
PopupMenuButton<String>(
tooltip: "",
icon: const Icon(Icons.more_vert),
@@ -101,18 +117,27 @@ class ServerPage extends StatefulWidget implements PageShape {
),
];
},
onSelected: (value) {
onSelected: (value) async {
if (value == "changeID") {
changeIdDialog();
} else if (value == "setPermanentPassword") {
setPermanentPasswordDialog(gFFI.dialogManager);
setPasswordDialog();
} else if (value == "setTemporaryPasswordLength") {
setTemporaryPasswordLengthDialog(gFFI.dialogManager);
} else if (value == kUsePermanentPassword ||
value == kUseTemporaryPassword ||
value == kUseBothPasswords) {
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
gFFI.serverModel.updatePasswordModel();
callback() {
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
gFFI.serverModel.updatePasswordModel();
}
if (value == kUsePermanentPassword &&
(await bind.mainGetPermanentPassword()).isEmpty) {
setPasswordDialog(notEmptyCallback: callback);
} else {
callback();
}
} else if (value.startsWith("AcceptSessionsVia")) {
value = value.substring("AcceptSessionsVia".length);
if (value == "Password") {
@@ -126,10 +151,10 @@ class ServerPage extends StatefulWidget implements PageShape {
})
];
ServerPage({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ServerPageState();
Widget build(BuildContext context) {
return actions[0];
}
}
class _ServerPageState extends State<ServerPage> {
@@ -854,6 +879,15 @@ void androidChannelInit() {
msgBox(gFFI.sessionId, type, title, text, link, gFFI.dialogManager);
break;
}
case "stop_service":
{
print(
"stop_service by kotlin, isStart:${gFFI.serverModel.isStart}");
if (gFFI.serverModel.isStart) {
gFFI.serverModel.stopService();
}
break;
}
}
} catch (e) {
debugPrintStack(label: "MethodCallHandler err:$e");

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:settings_ui/settings_ui.dart';
@@ -35,10 +36,41 @@ class SettingsPage extends StatefulWidget implements PageShape {
const url = 'https://rustdesk.com/';
enum KeepScreenOn {
never,
duringControlled,
serviceOn,
}
String _keepScreenOnToOption(KeepScreenOn value) {
switch (value) {
case KeepScreenOn.never:
return 'never';
case KeepScreenOn.duringControlled:
return 'during-controlled';
case KeepScreenOn.serviceOn:
return 'service-on';
}
}
KeepScreenOn optionToKeepScreenOn(String value) {
switch (value) {
case 'never':
return KeepScreenOn.never;
case 'service-on':
return KeepScreenOn.serviceOn;
default:
return KeepScreenOn.duringControlled;
}
}
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
final _hasIgnoreBattery = androidVersion >= 26;
final _hasIgnoreBattery =
false; //androidVersion >= 26; // remove because not work on every device
var _ignoreBatteryOpt = false;
var _enableStartOnBoot = false;
var _floatingWindowDisabled = false;
var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window
var _enableAbr = false;
var _denyLANDiscovery = false;
var _onlyWhiteList = false;
@@ -52,12 +84,40 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
var _fingerprint = "";
var _buildDate = "";
var _autoDisconnectTimeout = "";
var _hideServer = false;
var _hideProxy = false;
var _hideNetwork = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_enableAbr = option2bool(
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
_onlyWhiteList = whitelistNotEmpty();
_enableDirectIPAccess = option2bool(
kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
_enableRecordSession = option2bool(kOptionEnableRecordSession,
bind.mainGetOptionSync(key: kOptionEnableRecordSession));
_enableHardwareCodec = option2bool(kOptionEnableHwcodec,
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
_autoDisconnectTimeout =
bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
_hideServer =
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
_hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
_hideNetwork =
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y';
() async {
var update = false;
@@ -86,67 +146,21 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
_enableStartOnBoot = enableStartOnBoot;
}
final enableAbrRes = option2bool(
kOptionEnableAbr, await bind.mainGetOption(key: kOptionEnableAbr));
if (enableAbrRes != _enableAbr) {
var floatingWindowDisabled =
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
!await AndroidPermissionManager.check(kSystemAlertWindow);
if (floatingWindowDisabled != _floatingWindowDisabled) {
update = true;
_enableAbr = enableAbrRes;
_floatingWindowDisabled = floatingWindowDisabled;
}
final denyLanDiscovery = !option2bool('enable-lan-discovery',
await bind.mainGetOption(key: 'enable-lan-discovery'));
if (denyLanDiscovery != _denyLANDiscovery) {
final keepScreenOn = _floatingWindowDisabled
? KeepScreenOn.never
: optionToKeepScreenOn(
bind.mainGetLocalOption(key: kOptionKeepScreenOn));
if (keepScreenOn != _keepScreenOn) {
update = true;
_denyLANDiscovery = denyLanDiscovery;
}
final onlyWhiteList = (await bind.mainGetOption(key: kOptionWhitelist)) !=
defaultOptionWhitelist;
if (onlyWhiteList != _onlyWhiteList) {
update = true;
_onlyWhiteList = onlyWhiteList;
}
final enableDirectIPAccess = option2bool(kOptionDirectServer,
await bind.mainGetOption(key: kOptionDirectServer));
if (enableDirectIPAccess != _enableDirectIPAccess) {
update = true;
_enableDirectIPAccess = enableDirectIPAccess;
}
final enableRecordSession = option2bool(kOptionEnableRecordSession,
await bind.mainGetOption(key: kOptionEnableRecordSession));
if (enableRecordSession != _enableRecordSession) {
update = true;
_enableRecordSession = enableRecordSession;
}
final enableHardwareCodec = option2bool(kOptionEnableHwcodec,
await bind.mainGetOption(key: kOptionEnableHwcodec));
if (_enableHardwareCodec != enableHardwareCodec) {
update = true;
_enableHardwareCodec = enableHardwareCodec;
}
final autoRecordIncomingSession = option2bool(
kOptionAllowAutoRecordIncoming,
await bind.mainGetOption(key: kOptionAllowAutoRecordIncoming));
if (autoRecordIncomingSession != _autoRecordIncomingSession) {
update = true;
_autoRecordIncomingSession = autoRecordIncomingSession;
}
final localIP = await bind.mainGetOption(key: 'local-ip-addr');
if (localIP != _localIP) {
update = true;
_localIP = localIP;
}
final directAccessPort =
await bind.mainGetOption(key: kOptionDirectAccessPort);
if (directAccessPort != _directAccessPort) {
update = true;
_directAccessPort = directAccessPort;
_keepScreenOn = keepScreenOn;
}
final fingerprint = await bind.mainGetFingerprint();
@@ -160,21 +174,6 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
update = true;
_buildDate = buildDate;
}
final allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
await bind.mainGetOption(key: kOptionAllowAutoDisconnect));
if (allowAutoDisconnect != _allowAutoDisconnect) {
update = true;
_allowAutoDisconnect = allowAutoDisconnect;
}
final autoDisconnectTimeout =
await bind.mainGetOption(key: kOptionAutoDisconnectTimeout);
if (autoDisconnectTimeout != _autoDisconnectTimeout) {
update = true;
_autoDisconnectTimeout = autoDisconnectTimeout;
}
if (update) {
setState(() {});
}
@@ -282,9 +281,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
initialValue: _onlyWhiteList,
onToggle: (_) async {
update() async {
final onlyWhiteList =
(await bind.mainGetOption(key: kOptionWhitelist)) !=
defaultOptionWhitelist;
final onlyWhiteList = whitelistNotEmpty();
if (onlyWhiteList != _onlyWhiteList) {
setState(() {
_onlyWhiteList = onlyWhiteList;
@@ -301,10 +298,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
onToggle: isOptionFixed(kOptionEnableAbr)
? null
: (v) async {
await bind.mainSetOption(
key: kOptionEnableAbr, value: v ? defaultOptionYes : "N");
final newValue =
await bind.mainGetOption(key: kOptionEnableAbr) != "N";
await mainSetBoolOption(kOptionEnableAbr, v);
final newValue = await mainGetBoolOption(kOptionEnableAbr);
setState(() {
_enableAbr = newValue;
});
@@ -316,12 +311,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
onToggle: isOptionFixed(kOptionEnableRecordSession)
? null
: (v) async {
await bind.mainSetOption(
key: kOptionEnableRecordSession,
value: v ? defaultOptionYes : "N");
await mainSetBoolOption(kOptionEnableRecordSession, v);
final newValue =
await bind.mainGetOption(key: kOptionEnableRecordSession) !=
"N";
await mainGetBoolOption(kOptionEnableRecordSession);
setState(() {
_enableRecordSession = newValue;
});
@@ -493,7 +485,59 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
}));
onFloatingWindowChanged(bool toValue) async {
if (toValue) {
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
return;
}
}
}
final disable = !toValue;
bind.mainSetLocalOption(
key: kOptionDisableFloatingWindow,
value: disable ? 'Y' : defaultOptionNo);
setState(() => _floatingWindowDisabled = disable);
gFFI.serverModel.androidUpdatekeepScreenOn();
}
enhancementsTiles.add(SettingsTile.switchTile(
initialValue: !_floatingWindowDisabled,
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(translate('Floating window')),
Text('* ${translate('floating_window_tip')}',
style: Theme.of(context).textTheme.bodySmall),
]),
onToggle: bind.mainIsOptionFixed(key: kOptionDisableFloatingWindow)
? null
: onFloatingWindowChanged));
enhancementsTiles.add(_getPopupDialogRadioEntry(
title: 'Keep screen on',
list: [
_RadioEntry('Never', _keepScreenOnToOption(KeepScreenOn.never)),
_RadioEntry('During controlled',
_keepScreenOnToOption(KeepScreenOn.duringControlled)),
_RadioEntry('During service is on',
_keepScreenOnToOption(KeepScreenOn.serviceOn)),
],
getter: () => _keepScreenOnToOption(_floatingWindowDisabled
? KeepScreenOn.never
: optionToKeepScreenOn(
bind.mainGetLocalOption(key: kOptionKeepScreenOn))),
asyncSetter: isOptionFixed(kOptionKeepScreenOn) || _floatingWindowDisabled
? null
: (value) async {
await bind.mainSetLocalOption(
key: kOptionKeepScreenOn, value: value);
setState(() => _keepScreenOn = optionToKeepScreenOn(value));
gFFI.serverModel.androidUpdatekeepScreenOn();
},
));
final disabledSettings = bind.isDisableSettings();
final hideSecuritySettings =
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) == 'Y';
final settings = SettingsList(
sections: [
customClientSection,
@@ -517,13 +561,20 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
],
),
SettingsSection(title: Text(translate("Settings")), tiles: [
if (!disabledSettings)
if (!disabledSettings && !_hideNetwork && !_hideServer)
SettingsTile(
title: Text(translate('ID/Relay Server')),
leading: Icon(Icons.cloud),
onPressed: (context) {
showServerSettings(gFFI.dialogManager);
}),
if (!isIOS && !_hideNetwork && !_hideProxy)
SettingsTile(
title: Text(translate('Socks5/Http(s) Proxy')),
leading: Icon(Icons.network_ping),
onPressed: (context) {
changeSocks5Proxy();
}),
SettingsTile(
title: Text(translate('Language')),
leading: Icon(Icons.translate),
@@ -533,8 +584,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
SettingsTile(
title: Text(translate(
Theme.of(context).brightness == Brightness.light
? 'Dark Theme'
: 'Light Theme')),
? 'Light Theme'
: 'Dark Theme')),
leading: Icon(Theme.of(context).brightness == Brightness.light
? Icons.dark_mode
: Icons.light_mode),
@@ -551,12 +602,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
onToggle: isOptionFixed(kOptionEnableHwcodec)
? null
: (v) async {
await bind.mainSetOption(
key: kOptionEnableHwcodec,
value: v ? defaultOptionYes : "N");
await mainSetBoolOption(kOptionEnableHwcodec, v);
final newValue =
await bind.mainGetOption(key: kOptionEnableHwcodec) !=
"N";
await mainGetBoolOption(kOptionEnableHwcodec);
setState(() {
_enableHardwareCodec = newValue;
});
@@ -571,11 +619,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
title:
Text(translate('Automatically record incoming sessions')),
leading: Icon(Icons.videocam),
description: FutureBuilder(
builder: (ctx, data) => Offstage(
offstage: !data.hasData,
child: Text("${translate("Directory")}: ${data.data}")),
future: bind.mainVideoSaveDirectory(root: false)),
description: Text(
"${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"),
initialValue: _autoRecordIncomingSession,
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
? null
@@ -595,13 +640,19 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
),
],
),
if (isAndroid && !disabledSettings && !outgoingOnly)
if (isAndroid &&
!disabledSettings &&
!outgoingOnly &&
!hideSecuritySettings)
SettingsSection(
title: Text(translate("Share Screen")),
tiles: shareScreenTiles,
),
if (!bind.isIncomingOnly()) defaultDisplaySection(),
if (isAndroid && !disabledSettings && !outgoingOnly)
if (isAndroid &&
!disabledSettings &&
!outgoingOnly &&
!hideSecuritySettings)
SettingsSection(
title: Text(translate("Enhancements")),
tiles: enhancementsTiles,
@@ -756,7 +807,7 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
void showAbout(OverlayDialogManager dialogManager) {
dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text('${translate('About')} RustDesk'),
title: Text(translate('About RustDesk')),
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
Text('Version: $version'),
InkWell(
@@ -919,7 +970,7 @@ class _RadioEntry {
typedef _RadioEntryGetter = String Function();
typedef _RadioEntrySetter = Future<void> Function(String);
_getPopupDialogRadioEntry({
SettingsTile _getPopupDialogRadioEntry({
required String title,
required List<_RadioEntry> list,
required _RadioEntryGetter getter,
@@ -973,7 +1024,7 @@ _getPopupDialogRadioEntry({
return SettingsTile(
title: Text(translate(title)),
onPressed: (context) => showDialog(),
onPressed: asyncSetter == null ? null : (context) => showDialog(),
value: Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Obx(() => Text(translate(valueText.value))),

View File

@@ -535,6 +535,8 @@ class ChatModel with ChangeNotifier {
void onVoiceCallClosed(String reason) {
_voiceCallStatus.value = VoiceCallStatus.notStarted;
if (isAndroid) {
// We can always invoke "on_voice_call_closed"
// no matter if the `_voiceCallStatus` was `VoiceCallStatus.notStarted` or not.
parent.target?.invokeMethod("on_voice_call_closed");
}
}

View File

@@ -1152,4 +1152,27 @@ class InputModel {
platformFFI.stopDesktopWebListener();
}
}
void onMobileBack() => tap(MouseButtons.right);
void onMobileHome() => tap(MouseButtons.wheel);
Future<void> onMobileApps() async {
sendMouse('down', MouseButtons.wheel);
await Future.delayed(const Duration(milliseconds: 500));
sendMouse('up', MouseButtons.wheel);
}
// Simulate a key press event.
// `usbHidUsage` is the USB HID usage code of the key.
Future<void> tapHidKey(int usbHidUsage) async {
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true);
await Future.delayed(Duration(milliseconds: 100));
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false);
}
Future<void> onMobileVolumeUp() async =>
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage);
Future<void> onMobileVolumeDown() async =>
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage);
Future<void> onMobilePower() async =>
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage);
}

View File

@@ -192,10 +192,10 @@ class FfiModel with ChangeNotifier {
_permissions[k] = v == 'true';
});
// Only inited at remote page
if (desktopType == DesktopType.remote) {
if (parent.target?.connType == ConnType.defaultConn) {
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
}
debugPrint('$_permissions');
debugPrint('updatePermission: $_permissions');
notifyListeners();
}
@@ -438,20 +438,6 @@ class FfiModel with ChangeNotifier {
_handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) {
final running = evt['running'] == 'true';
parent.target?.elevationModel.onPortableServiceRunning(running);
if (running) {
if (pi.primaryDisplay != kInvalidDisplayIndex) {
if (pi.currentDisplay != pi.primaryDisplay) {
// Notify to switch display
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
'elevated_switch_display_msg', '', parent.target!.dialogManager);
bind.sessionSwitchDisplay(
isDesktop: isDesktop,
sessionId: sessionId,
value: Int32List.fromList([pi.primaryDisplay]),
);
}
}
}
}
handleAliasChanged(Map<String, dynamic> evt) {
@@ -917,10 +903,12 @@ class FfiModel with ChangeNotifier {
if (parent.target?.connType == ConnType.defaultConn &&
parent.target != null &&
parent.target!.ffiModel.permissions['keyboard'] != false) {
Timer(
Duration(milliseconds: delayMSecs),
() => parent.target!.dialogManager
.showMobileActionsOverlay(ffi: parent.target!));
Timer(Duration(milliseconds: delayMSecs), () {
if (parent.target!.dialogManager.mobileActionsOverlayVisible.isTrue) {
parent.target!.dialogManager
.showMobileActionsOverlay(ffi: parent.target!);
}
});
}
}
}
@@ -973,7 +961,9 @@ class FfiModel with ChangeNotifier {
}
updateLastCursorId(Map<String, dynamic> evt) {
parent.target?.cursorModel.id = int.parse(evt['id']);
// int.parse(evt['id']) may cause FormatException
// Unhandled Exception: FormatException: Positive input exceeds the limit of integer 18446744071749110741
parent.target?.cursorModel.id = evt['id'];
}
handleCursorId(Map<String, dynamic> evt) {
@@ -1185,10 +1175,11 @@ class ImageModel with ChangeNotifier {
onRgba(int display, Uint8List rgba) {
final pid = parent.target?.id;
final rect = parent.target?.ffiModel.pi.getDisplayRect(display);
img.decodeImageFromPixels(
rgba,
parent.target?.ffiModel.rect?.width.toInt() ?? 0,
parent.target?.ffiModel.rect?.height.toInt() ?? 0,
rect?.width.toInt() ?? 0,
rect?.height.toInt() ?? 0,
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
onPixelsCopied: () {
// Unlock the rgba memory from rust codes.
@@ -1573,22 +1564,24 @@ class CanvasModel with ChangeNotifier {
notifyListeners();
}
updateScale(double v) {
updateScale(double v, Offset focalPoint) {
if (parent.target?.imageModel.image == null) return;
final offset = parent.target?.cursorModel.offset ?? const Offset(0, 0);
var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero;
final px0 = (offset.dx - r.left) * _scale;
final py0 = (offset.dy - r.top) * _scale;
final s = _scale;
_scale *= v;
final maxs = parent.target?.imageModel.maxScale ?? 1;
final mins = parent.target?.imageModel.minScale ?? 1;
if (_scale > maxs) _scale = maxs;
if (_scale < mins) _scale = mins;
r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero;
final px1 = (offset.dx - r.left) * _scale;
final py1 = (offset.dy - r.top) * _scale;
_x -= px1 - px0;
_y -= py1 - py0;
// (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;
notifyListeners();
}
@@ -1619,7 +1612,7 @@ class CanvasModel with ChangeNotifier {
// data for cursor
class CursorData {
final String peerId;
final int id;
final String id;
final img2.Image image;
double scale;
Uint8List? data;
@@ -1699,12 +1692,12 @@ const _forbiddenCursorPng =
const _defaultCursorPng =
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC';
const kPreForbiddenCursorId = -2;
const kPreForbiddenCursorId = "-2";
final preForbiddenCursor = PredefinedCursor(
png: _forbiddenCursorPng,
id: kPreForbiddenCursorId,
);
const kPreDefaultCursorId = -1;
const kPreDefaultCursorId = "-1";
final preDefaultCursor = PredefinedCursor(
png: _defaultCursorPng,
id: kPreDefaultCursorId,
@@ -1717,7 +1710,7 @@ class PredefinedCursor {
img2.Image? _image2;
CursorData? _cache;
String png;
int id;
String id;
double Function(double)? hotxGetter;
double Function(double)? hotyGetter;
@@ -1733,7 +1726,7 @@ class PredefinedCursor {
_image2 = img2.decodePng(base64Decode(png));
if (_image2 != null) {
// The png type of forbidden cursor image is `PngColorType.indexed`.
if (isWindows && id == kPreForbiddenCursorId) {
if (id == kPreForbiddenCursorId) {
_image2 = _image2!.convert(format: img2.Format.uint8, numChannels: 4);
}
@@ -1775,13 +1768,15 @@ class PredefinedCursor {
class CursorModel with ChangeNotifier {
ui.Image? _image;
final _images = <int, Tuple3<ui.Image, double, double>>{};
final _images = <String, Tuple3<ui.Image, double, double>>{};
CursorData? _cache;
final _cacheMap = <int, CursorData>{};
final _cacheMap = <String, CursorData>{};
final _cacheKeys = <String>{};
double _x = -10000;
double _y = -10000;
int _id = -1;
// int.parse(evt['id']) may cause FormatException
// So we use String here.
String _id = "-1";
double _hotx = 0;
double _hoty = 0;
double _displayOriginX = 0;
@@ -1795,6 +1790,33 @@ class CursorModel with ChangeNotifier {
String peerId = '';
WeakReference<FFI> parent;
// Only for mobile, touch mode
// To block touch event above the KeyHelpTools
//
// A better way is to not listen events from the KeyHelpTools.
// But we're now using a Container(child: Stack(...)) to wrap the KeyHelpTools,
// and the listener is on the Container.
Rect? _keyHelpToolsRect;
// `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;
keyHelpToolsVisibilityChanged(Rect? r) {
_keyHelpToolsRect = r;
if (r == null) {
_lastIsBlocked = false;
} else {
// Block the touch event is safe here.
// `lastIsBlocked` is only used in onDoubleTap() to block the touch event from the KeyHelpTools.
// `lastIsBlocked` will be set when the cursor is moving or touch somewhere else.
_lastIsBlocked = true;
}
_yForKeyboardAdjust = _y;
}
get lastIsBlocked => _lastIsBlocked;
ui.Image? get image => _image;
CursorData? get cache => _cache;
@@ -1808,7 +1830,7 @@ class CursorModel with ChangeNotifier {
double get hotx => _hotx;
double get hoty => _hoty;
set id(int id) => _id = id;
set id(String id) => _id = id;
bool get isPeerControlProtected =>
DateTime.now().difference(_lastPeerMouse).inMilliseconds <
@@ -1839,28 +1861,52 @@ 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);
var keyboardHeight = m.viewInsets.bottom;
final size = m.size;
if (keyboardHeight < 100) return 0;
final s = parent.target?.canvasModel.scale ?? 1.0;
final thresh = (size.height - keyboardHeight) / 2;
var h = (_y - getVisibleRect().top) * s; // local physical display height
final h = (_yForKeyboardAdjust - getVisibleRect().top) *
scale; // local physical display height
return h - thresh;
}
move(double x, double y) {
moveLocal(x, y);
parent.target?.inputModel.moveMouse(_x, _y);
// mobile Soft keyboard, block touch event from the KeyHelpTools
shouldBlock(double x, double y) {
if (!(parent.target?.ffiModel.touchMode ?? false)) {
return false;
}
if (_keyHelpToolsRect == null) {
return false;
}
if (isPointInRect(Offset(x, y), _keyHelpToolsRect!)) {
return true;
}
return false;
}
moveLocal(double x, double y) {
final scale = parent.target?.canvasModel.scale ?? 1.0;
move(double x, double y) {
if (shouldBlock(x, y)) {
_lastIsBlocked = true;
return false;
}
_lastIsBlocked = false;
moveLocal(x, y, adjust: adjustForKeyboard());
parent.target?.inputModel.moveMouse(_x, _y);
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) / scale + _displayOriginY;
_y = (y - yoffset + adjust) / scale + _displayOriginY;
notifyListeners();
}
@@ -1986,7 +2032,7 @@ class CursorModel with ChangeNotifier {
}
updateCursorData(Map<String, dynamic> evt) async {
final id = int.parse(evt['id']);
final id = evt['id'];
final hotx = double.parse(evt['hotx']);
final hoty = double.parse(evt['hoty']);
final width = int.parse(evt['width']);
@@ -2011,7 +2057,7 @@ class CursorModel with ChangeNotifier {
Future<bool> _updateCache(
Uint8List rgba,
ui.Image image,
int id,
String id,
double hotx,
double hoty,
int w,
@@ -2393,9 +2439,10 @@ class FFI {
cursorModel.peerId = id;
}
final isNewPeer = tabWindowId == null;
// If tabWindowId != null, this session is a "tab -> window" one.
// Else this session is a new one.
if (tabWindowId == null) {
if (isNewPeer) {
// ignore: unused_local_variable
final addRes = bind.sessionAddSync(
sessionId: sessionId,
@@ -2414,20 +2461,32 @@ class FFI {
'Unreachable, failed to add existed session to $id, the displays is null while display is $display');
return;
}
final addRes = bind.sessionAddExistedSync(id: id, sessionId: sessionId);
final addRes = bind.sessionAddExistedSync(
id: id, sessionId: sessionId, displays: Int32List.fromList(displays));
if (addRes != '') {
debugPrint(
'Unreachable, failed to add existed session to $id, $addRes');
return;
}
bind.sessionTryAddDisplay(
sessionId: sessionId, displays: Int32List.fromList(displays));
ffiModel.pi.currentDisplay = display;
}
if (isDesktop && connType == ConnType.defaultConn) {
textureModel.updateCurrentDisplay(display ?? 0);
}
final stream = bind.sessionStart(sessionId: sessionId, id: id);
// CAUTION: `sessionStart()` and `sessionStartWithDisplays()` are an async functions.
// Though the stream is returned immediately, the stream may not be ready.
// Any operations that depend on the stream should be carefully handled.
late final Stream<EventToUI> stream;
if (isNewPeer || display == null || displays == null) {
stream = bind.sessionStart(sessionId: sessionId, id: id);
} else {
// We have to put displays in `sessionStart()` to make sure the stream is ready
// and then the displays' capturing requests can be sent.
stream = bind.sessionStartWithDisplays(
sessionId: sessionId, id: id, displays: Int32List.fromList(displays));
}
if (isWeb) {
platformFFI.setRgbaCallback((int display, Uint8List data) {
onEvent2UIRgba();
@@ -2438,14 +2497,6 @@ class FFI {
final cb = ffiModel.startEventListener(sessionId, id);
// Force refresh displays.
// The controlled side may not refresh the image when the (peer,display) is already subscribed.
if (displays != null) {
for (final display in displays) {
bind.sessionRefresh(sessionId: sessionId, display: display);
}
}
imageModel.updateUserTextureRender();
final hasGpuTextureRender = bind.mainHasGpuTextureRender();
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
@@ -2496,20 +2547,23 @@ class FFI {
}
} else if (message is EventToUI_Rgba) {
final display = message.field0;
if (imageModel.useTextureRender) {
debugPrint("EventToUI_Rgba display:$display");
if (imageModel.useTextureRender || ffiModel.pi.forceTextureRender) {
//debugPrint("EventToUI_Rgba display:$display");
textureModel.setTextureType(display: display, gpuTexture: false);
onEvent2UIRgba();
} else {
// Fetch the image buffer from rust codes.
final sz = platformFFI.getRgbaSize(sessionId, display);
if (sz == 0) {
platformFFI.nextRgba(sessionId, display);
return;
}
final rgba = platformFFI.getRgba(sessionId, display, sz);
if (rgba != null) {
onEvent2UIRgba();
imageModel.onRgba(display, rgba);
} else {
platformFFI.nextRgba(sessionId, display);
}
}
} else if (message is EventToUI_Texture) {
@@ -2685,6 +2739,7 @@ class PeerInfo with ChangeNotifier {
bool get isSupportMultiDisplay =>
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
bool get forceTextureRender => currentDisplay == kAllDisplayValue;
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
@@ -2693,30 +2748,32 @@ class PeerInfo with ChangeNotifier {
bool get isAmyuniIdd =>
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
Display? tryGetDisplay() {
Display? tryGetDisplay({int? display}) {
if (displays.isEmpty) {
return null;
}
if (currentDisplay == kAllDisplayValue) {
display ??= currentDisplay;
if (display == kAllDisplayValue) {
return displays[0];
} else {
if (currentDisplay > 0 && currentDisplay < displays.length) {
return displays[currentDisplay];
if (display > 0 && display < displays.length) {
return displays[display];
} else {
return displays[0];
}
}
}
Display? tryGetDisplayIfNotAllDisplay() {
Display? tryGetDisplayIfNotAllDisplay({int? display}) {
if (displays.isEmpty) {
return null;
}
if (currentDisplay == kAllDisplayValue) {
display ??= currentDisplay;
if (display == kAllDisplayValue) {
return null;
}
if (currentDisplay >= 0 && currentDisplay < displays.length) {
return displays[currentDisplay];
if (display >= 0 && display < displays.length) {
return displays[display];
} else {
return null;
}
@@ -2740,6 +2797,12 @@ class PeerInfo with ChangeNotifier {
}
return 1.0;
}
Rect? getDisplayRect(int display) {
final d = tryGetDisplayIfNotAllDisplay(display: display);
if (d == null) return null;
return Rect.fromLTWH(d.x, d.y, d.width.toDouble(), d.height.toDouble());
}
}
const canvasKey = 'canvas';

View File

@@ -117,9 +117,13 @@ class PlatformFFI {
? DynamicLibrary.open('librustdesk.so')
: isWindows
? DynamicLibrary.open('librustdesk.dll')
: isMacOS
? DynamicLibrary.open("liblibrustdesk.dylib")
: DynamicLibrary.process();
:
// Use executable itself as the dynamic library for MacOS.
// Multiple dylib instances will cause some global instances to be invalid.
// eg. `lazy_static` objects in rust side, will be created more than once, which is not expected.
//
// isMacOS? DynamicLibrary.open("liblibrustdesk.dylib") :
DynamicLibrary.process();
debugPrint('initializing FFI $_appType');
try {
_session_get_rgba = dylib.lookupFunction<F3Dart, F3>("session_get_rgba");
@@ -132,9 +136,10 @@ class PlatformFFI {
_ffiBind = RustdeskImpl(dylib);
if (isLinux) {
// Start a dbus service, no need to await
_ffiBind.mainStartDbusServer();
_ffiBind.mainStartPa();
if (isMain) {
// Start a dbus service for uri links, no need to await
_ffiBind.mainStartDbusServer();
}
} else if (isMacOS && isMain) {
// Start ipc service for uri links.
_ffiBind.mainStartIpcUrlServer();

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
@@ -176,6 +177,11 @@ class ServerModel with ChangeNotifier {
await timerCallback();
});
}
// Initial keyboard status is off on mobile
if (isMobile) {
bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
}
}
/// 1. check android permission
@@ -226,8 +232,7 @@ class ServerModel with ChangeNotifier {
_approveMode = approveMode;
update = true;
}
var stopped = option2bool(
"stop-service", await bind.mainGetOption(key: "stop-service"));
var stopped = await mainGetBoolOption(kOptionStopService);
final oldPwdText = _serverPasswd.text;
if (stopped ||
verificationMethod == kUsePermanentPassword ||
@@ -340,6 +345,20 @@ class ServerModel with ChangeNotifier {
return res;
}
Future<bool> checkFloatingWindowPermission() async {
debugPrint("androidVersion $androidVersion");
if (androidVersion < 23) {
return false;
}
if (await AndroidPermissionManager.check(kSystemAlertWindow)) {
debugPrint("alert window permission already granted");
return true;
}
var res = await AndroidPermissionManager.request(kSystemAlertWindow);
debugPrint("alert window permission request result: $res");
return res;
}
/// Toggle the screen sharing service.
toggleService() async {
if (_isStart) {
@@ -367,6 +386,9 @@ class ServerModel with ChangeNotifier {
}
} else {
await checkRequestNotificationPermission();
if (bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) != 'Y') {
await checkFloatingWindowPermission();
}
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
await AndroidPermissionManager.request(kManageExternalStorage);
}
@@ -405,7 +427,7 @@ class ServerModel with ChangeNotifier {
await bind.mainStartService();
updateClientState();
if (isAndroid) {
WakelockPlus.enable();
androidUpdatekeepScreenOn();
}
}
@@ -498,6 +520,7 @@ class ServerModel with ChangeNotifier {
}
if (_clients.length != oldClientLenght) {
notifyListeners();
if (isAndroid) androidUpdatekeepScreenOn();
}
}
@@ -532,6 +555,7 @@ class ServerModel with ChangeNotifier {
scrollToBottom();
notifyListeners();
if (isAndroid && !client.authorized) showLoginDialog(client);
if (isAndroid) androidUpdatekeepScreenOn();
} catch (e) {
debugPrint("Failed to call loginRequest,error:$e");
}
@@ -652,6 +676,7 @@ class ServerModel with ChangeNotifier {
final index = _clients.indexOf(client);
tabController.remove(index);
_clients.remove(client);
if (isAndroid) androidUpdatekeepScreenOn();
}
}
@@ -675,6 +700,7 @@ class ServerModel with ChangeNotifier {
if (desktopType == DesktopType.cm && _clients.isEmpty) {
hideCmWindow();
}
if (isAndroid) androidUpdatekeepScreenOn();
notifyListeners();
} catch (e) {
debugPrint("onClientRemove failed,error:$e");
@@ -686,6 +712,7 @@ class ServerModel with ChangeNotifier {
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
_clients.clear();
tabController.state.value.tabs.clear();
if (isAndroid) androidUpdatekeepScreenOn();
}
void jumpTo(int id) {
@@ -723,6 +750,27 @@ class ServerModel with ChangeNotifier {
debugPrint("updateVoiceCallState failed: $e");
}
}
void androidUpdatekeepScreenOn() async {
if (!isAndroid) return;
var floatingWindowDisabled =
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
!await AndroidPermissionManager.check(kSystemAlertWindow);
final keepScreenOn = floatingWindowDisabled
? KeepScreenOn.never
: optionToKeepScreenOn(
bind.mainGetLocalOption(key: kOptionKeepScreenOn));
final on = ((keepScreenOn == KeepScreenOn.serviceOn) && _isStart) ||
(keepScreenOn == KeepScreenOn.duringControlled &&
_clients.map((e) => !e.disconnected).isNotEmpty);
if (on != await WakelockPlus.enabled) {
if (on) {
WakelockPlus.enable();
} else {
WakelockPlus.disable();
}
}
}
}
enum ClientType {

View File

@@ -13,13 +13,14 @@ Future<ui.Image?> decodeImageFromPixels(
int? rowBytes,
int? targetWidth,
int? targetHeight,
VoidCallback? onPixelsCopied,
VoidCallback? onPixelsCopied, // must ensure onPixelsCopied is called no matter this function succeeds
bool allowUpscaling = true,
}) async {
if (targetWidth != null) {
assert(allowUpscaling || targetWidth <= width);
if (!(allowUpscaling || targetWidth <= width)) {
print("not allow upscaling but targetWidth > width");
onPixelsCopied?.call();
return null;
}
}
@@ -27,6 +28,7 @@ Future<ui.Image?> decodeImageFromPixels(
assert(allowUpscaling || targetHeight <= height);
if (!(allowUpscaling || targetHeight <= height)) {
print("not allow upscaling but targetHeight > height");
onPixelsCopied?.call();
return null;
}
}
@@ -36,6 +38,7 @@ Future<ui.Image?> decodeImageFromPixels(
buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
onPixelsCopied?.call();
} catch (e) {
onPixelsCopied?.call();
return null;
}

View File

@@ -59,15 +59,13 @@ class RustdeskImpl {
}
String sessionAddExistedSync(
{required String id, required UuidValue sessionId, dynamic hint}) {
{required String id,
required UuidValue sessionId,
required Int32List displays,
dynamic hint}) {
return '';
}
void sessionTryAddDisplay(
{required UuidValue sessionId,
required Int32List displays,
dynamic hint}) {}
String sessionAddSync(
{required UuidValue sessionId,
required String id,
@@ -94,6 +92,14 @@ class RustdeskImpl {
return Stream.empty();
}
Stream<EventToUI> sessionStartWithDisplays(
{required UuidValue sessionId,
required String id,
required Int32List displays,
dynamic hint}) {
throw UnimplementedError();
}
Future<bool?> sessionGetRemember(
{required UuidValue sessionId, dynamic hint}) {
return Future(
@@ -937,7 +943,7 @@ class RustdeskImpl {
throw UnimplementedError();
}
Future<String> mainVideoSaveDirectory({required bool root, dynamic hint}) {
String mainVideoSaveDirectory({required bool root, dynamic hint}) {
throw UnimplementedError();
}
@@ -1406,10 +1412,6 @@ class RustdeskImpl {
return false;
}
Future<void> mainStartPa({dynamic hint}) {
throw UnimplementedError();
}
bool mainHideDocker({dynamic hint}) {
throw UnimplementedError();
}
@@ -1612,5 +1614,13 @@ class RustdeskImpl {
throw UnimplementedError();
}
bool mainHasValidBotSync({dynamic hint}) {
throw UnimplementedError();
}
Future<String> mainVerifyBot({required String token, dynamic hint}) {
throw UnimplementedError();
}
void dispose() {}
}

View File

@@ -335,7 +335,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: 3535741662c5b7529e182227a277a8551aed3398
resolved-ref: "336308d86ec8b9640504a371b50ba500eb779363"
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
source: git
version: "0.1.0"
@@ -790,13 +790,13 @@ packages:
source: hosted
version: "0.2.1+1"
intl:
dependency: transitive
dependency: "direct overridden"
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.18.1"
version: "0.19.0"
io:
dependency: transitive
description:

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.2.5+39
version: 1.2.7+46
environment:
sdk: '^3.1.0'
@@ -79,7 +79,7 @@ dependencies:
git:
url: https://github.com/rustdesk-org/flutter_improved_scrolling
uni_links: ^0.5.1
uni_links_desktop: ^0.1.7
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
bot_toast: ^4.0.3
@@ -114,6 +114,9 @@ dev_dependencies:
flutter_lints: ^2.0.2
ffigen: ^8.0.2
dependency_overrides:
intl: ^0.19.0
# rerun: flutter pub run flutter_launcher_icons
flutter_icons:
image_path: "../res/icon.png"

View File

@@ -5,5 +5,5 @@ flutter pub get
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h
# call `flutter clean` if cargo build fails
# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/
cargo build --features "flutter,flutter_texture_render"
cargo build --features flutter
flutter run $@

View File

@@ -1,17 +0,0 @@
# RustDesk web
## Functions
### Current and planned
- [x] Outgoing
- [ ] Address book
- [ ] Force relay
### Unsupported
1. Incoming
2. LAN
3. Gpu texture render. We use WebGL instead.
### No plans

View File

@@ -1 +0,0 @@
<svg viewBox="0 0 375 375" style="width:32px;height:32px;margin:0 4px 4px 0" xmlns="http://www.w3.org/2000/svg"><rect transform="matrix(.91553 0 0 .91553 -152.92 116.76)" x="167.03" y="-127.54" width="409.6" height="409.6" rx="64" ry="64" fill="#0071ff"></rect><path d="M150.428 322.264c-29.063-6.202-53.897-22.439-73.115-47.804-19.507-25.746-27.838-55.355-25.723-91.414 6.655-62.013 47.667-106.753 99.687-120.411 4.509-.989 8.353-3.462 12.55-1.322 3.22 1.64 6.028 4.467 7.206 7.251 1.25 2.955 1.877 21.54.99 29.331-1.076 9.46-3.877 12.418-14.566 15.388-29.723 10.195-48.105 34.07-53.697 61.017-4.8 29.668 2.951 59.729 21.528 78.727 8.966 8.993 17.92 14.24 30.869 18.086 8.646 2.57 13.393 5.758 15.036 10.102 1.085 2.867 1.63 22.984.779 28.772-1.33 9.046-1.702 9.796-5.792 11.667-5.029 2.3-7.404 2.392-15.752.61zm50.708.29c-3.092-1.402-5.673-4.83-6.73-8.94-.134-9.408-2.366-25.754 1.02-33.373 1.88-4.128 4.65-5.999 12.433-8.396 21.267-6.551 37.593-19.88 46.806-38.213 11.11-22.108 11.877-55.183 1.808-77.975-9.154-20.723-25.7-35.217-48.555-42.534-8.872-2.84-12.004-5.065-12.968-9.21-1.002-4.31-1.435-19.87-.785-28.218.682-8.766 1.249-9.99 6.162-13.318 3.701-2.505 5.482-2.446 17.223.575 36.718 10.077 65.97 33.597 83.026 66.68 18.495 37.034 19.191 86.11 1.742 122.655-17.233 36.09-50.591 62.511-88.622 70.194-8.172 1.65-9.07 1.656-12.56.073z" fill="#fff"></path></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,19 +0,0 @@
export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY = 'Desktop session not ready';
export const LOGIN_MSG_DESKTOP_XSESSION_FAILED = 'Desktop xsession failed';
export const LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER = 'Desktop session another user login';
export const LOGIN_MSG_DESKTOP_XORG_NOT_FOUND = 'Desktop xorg not found';
// ls /usr/share/xsessions/
export const LOGIN_MSG_DESKTOP_NO_DESKTOP = 'Desktop none';
export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY =
'Desktop session not ready, password empty';
export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG =
'Desktop session not ready, password wrong';
export const LOGIN_MSG_PASSWORD_EMPTY = 'Empty Password';
export const LOGIN_MSG_PASSWORD_WRONG = 'Wrong Password';
export const LOGIN_MSG_2FA_WRONG = 'Wrong 2FA Code';
export const REQUIRE_2FA = '2FA Required';
export const LOGIN_MSG_NO_PASSWORD_ACCESS = 'No Password Access';
export const LOGIN_MSG_OFFLINE = 'Offline';
export const LOGIN_SCREEN_WAYLAND = 'Wayland login screen is not supported';
export const SCRAP_X11_REQUIRED = 'x11 expected';
export const SCRAP_X11_REF_URL = 'https://rustdesk.com/docs/en/manual/linux/#x11-required';

View File

@@ -1,792 +0,0 @@
import Connection from "./connection";
import PORT from "./connection";
import _sodium from "libsodium-wrappers";
import { CursorData } from "./message";
import { loadVp9 } from "./codec";
import { checkIfRetry, version } from "./gen_js_from_hbb";
import { initZstd, translate } from "./common";
import PCMPlayer from "pcm-player";
window.curConn = undefined;
window.isMobile = () => {
return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4));
}
export function isDesktop() {
return !isMobile();
}
export function msgbox(type, title, text, link) {
if (!type || (type == 'error' && !text)) return;
const text2 = text.toLowerCase();
var hasRetry = checkIfRetry(type, title, text) ? 'true' : '';
onGlobalEvent(JSON.stringify({ name: 'msgbox', type, title, text, link: link ?? '', hasRetry }));
}
function jsonfyForDart(payload) {
var tmp = {};
for (const [key, value] of Object.entries(payload)) {
if (!key) continue;
if (value instanceof String || typeof value == 'string') {
tmp[key] = value;
} else if (value instanceof Uint8Array) {
tmp[key] = '[' + value.toString() + ']';
} else {
tmp[key] = JSON.stringify(value);
}
}
return tmp;
}
export function pushEvent(name, payload) {
payload = jsonfyForDart(payload);
payload.name = name;
onGlobalEvent(JSON.stringify(payload));
}
let yuvWorker;
let yuvCanvas;
let gl;
let pixels;
let flipPixels;
let oldSize;
if (YUVCanvas.WebGLFrameSink.isAvailable()) {
var canvas = document.createElement('canvas');
yuvCanvas = YUVCanvas.attach(canvas, { webGL: true });
gl = canvas.getContext("webgl");
} else {
yuvWorker = new Worker("./yuv.js");
}
let testSpeed = [0, 0];
export function draw(display, frame) {
if (yuvWorker) {
// frame's (y/u/v).bytes already detached, can not transferrable any more.
yuvWorker.postMessage({ display, frame });
} else {
var tm0 = new Date().getTime();
yuvCanvas.drawFrame(frame);
var width = canvas.width;
var height = canvas.height;
var size = width * height * 4;
if (size != oldSize) {
pixels = new Uint8Array(size);
flipPixels = new Uint8Array(size);
oldSize = size;
}
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
const row = width * 4;
const end = (height - 1) * row;
for (let i = 0; i < size; i += row) {
flipPixels.set(pixels.subarray(i, i + row), end - i);
}
onRgba(display, flipPixels);
testSpeed[1] += new Date().getTime() - tm0;
testSpeed[0] += 1;
if (testSpeed[0] > 30) {
console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0]));
testSpeed = [0, 0];
}
}
/*
var testCanvas = document.getElementById("test-yuv-decoder-canvas");
if (testCanvas && currentFrame) {
var ctx = testCanvas.getContext("2d");
testCanvas.width = frame.format.displayWidth;
testCanvas.height = frame.format.displayHeight;
var img = ctx.createImageData(testCanvas.width, testCanvas.height);
img.data.set(currentFrame);
ctx.putImageData(img, 0, 0);
}
*/
}
export function sendOffCanvas(c) {
let canvas = c.transferControlToOffscreen();
yuvWorker.postMessage({ canvas }, [canvas]);
}
export function setConn(conn) {
window.curConn = conn;
}
export function getConn() {
return window.curConn;
}
export async function startConn(id) {
setByName('remote_id', id);
await curConn.start(id);
}
export function close() {
getConn()?.close();
setConn(undefined);
}
export function newConn() {
window.curConn?.close();
const conn = new Connection();
setConn(conn);
return conn;
}
let sodium;
export async function verify(signed, pk) {
if (!sodium) {
await _sodium.ready;
sodium = _sodium;
}
if (typeof pk == 'string') {
pk = decodeBase64(pk);
}
return sodium.crypto_sign_open(signed, pk);
}
export function decodeBase64(pk) {
return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL);
}
export function genBoxKeyPair() {
const pair = sodium.crypto_box_keypair();
const sk = pair.privateKey;
const pk = pair.publicKey;
return [sk, pk];
}
export function genSecretKey() {
return sodium.crypto_secretbox_keygen();
}
export function seal(unsigned, theirPk, ourSk) {
const nonce = Uint8Array.from(Array(24).fill(0));
return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk);
}
function makeOnce(value) {
var byteArray = Array(24).fill(0);
for (var index = 0; index < byteArray.length && value > 0; index++) {
var byte = value & 0xff;
byteArray[index] = byte;
value = (value - byte) / 256;
}
return Uint8Array.from(byteArray);
};
export function encrypt(unsigned, nonce, key) {
return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key);
}
export function decrypt(signed, nonce, key) {
return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key);
}
window.setByName = (name, value) => {
switch (name) {
case 'remote_id':
localStorage.setItem('remote-id', value);
break;
case 'connect':
newConn();
startConn(value);
break;
case 'login':
value = JSON.parse(value);
curConn.setRemember(value.remember);
curConn.login({
os_login: {
username: value.os_username,
password: value.os_password,
},
password: value.password,
});
break;
case 'close':
close();
break;
case 'refresh':
curConn.refresh();
break;
case 'reconnect':
curConn?.reconnect();
break;
case 'toggle_option':
curConn.toggleOption(value);
break;
case 'toggle_privacy_mode':
curConn.togglePrivacyMode(value);
break;
case 'image_quality':
curConn.setImageQuality(value);
break;
case 'lock_screen':
curConn.lockScreen();
break;
case 'ctrl_alt_del':
curConn.ctrlAltDel();
break;
case 'switch_display':
curConn.switchDisplay(value);
break;
case 'remove':
const peers = getPeers();
delete peers[value];
localStorage.setItem('peers', JSON.stringify(peers));
break;
case 'input_key':
value = JSON.parse(value);
curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
break;
case 'input_string':
curConn.inputString(value);
break;
case 'send_mouse':
if (!curConn) return;
let mask = 0;
value = JSON.parse(value);
switch (value.type) {
case 'down':
mask = 1;
break;
case 'up':
mask = 2;
break;
case 'wheel':
mask = 3;
break;
}
switch (value.buttons) {
case 'left':
mask |= 1 << 3;
break;
case 'right':
mask |= 2 << 3;
break;
case 'wheel':
mask |= 4 << 3;
}
curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
break;
case 'send_2fa':
curConn.send2fa(value);
break;
case 'option':
value = JSON.parse(value);
localStorage.setItem(value.name, value.value);
break;
case 'options':
value = JSON.parse(value);
for (const [key, value] of Object.entries(value)) {
localStorage.setItem(key, value);
}
break;
case 'option:local':
case 'option:flutter:local':
case 'option:flutter:peer':
value = JSON.parse(value);
localStorage.setItem(name + ':' + value.name, value.value);
break;
case 'option:user:default':
setUserDefaultOption(value);
break;
case 'option:session':
value = JSON.parse(value);
curConn.setOption(value.name, value.value);
break;
case 'option:peer':
setPeerOption(value);
break;
case 'option:toggle':
return curConn.toggleOption(value);
case 'input_os_password':
curConn.inputOsPassword(value);
break;
case 'session_add_sync':
return sessionAdd(value);
case 'session_start':
sessionStart(value);
break;
case 'session_close':
sessionClose(value);
break;
case 'elevate_with_logon':
curConn.elevateWithLogon(value);
break;
case 'forget':
curConn.setRemember(false);
break;
case 'peer_has_password':
const options = getPeers()[value] || {};
return (options['password'] ?? '') !== '';
case 'peer_exists':
return !(!getPeers()[value]);
case 'restart':
curConn.restart();
break;
case 'fav':
return localStorage.setItem('fav', value);
case 'query_onlines':
queryOnlines(value);
break;
case 'change_prefer_codec':
curConn.changePreferCodec(value);
case 'cursor':
setCustomCursor(value);
break;
default:
break;
}
}
window.getByName = (name, arg) => {
let v = _getByName(name, arg);
if (typeof v == 'string' || v instanceof String) return v;
if (v == undefined || v == null) return '';
return JSON.stringify(v);
}
function _getByName(name, arg) {
switch (name) {
case 'remote_id':
return localStorage.getItem('remote-id');
case 'remember':
return curConn.getRemember();
case 'toggle_option':
return curConn.getOption(arg) || false;
case 'option':
return localStorage.getItem(arg);
case 'options':
const keys = [
'custom-rendezvous-server',
'relay-server',
'api-server',
'key'
];
const obj = {};
keys.forEach(key => {
const v = localStorage.getItem(key);
if (v) obj[key] = v;
});
return JSON.stringify(obj);
case 'option:local':
case 'option:flutter:local':
case 'option:flutter:peer':
return localStorage.getItem(name + ':' + arg);
case 'image_quality':
return curConn.getImageQuality();
case 'translate':
arg = JSON.parse(arg);
return translate(arg.locale, arg.text);
case 'option:user:default':
return getUserDefaultOption(arg);
case 'option:session':
if (curConn) {
return curConn.getOption(arg);
} else {
return getUserDefaultOption(arg);
}
case 'option:peer':
return getPeerOption(arg);
case 'option:toggle':
return curConn.getToggleOption(arg);
case 'get_conn_status':
if (curConn) {
return curConn.getStatus();
} else {
return JSON.stringify({ status_num: 0 });
}
case 'test_if_valid_server':
break;
case 'version':
return version;
case 'load_recent_peers':
loadRecentPeers();
break;
case 'load_fav_peers':
loadFavPeers();
break;
case 'fav':
return localStorage.getItem('fav') ?? '[]';
case 'load_recent_peers_sync':
return JSON.stringify({
peers: JSON.stringify(getRecentPeers())
});
case 'api_server':
return getApiServer();
case 'is_using_public_server':
return !localStorage.getItem('custom-rendezvous-server');
case 'get_version_number':
return getVersionNumber(arg);
case 'audit_server':
return getAuditServer(arg);
case 'alternative_codecs':
return getAlternativeCodecs();
case 'screen_info':
return JSON.stringify({
frame: {
l: window.screenX,
t: window.screenY,
r: window.screenX + window.innerWidth,
b: window.screenY + window.innerHeight,
},
visibleFrame: {
l: window.screen.availLeft,
t: window.screen.availTop,
r: window.screen.availLeft + window.screen.availWidth,
b: window.screen.availTop + window.screen.availHeight,
},
scaleFactor: window.devicePixelRatio,
});
case 'main_display':
return JSON.stringify({
w: window.screen.availWidth,
h: window.screen.availHeight,
scaleFactor: window.devicePixelRatio,
});
}
return '';
}
let opusWorker = new Worker("./libopus.js");
let pcmPlayer;
export function initAudio(channels, sampleRate) {
pcmPlayer = newAudioPlayer(channels, sampleRate);
opusWorker.postMessage({ channels, sampleRate });
}
export function playAudio(packet) {
opusWorker.postMessage(packet, [packet.buffer]);
}
window.init = async () => {
if (yuvWorker) {
yuvWorker.onmessage = (e) => {
onRgba(e.data.display, e.data.frame);
}
}
opusWorker.onmessage = (e) => {
pcmPlayer.feed(e.data);
}
loadVp9(() => { });
await initZstd();
console.log('init done');
}
export function getPeers() {
return getJsonObj('peers');
}
export function getJsonObj(key) {
try {
return JSON.parse(localStorage.getItem(key)) || {};
} catch (e) {
return {};
}
}
function newAudioPlayer(channels, sampleRate) {
return new PCMPlayer({
channels,
sampleRate,
flushingTime: 2000
});
}
export function copyToClipboard(text) {
if (window.clipboardData && window.clipboardData.setData) {
// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
return window.clipboardData.setData("Text", text);
}
else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
var textarea = document.createElement("textarea");
textarea.textContent = text;
textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge.
document.body.appendChild(textarea);
textarea.select();
try {
return document.execCommand("copy"); // Security exception may be thrown by some browsers.
}
catch (ex) {
console.warn("Copy to clipboard failed.", ex);
// return prompt("Copy to clipboard: Ctrl+C, Enter", text);
}
finally {
document.body.removeChild(textarea);
}
}
}
// Dup to the function in hbb_common, lib.rs
// Maybe we need to move this function to js part.
export function getVersionNumber(v) {
try {
let versions = v.split('-');
let n = 0;
// The first part is the version number.
// 1.1.10 -> 1001100, 1.2.3 -> 1001030, multiple the last number by 10
// to leave space for patch version.
if (versions.length > 0) {
let last = 0;
for (let x of versions[0].split('.')) {
last = parseInt(x) || 0;
n = n * 1000 + last;
}
n -= last;
n += last * 10;
}
if (versions.length > 1) {
n += parseInt(versions[1]) || 0;
}
// Ignore the rest
return n;
}
catch (e) {
console.error('Failed to parse version number: "' + v + '" ' + e.message);
return 0;
}
}
// Set the cursor for the flutter-view element
function setCustomCursor(value) {
try {
const obj = JSON.parse(value);
// document querySelector or evaluate can not find the custom element
var body = document.body;
for (var i = 0; i < body.children.length; i++) {
var child = body.children[i];
if (child.tagName == 'FLUTTER-VIEW') {
child.style.cursor = `url(${obj.url}) ${obj.hotx} ${obj.hoty}, auto`;
}
}
} catch (e) {
console.error('Failed to set custom cursor: ' + e.message);
}
}
// ========================== options begin ==========================
function setUserDefaultOption(value) {
try {
const ojb = JSON.parse(value);
const userDefaultOptions = JSON.parse(localStorage.getItem('user-default-options')) || {};
userDefaultOptions[ojb.name] = ojb.value;
localStorage.setItem('user-default-options', JSON.stringify(userDefaultOptions));
}
catch (e) {
console.error('Failed to set user default options: ' + e.message);
}
}
export function getUserDefaultOption(value) {
const defaultOptions = {
'view_style': 'original',
'scroll_style': 'scrollauto',
'image_quality': 'balanced',
'codec-preference': 'auto',
'custom_image_quality': '50',
'custom-fps': '30',
};
try {
const userDefaultOptions = JSON.parse(localStorage.getItem('user-default-options')) || {};
return userDefaultOptions[value] || defaultOptions[value] || '';
}
catch (e) {
console.error('Failed to get user default options: ' + e.message);
return defaultOptions[value] || '';
}
}
function getPeerOption(value) {
try {
const obj = JSON.parse(value);
const options = getPeers()[obj.id] || {};
return options[obj.name] ?? getUserDefaultOption(obj.name);
}
catch (e) {
console.error('Failed to get peer option: "' + value + '", ' + e.message);
}
}
function setPeerOption(param) {
try {
const obj = JSON.parse(param);
const id = obj.id;
const name = obj.name;
const value = obj.value;
const peers = getPeers();
const options = peers[id] || {};
if (value == undefined) {
delete options[name];
} else {
options[name] = value;
}
options["tm"] = new Date().getTime();
peers[id] = options;
localStorage.setItem("peers", JSON.stringify(peers));
}
catch (e) {
console.error('Failed to set peer option: "' + value + '", ' + e.message);
}
}
// ========================= options end ===========================
// ========================== peers begin ==========================
function getRecentPeers() {
const peers = [];
for (const [id, value] of Object.entries(getPeers())) {
if (!id) continue;
const tm = value['tm'];
const info = value['info'];
const cardInfo = {
id: id,
username: info['username'] || '',
hostname: info['hostname'] || '',
platform: info['platform'] || '',
alias: value.alias || '',
};
if (!tm || !cardInfo) continue;
peers.push([tm, id, cardInfo]);
}
return peers.sort().reverse().map(x => x[2]);
}
function loadRecentPeers() {
const peersRecent = getRecentPeers();
if (peersRecent) {
onRegisteredEvent(JSON.stringify({ name: 'load_recent_peers', peers: JSON.stringify(peersRecent) }));
}
}
function loadFavPeers() {
try {
const fav = localStorage.getItem('fav') ?? '[]';
const favs = JSON.parse(fav);
const peersFav = getRecentPeers().filter(x => favs.includes(x.id));
if (peersFav) {
onRegisteredEvent(JSON.stringify({ name: 'load_fav_peers', peers: JSON.stringify(peersFav) }));
}
} catch (e) {
console.error('Failed to load fav peers: ' + e.message);
}
}
export function queryOnlines(value) {
// TODO: implement this
}
// ========================== peers end ===========================
// ========================== session begin ==========================
function sessionAdd(value) {
try {
const data = JSON.parse(value);
window.curConn?.close();
const conn = new Connection();
setConn(conn);
return '';
} catch (e) {
return e.message;
}
}
function sessionStart(value) {
try {
const conn = getConn();
if (!conn) {
return;
}
const data = JSON.parse(value);
if (data['id']) {
startConn(data['id']);
} else {
msgbox('error', 'Error', 'No id found in session data ' + value, '');
}
} catch (e) {
// TODO: better error handling
msgbox('error', 'Error', e.message, '');
}
}
function sessionClose(value) {
close();
}
// ========================== session end ===========================
// ========================== settings begin ==========================
function increasePort(host, offset) {
function isIPv6(str) {
const ipv6Pattern = /^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$/;
return ipv6Pattern.test(str);
}
if (isIPv6(host)) {
if (host.startsWith('[')) {
let tmp = host.split(']:');
if (tmp.length === 2) {
let port = parseInt(tmp[1]) || 0;
if (port > 0) {
return `${tmp[0]}]:${port + offset}`;
}
}
}
} else if (host.includes(':')) {
let tmp = host.split(':');
if (tmp.length === 2) {
let port = parseInt(tmp[1]) || 0;
if (port > 0) {
return `${tmp[0]}:${port + offset}`;
}
}
}
return host;
}
function getAlternativeCodecs() {
return JSON.stringify({
vp8: true,
av1: false,
h264: false,
h265: false,
});
}
// ========================== settings end ===========================
// ========================== server begin ==========================
function getApiServer() {
const api_server = localStorage.getItem('api-server');
if (api_server) {
return api_server;
}
const custom_rendezvous_server = localStorage.getItem('custom-rendezvous-server');
if (custom_rendezvous_server) {
let s = increasePort(custom_rendezvous_server, -2);
if (s == custom_rendezvous_server) {
return `http://${s}:${PORT - 2}`;
} else {
return `http://${s}`;
}
}
return 'https://admin.rustdesk.com';
}
function getAuditServer(typ) {
if (!localStorage.getItem("access_token")) {
return '';
}
const api_server = getApiServer();
if (!api_server || api_server.includes('rustdesk.com')) {
return '';
}
return api_server + '/api/audit/' + typ;
}
// ========================== server end ============================

1
flutter/web/v1/README.md Normal file
View File

@@ -0,0 +1 @@
v1 is not compatible with current Flutter source code.

View File

@@ -3,19 +3,19 @@
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "python ./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && python ./ts_proto.py && tsc && vite build",
"build": "./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && ./ts_proto.py && tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "^4.4.4",
"vite": "2.8"
"vite": "^2.7.2"
},
"dependencies": {
"fast-sha256": "^1.3.0",
"libsodium": "^0.7.9",
"libsodium-wrappers": "^0.7.9",
"pcm-player": "^0.0.11",
"ts-proto": "^1.169.1",
"ts-proto": "^1.101.0",
"wasm-feature-detect": "^1.2.11",
"zstddec": "^0.0.2"
}

View File

@@ -25,7 +25,7 @@ export async function loadVp9(callback) {
// Multithreading is used only if `options.threading` is true.
// This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs,
// currently available in Firefox and Chrome with experimental flags enabled.
// All major browsers disabled SharedArrayBuffer by default on January 5, 2018
// 所有主流浏览器均默认于2018年1月5日禁用SharedArrayBuffer
const isSIMD = await simd();
console.log('isSIMD: ' + isSIMD);
window.OGVLoader.loadClass(

View File

@@ -4,10 +4,9 @@ import * as rendezvous from "./rendezvous.js";
import { loadVp9 } from "./codec";
import * as sha256 from "fast-sha256";
import * as globals from "./globals";
import * as consts from "./consts";
import { decompress, mapKey, sleep } from "./common";
export const PORT = 21116;
const PORT = 21116;
const HOSTS = [
"rs-sg.rustdesk.com",
"rs-cn.rustdesk.com",
@@ -16,8 +15,8 @@ const HOSTS = [
let HOST = localStorage.getItem("rendezvous-server") || HOSTS[0];
const SCHEMA = "ws://";
type MsgboxCallback = (type: string, title: string, text: string, link: string) => void;
type DrawCallback = (display: number, data: Uint8Array) => void;
type MsgboxCallback = (type: string, title: string, text: string) => void;
type DrawCallback = (data: Uint8Array) => void;
//const cursorCanvas = document.createElement("canvas");
export default class Connection {
@@ -67,7 +66,7 @@ export default class Connection {
try {
this._password = Uint8Array.from(JSON.parse("[" + p + "]"));
} catch (e) {
console.error('Failed to get password, ' + e);
console.error(e);
}
}
}
@@ -171,7 +170,7 @@ export default class Connection {
pk = undefined;
}
} catch (e) {
console.error('Failed to verify id pk, ', e);
console.error(e);
pk = undefined;
}
if (!pk)
@@ -196,7 +195,7 @@ export default class Connection {
try {
signedId = await globals.verify(signedId.id, Uint8Array.from(pk!));
} catch (e) {
console.error('Failed to verify signed id pk, ', e);
console.error(e);
// fall back to non-secure connection in case pk mismatch
console.error("pk mismatch, fall back to non-secure");
const public_key = message.PublicKey.fromPartial({});
@@ -243,12 +242,26 @@ export default class Connection {
this.login();
} else if (msg?.test_delay) {
const test_delay = msg?.test_delay;
console.log('test delay: ', test_delay);
console.log(test_delay);
if (!test_delay.from_client) {
this._ws?.sendMessage({ test_delay });
}
} else if (msg?.login_response) {
this.handleLoginResponse(msg?.login_response);
const r = msg?.login_response;
if (r.error) {
if (r.error == "Wrong Password") {
this._password = undefined;
this.msgbox(
"re-input-password",
r.error,
"Do you want to enter again?"
);
} else {
this.msgbox("error", "Login Error", r.error);
}
} else if (r.peer_info) {
this.handlePeerInfo(r.peer_info);
}
} else if (msg?.video_frame) {
this.handleVideoFrame(msg?.video_frame!);
} else if (msg?.clipboard) {
@@ -261,7 +274,7 @@ export default class Connection {
try {
globals.copyToClipboard(new TextDecoder().decode(cb.content));
} catch (e) {
console.error('Failed to copy to clipboard, ', e);
console.error(e);
}
// globals.pushEvent("clipboard", cb);
} else if (msg?.cursor_data) {
@@ -305,110 +318,13 @@ export default class Connection {
}
}
handleLoginResponse(response: message.LoginResponse) {
const loginErrorMap: Record<string, any> = {
[consts.LOGIN_SCREEN_WAYLAND]: {
msgtype: "error",
title: "Login Error",
text: "Login screen using Wayland is not supported",
link: "https://rustdesk.com/docs/en/manual/linux/#login-screen",
try_again: true,
},
[consts.LOGIN_MSG_DESKTOP_SESSION_NOT_READY]: {
msgtype: "session-login",
title: "",
text: "",
link: "",
try_again: true,
},
[consts.LOGIN_MSG_DESKTOP_XSESSION_FAILED]: {
msgtype: "session-re-login",
title: "",
text: "",
link: "",
try_again: true,
},
[consts.LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER]: {
msgtype: "info-nocancel",
title: "another_user_login_title_tip",
text: "another_user_login_text_tip",
link: "",
try_again: false,
},
[consts.LOGIN_MSG_DESKTOP_XORG_NOT_FOUND]: {
msgtype: "info-nocancel",
title: "xorg_not_found_title_tip",
text: "xorg_not_found_text_tip",
link: "https://rustdesk.com/docs/en/manual/linux/#login-screen",
try_again: true,
},
[consts.LOGIN_MSG_DESKTOP_NO_DESKTOP]: {
msgtype: "info-nocancel",
title: "no_desktop_title_tip",
text: "no_desktop_text_tip",
link: "https://rustdesk.com/docs/en/manual/linux/#login-screen",
try_again: true,
},
[consts.LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY]: {
msgtype: "session-login-password",
title: "",
text: "",
link: "",
try_again: true,
},
[consts.LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG]: {
msgtype: "session-login-re-password",
title: "",
text: "",
link: "",
try_again: true,
},
[consts.LOGIN_MSG_NO_PASSWORD_ACCESS]: {
msgtype: "wait-remote-accept-nook",
title: "Prompt",
text: "Please wait for the remote side to accept your session request...",
link: "",
try_again: true,
},
};
const err = response.error;
if (err) {
if (err == consts.LOGIN_MSG_PASSWORD_EMPTY) {
this._password = undefined;
this.msgbox("input-password", "Password Required", "", "");
}
if (err == consts.LOGIN_MSG_PASSWORD_WRONG) {
this._password = undefined;
this.msgbox(
"re-input-password",
err,
"Do you want to enter again?"
);
} else if (err == consts.LOGIN_MSG_2FA_WRONG || err == consts.REQUIRE_2FA) {
this.msgbox("input-2fa", err, "");
} else if (err in loginErrorMap) {
const m = loginErrorMap[err];
this.msgbox(m.msgtype, m.title, m.text, m.link);
} else {
if (err.includes(consts.SCRAP_X11_REQUIRED)) {
this.msgbox("error", "Login Error", err, consts.SCRAP_X11_REF_URL);
} else {
this.msgbox("error", "Login Error", err);
}
}
} else if (response.peer_info) {
this.handlePeerInfo(response.peer_info);
}
msgbox(type_: string, title: string, text: string) {
this._msgbox?.(type_, title, text);
}
msgbox(type_: string, title: string, text: string, link: string = '') {
this._msgbox?.(type_, title, text, link);
}
draw(display: number, frame: any) {
this._draw?.(display, frame);
globals.draw(display, frame);
draw(frame: any) {
this._draw?.(frame);
globals.draw(frame);
}
close() {
@@ -431,55 +347,38 @@ export default class Connection {
this._draw = callback;
}
login(info?: {
os_login?: message.OSLogin,
password?: Uint8Array
}) {
if (info?.password) {
login(password: string | undefined = undefined) {
if (password) {
const salt = this._hash?.salt;
let p = hash([info.password, salt!]);
let p = hash([password, salt!]);
this._password = p;
const challenge = this._hash?.challenge;
p = hash([p, challenge!]);
this.msgbox("connecting", "Connecting...", "Logging in...");
this._sendLoginMessage({ os_login: info.os_login, password: p });
this._sendLoginMessage(p);
} else {
let p = this._password;
if (p) {
const challenge = this._hash?.challenge;
p = hash([p, challenge!]);
}
this._sendLoginMessage({ os_login: info?.os_login, password: p });
this._sendLoginMessage(p);
}
}
changePreferCodec() {
const supported_decoding = message.SupportedDecoding.fromPartial({
ability_vp9: 1,
ability_h264: 1,
});
const option = message.OptionMessage.fromPartial({ supported_decoding });
const misc = message.Misc.fromPartial({ option });
this._ws?.sendMessage({ misc });
}
async reconnect() {
this.close();
await this.start(this._id);
}
_sendLoginMessage(login: {
os_login?: message.OSLogin,
password?: Uint8Array,
}) {
_sendLoginMessage(password: Uint8Array | undefined = undefined) {
const login_request = message.LoginRequest.fromPartial({
username: this._id!,
my_id: "web", // to-do
my_name: "web", // to-do
password: login.password,
password,
option: this.getOptionMessage(),
video_ack_required: true,
os_login: login.os_login,
});
this._ws?.sendMessage({ login_request });
}
@@ -536,7 +435,7 @@ export default class Connection {
i++;
if (i == n) this.sendVideoReceived();
if (ok && dec.frameBuffer && n == i) {
this.draw(vf.display, dec.frameBuffer);
this.draw(dec.frameBuffer);
const now = new Date().getTime();
var elapsed = now - tm;
this._videoTestSpeed[1] += elapsed;
@@ -544,9 +443,9 @@ export default class Connection {
if (this._videoTestSpeed[0] >= 30) {
console.log(
"video decoder: " +
parseInt(
"" + this._videoTestSpeed[1] / this._videoTestSpeed[0]
)
parseInt(
"" + this._videoTestSpeed[1] / this._videoTestSpeed[0]
)
);
this._videoTestSpeed = [0, 0];
}
@@ -557,17 +456,8 @@ export default class Connection {
}
handlePeerInfo(pi: message.PeerInfo) {
localStorage.setItem('last_remote_id', this._id);
this._peerInfo = pi;
if (pi.current_display > pi.displays.length) {
pi.current_display = 0;
}
if (globals.getVersionNumber(pi.version) < globals.getVersionNumber("1.1.10")) {
this.setPermission("restart", false);
}
if (pi.displays.length == 0) {
this.setOption("info", pi);
globals.pushEvent("update_privacy_mode", {});
this.msgbox("error", "Remote Error", "No Display");
return;
}
@@ -577,7 +467,6 @@ export default class Connection {
if (p) this.inputOsPassword(p);
const username = this.getOption("info")?.username;
if (username && !pi.username) pi.username = username;
globals.pushEvent("update_privacy_mode", {});
this.setOption("info", pi);
if (this.getRemember()) {
if (this._password?.length) {
@@ -592,10 +481,6 @@ export default class Connection {
}
}
setPermission(name: string, value: Boolean) {
globals.pushEvent("permission", { [name]: value });
}
shouldAutoLogin(): string {
const l = this.getOption("lock-after-session-end");
const a = !!this.getOption("auto-login");
@@ -631,7 +516,7 @@ export default class Connection {
default:
return;
}
this.setPermission(name, p.enabled);
globals.pushEvent("permission", { [name]: p.enabled });
} else if (misc.switch_display) {
this.loadVideoDecoder();
globals.pushEvent("switch_display", misc.switch_display);
@@ -652,27 +537,7 @@ export default class Connection {
}
getOption(name: string): any {
return this._options[name] ?? globals.getUserDefaultOption(name);
}
getToggleOption(name: string): Boolean {
// TODO: more default settings
const defaultToggleTrue = [
'show-remote-cursor',
'privacy-mode',
'enable-file-copy-paste',
'allow_swap_key',
];
return this._options[name] || (defaultToggleTrue.includes(name) ? true : false);
}
// TODO:
getStatus(): String {
return JSON.stringify({ status_num: 10 });
}
// TODO:
checkConnStatus() {
return this._options[name];
}
setOption(name: string, value: any) {
@@ -727,70 +592,17 @@ export default class Connection {
this._ws?.sendMessage({ key_event });
}
restart() {
const misc = message.Misc.fromPartial({});
misc.restart_remote_device = true;
this._ws?.sendMessage({ misc });
}
inputString(seq: string) {
const key_event = message.KeyEvent.fromPartial({ seq });
this._ws?.sendMessage({ key_event });
}
send2fa(code: string) {
const auth_2fa = message.Auth2FA.fromPartial({ code });
this._ws?.sendMessage({ auth_2fa });
}
_captureDisplays({ add, sub, set }: {
add?: number[], sub?: number[], set?: number[]
}) {
const capture_displays = message.CaptureDisplays.fromPartial({ add, sub, set });
const misc = message.Misc.fromPartial({ capture_displays });
switchDisplay(display: number) {
const switch_display = message.SwitchDisplay.fromPartial({ display });
const misc = message.Misc.fromPartial({ switch_display });
this._ws?.sendMessage({ misc });
}
switchDisplay(v: string) {
try {
const obj = JSON.parse(v);
const value = obj.value;
const isDesktop = obj.isDesktop;
if (value.length == 1) {
const switch_display = message.SwitchDisplay.fromPartial({ display: value[0] });
const misc = message.Misc.fromPartial({ switch_display });
this._ws?.sendMessage({ misc });
if (!isDesktop) {
this._captureDisplays({ set: value });
} else {
// If support merging images, check_remove_unused_displays() in ui_session_interface.rs
}
} else {
this._captureDisplays({ set: value });
}
}
catch (e) {
console.log('Failed to switch display, invalid param "' + v + '"');
}
}
elevateWithLogon(value: string) {
try {
const obj = JSON.parse(value);
const logon = message.ElevationRequestWithLogon.fromPartial({
username: obj.username,
password: obj.password
});
const elevation_request = message.ElevationRequest.fromPartial({ logon });
const misc = message.Misc.fromPartial({ elevation_request });
this._ws?.sendMessage({ misc });
}
catch (e) {
console.log('Failed to elevate with logon, invalid param "' + value + '"');
}
}
async inputOsPassword(seq: string) {
this.inputMouse();
await sleep(50);
@@ -839,52 +651,6 @@ export default class Connection {
}
toggleOption(name: string) {
// } else if name == "block-input" {
// option.block_input = BoolOption::Yes.into();
// } else if name == "unblock-input" {
// option.block_input = BoolOption::No.into();
// } else if name == "show-quality-monitor" {
// config.show_quality_monitor.v = !config.show_quality_monitor.v;
// } else if name == "allow_swap_key" {
// config.allow_swap_key.v = !config.allow_swap_key.v;
// } else if name == "view-only" {
// config.view_only.v = !config.view_only.v;
// let f = |b: bool| {
// if b {
// BoolOption::Yes.into()
// } else {
// BoolOption::No.into()
// }
// };
// if config.view_only.v {
// option.disable_keyboard = f(true);
// option.disable_clipboard = f(true);
// option.show_remote_cursor = f(true);
// option.enable_file_transfer = f(false);
// option.lock_after_session_end = f(false);
// } else {
// option.disable_keyboard = f(false);
// option.disable_clipboard = f(self.get_toggle_option("disable-clipboard"));
// option.show_remote_cursor = f(self.get_toggle_option("show-remote-cursor"));
// option.enable_file_transfer = f(self.config.enable_file_transfer.v);
// option.lock_after_session_end = f(self.config.lock_after_session_end.v);
// }
// } else {
// let is_set = self
// .options
// .get(&name)
// .map(|o| !o.is_empty())
// .unwrap_or(false);
// if is_set {
// self.config.options.remove(&name);
// } else {
// self.config.options.insert(name, "Y".to_owned());
// }
// self.config.store(&self.id);
// return None;
// }
const v = !this._options[name];
const option = message.OptionMessage.fromPartial({});
const v2 = v
@@ -906,43 +672,13 @@ export default class Connection {
case "privacy-mode":
option.privacy_mode = v2;
break;
case "enable-file-copy-paste":
option.enable_file_transfer = v2;
break;
case "block-input":
option.block_input = message.OptionMessage_BoolOption.Yes;
break;
case "unblock-input":
option.block_input = message.OptionMessage_BoolOption.No;
break;
case "show-quality-monitor":
case "allow-swap-key":
break;
case "view-only":
if (v) {
option.disable_keyboard = message.OptionMessage_BoolOption.Yes;
option.disable_clipboard = message.OptionMessage_BoolOption.Yes;
option.show_remote_cursor = message.OptionMessage_BoolOption.Yes;
option.enable_file_transfer = message.OptionMessage_BoolOption.No;
option.lock_after_session_end = message.OptionMessage_BoolOption.No;
} else {
option.disable_keyboard = message.OptionMessage_BoolOption.No;
option.disable_clipboard = this.getToggleOption("disable-clipboard")
? message.OptionMessage_BoolOption.Yes
: message.OptionMessage_BoolOption.No;
option.show_remote_cursor = this.getToggleOption("show-remote-cursor")
? message.OptionMessage_BoolOption.Yes
: message.OptionMessage_BoolOption.No;
option.enable_file_transfer = this.getToggleOption("enable-file-copy-paste")
? message.OptionMessage_BoolOption.Yes
: message.OptionMessage_BoolOption.No;
option.lock_after_session_end = this.getToggleOption("lock-after-session-end")
? message.OptionMessage_BoolOption.Yes
: message.OptionMessage_BoolOption.No;
}
break;
default:
this.setOption(name, this._options[name] ? undefined : "Y");
return;
}
if (name.indexOf("block-input") < 0) this.setOption(name, v);
@@ -950,20 +686,6 @@ export default class Connection {
this._ws?.sendMessage({ misc });
}
togglePrivacyMode(value: string) {
try {
const obj = JSON.parse(value);
const toggle_privacy_mode = message.TogglePrivacyMode.fromPartial({
impl_key: obj.impl_key,
on: obj.on,
});
const misc = message.Misc.fromPartial({ toggle_privacy_mode });
this._ws?.sendMessage({ misc });
} catch (e) {
console.log('Failed to toggle privacy mode, invalid param "' + value + '"')
}
}
getImageQuality() {
return this.getOption("image-quality");
}
@@ -998,7 +720,7 @@ export default class Connection {
loadVp9((decoder: any) => {
this._videoDecoder = decoder;
console.log("vp9 loaded");
console.log('The decoder: ', decoder);
console.log(decoder);
});
}
}

View File

@@ -0,0 +1,383 @@
import Connection from "./connection";
import _sodium from "libsodium-wrappers";
import { CursorData } from "./message";
import { loadVp9 } from "./codec";
import { checkIfRetry, version } from "./gen_js_from_hbb";
import { initZstd, translate } from "./common";
import PCMPlayer from "pcm-player";
window.curConn = undefined;
window.isMobile = () => {
return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4));
}
export function isDesktop() {
return !isMobile();
}
export function msgbox(type, title, text) {
if (!type || (type == 'error' && !text)) return;
const text2 = text.toLowerCase();
var hasRetry = checkIfRetry(type, title, text) ? 'true' : '';
onGlobalEvent(JSON.stringify({ name: 'msgbox', type, title, text, hasRetry }));
}
function jsonfyForDart(payload) {
var tmp = {};
for (const [key, value] of Object.entries(payload)) {
if (!key) continue;
tmp[key] = value instanceof Uint8Array ? '[' + value.toString() + ']' : JSON.stringify(value);
}
return tmp;
}
export function pushEvent(name, payload) {
payload = jsonfyForDart(payload);
payload.name = name;
onGlobalEvent(JSON.stringify(payload));
}
let yuvWorker;
let yuvCanvas;
let gl;
let pixels;
let flipPixels;
let oldSize;
if (YUVCanvas.WebGLFrameSink.isAvailable()) {
var canvas = document.createElement('canvas');
yuvCanvas = YUVCanvas.attach(canvas, { webGL: true });
gl = canvas.getContext("webgl");
} else {
yuvWorker = new Worker("./yuv.js");
}
let testSpeed = [0, 0];
export function draw(frame) {
if (yuvWorker) {
// frame's (y/u/v).bytes already detached, can not transferrable any more.
yuvWorker.postMessage(frame);
} else {
var tm0 = new Date().getTime();
yuvCanvas.drawFrame(frame);
var width = canvas.width;
var height = canvas.height;
var size = width * height * 4;
if (size != oldSize) {
pixels = new Uint8Array(size);
flipPixels = new Uint8Array(size);
oldSize = size;
}
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
const row = width * 4;
const end = (height - 1) * row;
for (let i = 0; i < size; i += row) {
flipPixels.set(pixels.subarray(i, i + row), end - i);
}
onRgba(flipPixels);
testSpeed[1] += new Date().getTime() - tm0;
testSpeed[0] += 1;
if (testSpeed[0] > 30) {
console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0]));
testSpeed = [0, 0];
}
}
/*
var testCanvas = document.getElementById("test-yuv-decoder-canvas");
if (testCanvas && currentFrame) {
var ctx = testCanvas.getContext("2d");
testCanvas.width = frame.format.displayWidth;
testCanvas.height = frame.format.displayHeight;
var img = ctx.createImageData(testCanvas.width, testCanvas.height);
img.data.set(currentFrame);
ctx.putImageData(img, 0, 0);
}
*/
}
export function sendOffCanvas(c) {
let canvas = c.transferControlToOffscreen();
yuvWorker.postMessage({ canvas }, [canvas]);
}
export function setConn(conn) {
window.curConn = conn;
}
export function getConn() {
return window.curConn;
}
export async function startConn(id) {
setByName('remote_id', id);
await curConn.start(id);
}
export function close() {
getConn()?.close();
setConn(undefined);
}
export function newConn() {
window.curConn?.close();
const conn = new Connection();
setConn(conn);
return conn;
}
let sodium;
export async function verify(signed, pk) {
if (!sodium) {
await _sodium.ready;
sodium = _sodium;
}
if (typeof pk == 'string') {
pk = decodeBase64(pk);
}
return sodium.crypto_sign_open(signed, pk);
}
export function decodeBase64(pk) {
return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL);
}
export function genBoxKeyPair() {
const pair = sodium.crypto_box_keypair();
const sk = pair.privateKey;
const pk = pair.publicKey;
return [sk, pk];
}
export function genSecretKey() {
return sodium.crypto_secretbox_keygen();
}
export function seal(unsigned, theirPk, ourSk) {
const nonce = Uint8Array.from(Array(24).fill(0));
return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk);
}
function makeOnce(value) {
var byteArray = Array(24).fill(0);
for (var index = 0; index < byteArray.length && value > 0; index++) {
var byte = value & 0xff;
byteArray[index] = byte;
value = (value - byte) / 256;
}
return Uint8Array.from(byteArray);
};
export function encrypt(unsigned, nonce, key) {
return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key);
}
export function decrypt(signed, nonce, key) {
return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key);
}
window.setByName = (name, value) => {
switch (name) {
case 'remote_id':
localStorage.setItem('remote-id', value);
break;
case 'connect':
newConn();
startConn(value);
break;
case 'login':
value = JSON.parse(value);
curConn.setRemember(value.remember == 'true');
curConn.login(value.password);
break;
case 'close':
close();
break;
case 'refresh':
curConn.refresh();
break;
case 'reconnect':
curConn.reconnect();
break;
case 'toggle_option':
curConn.toggleOption(value);
break;
case 'image_quality':
curConn.setImageQuality(value);
break;
case 'lock_screen':
curConn.lockScreen();
break;
case 'ctrl_alt_del':
curConn.ctrlAltDel();
break;
case 'switch_display':
curConn.switchDisplay(value);
break;
case 'remove':
const peers = getPeers();
delete peers[value];
localStorage.setItem('peers', JSON.stringify(peers));
break;
case 'input_key':
value = JSON.parse(value);
curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
break;
case 'input_string':
curConn.inputString(value);
break;
case 'send_mouse':
let mask = 0;
value = JSON.parse(value);
switch (value.type) {
case 'down':
mask = 1;
break;
case 'up':
mask = 2;
break;
case 'wheel':
mask = 3;
break;
}
switch (value.buttons) {
case 'left':
mask |= 1 << 3;
break;
case 'right':
mask |= 2 << 3;
break;
case 'wheel':
mask |= 4 << 3;
}
curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
break;
case 'option':
value = JSON.parse(value);
localStorage.setItem(value.name, value.value);
break;
case 'peer_option':
value = JSON.parse(value);
curConn.setOption(value.name, value.value);
break;
case 'input_os_password':
curConn.inputOsPassword(value);
break;
default:
break;
}
}
window.getByName = (name, arg) => {
let v = _getByName(name, arg);
if (typeof v == 'string' || v instanceof String) return v;
if (v == undefined || v == null) return '';
return JSON.stringify(v);
}
function getPeersForDart() {
const peers = [];
for (const [id, value] of Object.entries(getPeers())) {
if (!id) continue;
const tm = value['tm'];
const info = value['info'];
if (!tm || !info) continue;
peers.push([tm, id, info]);
}
return peers.sort().reverse().map(x => x.slice(1));
}
function _getByName(name, arg) {
switch (name) {
case 'peers':
return getPeersForDart();
case 'remote_id':
return localStorage.getItem('remote-id');
case 'remember':
return curConn.getRemember();
case 'toggle_option':
return curConn.getOption(arg) || false;
case 'option':
return localStorage.getItem(arg);
case 'image_quality':
return curConn.getImageQuality();
case 'translate':
arg = JSON.parse(arg);
return translate(arg.locale, arg.text);
case 'peer_option':
return curConn.getOption(arg);
case 'test_if_valid_server':
break;
case 'version':
return version;
}
return '';
}
let opusWorker = new Worker("./libopus.js");
let pcmPlayer;
export function initAudio(channels, sampleRate) {
pcmPlayer = newAudioPlayer(channels, sampleRate);
opusWorker.postMessage({ channels, sampleRate });
}
export function playAudio(packet) {
opusWorker.postMessage(packet, [packet.buffer]);
}
window.init = async () => {
if (yuvWorker) {
yuvWorker.onmessage = (e) => {
onRgba(e.data);
}
}
opusWorker.onmessage = (e) => {
pcmPlayer.feed(e.data);
}
loadVp9(() => { });
await initZstd();
console.log('init done');
}
export function getPeers() {
try {
return JSON.parse(localStorage.getItem('peers')) || {};
} catch (e) {
return {};
}
}
function newAudioPlayer(channels, sampleRate) {
return new PCMPlayer({
channels,
sampleRate,
flushingTime: 2000
});
}
export function copyToClipboard(text) {
if (window.clipboardData && window.clipboardData.setData) {
// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
return window.clipboardData.setData("Text", text);
}
else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
var textarea = document.createElement("textarea");
textarea.textContent = text;
textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge.
document.body.appendChild(textarea);
textarea.select();
try {
return document.execCommand("copy"); // Security exception may be thrown by some browsers.
}
catch (ex) {
console.warn("Copy to clipboard failed.", ex);
// return prompt("Copy to clipboard: Ctrl+C, Enter", text);
}
finally {
document.body.removeChild(textarea);
}
}
}

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