146 Commits
1.2.5 ... 1.2.6

Author SHA1 Message Date
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
179 changed files with 3610 additions and 2512 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

View File

@@ -11,10 +11,12 @@ on:
default: "nightly"
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"
@@ -22,7 +24,7 @@ env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.03.25
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
VERSION: "1.2.5"
VERSION: "1.2.6"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -84,7 +86,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"
@@ -810,7 +812,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 +854,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 }}
@@ -1564,6 +1573,7 @@ jobs:
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.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

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

@@ -0,0 +1,216 @@
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.13.9"
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.6"
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-rustdesk-android:
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: vedantmgoyal2009/winget-releaser@v2
with:
identifier: RustDesk.RustDesk
version: ${{ github.event.release.tag_name }}

35
Cargo.lock generated
View File

@@ -2917,7 +2917,7 @@ dependencies = [
"tokio",
"tokio-native-tls",
"tokio-rustls 0.26.0",
"tokio-socks 0.5.2",
"tokio-socks 0.5.2-1",
"tokio-util",
"toml 0.7.8",
"url",
@@ -3037,8 +3037,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hwcodec"
version = "0.4.15"
source = "git+https://github.com/21pages/hwcodec#1d504ee590c15472813fecc22cee4b8149b2b8cd"
version = "0.4.18"
source = "git+https://github.com/21pages/hwcodec#b84d5bbefa949194d1fc51a5c7e9d7988e315ce6"
dependencies = [
"bindgen 0.59.2",
"cc",
@@ -4327,7 +4327,7 @@ dependencies = [
[[package]]
name = "pam"
version = "0.7.0"
source = "git+https://github.com/fufesou/pam#10da2cbbabe32cbc9de22a66abe44738e7ec0ea0"
source = "git+https://github.com/fufesou/pam#3a2aaa6e07b176d8e2d66a5eec38d2ddb45f009f"
dependencies = [
"libc",
"pam-macros",
@@ -4349,8 +4349,7 @@ dependencies = [
[[package]]
name = "pam-sys"
version = "1.0.0-alpha4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e9dfd42858f6a6bb1081079fd9dc259ca3e2aaece6cb689fd36b1058046c969"
source = "git+https://github.com/fufesou/pam-sys?branch=fix/v1.0.0-alpha4_gnuc_va_list#3337c9bb9a9c68d7497ec8c93cad2368c26091b7"
dependencies = [
"bindgen 0.59.2",
"libc",
@@ -4383,8 +4382,8 @@ dependencies = [
[[package]]
name = "parity-tokio-ipc"
version = "0.7.3-3"
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#e8448ade10d6d972d0b2307646424b36ab202ff5"
version = "0.7.3-4"
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#3623ec9ebef50c9b118e03b03df831008a4d1441"
dependencies = [
"futures",
"libc",
@@ -5289,7 +5288,7 @@ dependencies = [
[[package]]
name = "rustdesk"
version = "1.2.5"
version = "1.2.6"
dependencies = [
"android-wakelock",
"android_logger",
@@ -5386,7 +5385,7 @@ dependencies = [
[[package]]
name = "rustdesk-portable-packer"
version = "1.2.5"
version = "1.2.6"
dependencies = [
"brotli",
"dirs 5.0.1",
@@ -6128,7 +6127,7 @@ dependencies = [
[[package]]
name = "tao"
version = "0.25.0"
source = "git+https://github.com/rustdesk-org/tao?branch=dev#1cad16b200485bbccc67dcee2d339eac6e1c16ad"
source = "git+https://github.com/rustdesk-org/tao?branch=dev#288c219cb0527e509590c2b2d8e7072aa9feb2d3"
dependencies = [
"bitflags 1.3.2",
"cc",
@@ -6168,7 +6167,7 @@ dependencies = [
[[package]]
name = "tao-macros"
version = "0.1.2"
source = "git+https://github.com/rustdesk-org/tao?branch=dev#1cad16b200485bbccc67dcee2d339eac6e1c16ad"
source = "git+https://github.com/rustdesk-org/tao?branch=dev#288c219cb0527e509590c2b2d8e7072aa9feb2d3"
dependencies = [
"proc-macro2 1.0.79",
"quote 1.0.35",
@@ -6359,9 +6358,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.37.0"
version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [
"backtrace",
"bytes",
@@ -6378,9 +6377,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2 1.0.79",
"quote 1.0.35",
@@ -6432,8 +6431,8 @@ dependencies = [
[[package]]
name = "tokio-socks"
version = "0.5.2"
source = "git+https://github.com/rustdesk-org/tokio-socks#51037c93f8be34196fd2b6de9f674e8dfae3d01e"
version = "0.5.2-1"
source = "git+https://github.com/rustdesk-org/tokio-socks#94e97c6d7c93b0bcbfa54f2dc397c1da0a6e43d3"
dependencies = [
"bytes",
"either",

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk"
version = "1.2.5"
version = "1.2.6"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.2.5
version: 1.2.6
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.6
exec: usr/lib/rustdesk/rustdesk
exec_args: $@
apt:

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

@@ -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

@@ -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>

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,21 +905,27 @@ 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),
@@ -1008,7 +1028,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) {
@@ -1421,14 +1442,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 +1466,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 +1503,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,27 +2690,40 @@ 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;
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: [
@@ -2745,12 +2780,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 +2802,6 @@ Widget buildErrorBanner(BuildContext context,
message: translate(err.value),
child: Text(
translate(err.value),
overflow: TextOverflow.ellipsis,
),
)).marginSymmetric(vertical: 2),
),
@@ -2922,10 +2954,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);
}
@@ -3372,3 +3405,27 @@ get defaultOptionNo => isCustomClient ? 'N' : '';
get defaultOptionWhitelist => isCustomClient ? ',' : '';
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
// `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

@@ -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

@@ -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;
}
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.value.dy;
}
// reset
if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() {
_position = Offset(_position.dx, _saveHeight);
position.value = Offset(position.value.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.value.dy > contextHeight) {
final y = contextHeight - sumHeight;
setState(() {
_position = Offset(_position.dx, y);
position.value = Offset(position.value.dx, y);
});
}
}
@@ -435,14 +526,14 @@ class IOSDraggableState extends State<IOSDraggable> {
return Stack(
children: [
Positioned(
left: _position.dx,
top: _position.dy,
left: position.value.dx,
top: position.value.dy,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
_position += details.delta;
position.value += details.delta;
});
_chatModel?.setChatWindowPosition(_position);
_chatModel?.setChatWindowPosition(position.value);
},
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

@@ -887,7 +887,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 +943,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 +1048,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

@@ -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

@@ -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

@@ -583,8 +583,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 +595,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 +610,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'))));
}

View File

@@ -133,9 +133,17 @@ 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 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";

View File

@@ -671,7 +671,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(() {});

View File

@@ -315,11 +315,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 +413,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,
)
],
];
@@ -504,9 +510,9 @@ class _GeneralState extends State<_General> {
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;

View File

@@ -36,12 +36,24 @@ 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;
@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();
WidgetsBinding.instance.addObserver(this);
Get.put<DesktopTabController>(tabController);
RemoteCountState.init();
tabController.add(TabInfo(
@@ -68,8 +80,10 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
@override
void dispose() {
super.dispose();
WidgetsBinding.instance.removeObserver(this);
Get.delete<DesktopTabController>();
super.dispose();
}
@override
@@ -89,12 +103,17 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
),
),
)));
widget() => MouseRegion(
onEnter: (_) async {
await shouldBeBlocked(_block, canBeBlocked);
},
child: FocusScope(child: tabWidget, canRequestFocus: !_block.value));
return isMacOS || kUseCompatibleUiMode
? tabWidget
? Obx(() => widget())
: Obx(
() => DragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
child: tabWidget,
child: widget(),
),
);
}

View File

@@ -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;
@@ -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,
@@ -617,10 +622,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 +644,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 +671,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

@@ -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';
@@ -2351,7 +2336,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 +2347,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 +2363,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 +2452,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

@@ -320,16 +320,7 @@ class DesktopTab extends StatelessWidget {
if (tabType != DesktopTabType.main) {
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);
});
return buildRemoteBlock(child: child, use: canBeBlocked);
}
List<Widget> _tabWidgets = [];
@@ -397,6 +388,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);
}

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

@@ -82,13 +82,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 +106,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.
@@ -419,17 +419,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,7 +483,11 @@ 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,
@@ -679,6 +687,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 +712,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 +738,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 +849,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),
@@ -901,7 +934,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(),

View File

@@ -854,6 +854,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

@@ -35,10 +35,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;
@@ -58,6 +89,27 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
super.initState();
WidgetsBinding.instance.addObserver(this);
_enableAbr = option2bool(
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
_onlyWhiteList = (bind.mainGetOptionSync(key: kOptionWhitelist)) !=
defaultOptionWhitelist;
_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);
() async {
var update = false;
@@ -86,67 +138,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 +166,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(() {});
}
@@ -301,10 +292,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 +305,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,6 +479,56 @@ 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 settings = SettingsList(
sections: [
@@ -533,8 +569,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 +587,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 +604,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
@@ -919,7 +949,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 +1003,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

@@ -917,10 +917,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 +975,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 +1189,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 +1578,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 +1626,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 +1706,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 +1724,7 @@ class PredefinedCursor {
img2.Image? _image2;
CursorData? _cache;
String png;
int id;
String id;
double Function(double)? hotxGetter;
double Function(double)? hotyGetter;
@@ -1775,13 +1782,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 +1804,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 +1844,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 +1875,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 +2046,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 +2071,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 +2453,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 +2475,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 +2511,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 +2561,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 +2753,7 @@ class PeerInfo with ChangeNotifier {
bool get isSupportMultiDisplay =>
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
bool get forceTextureRender => currentDisplay == kAllDisplayValue;
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
@@ -2693,30 +2762,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 +2811,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

@@ -132,9 +132,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';
@@ -226,8 +227,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 +340,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 +381,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 +422,7 @@ class ServerModel with ChangeNotifier {
await bind.mainStartService();
updateClientState();
if (isAndroid) {
WakelockPlus.enable();
androidUpdatekeepScreenOn();
}
}
@@ -498,6 +515,7 @@ class ServerModel with ChangeNotifier {
}
if (_clients.length != oldClientLenght) {
notifyListeners();
if (isAndroid) androidUpdatekeepScreenOn();
}
}
@@ -532,6 +550,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 +671,7 @@ class ServerModel with ChangeNotifier {
final index = _clients.indexOf(client);
tabController.remove(index);
_clients.remove(client);
if (isAndroid) androidUpdatekeepScreenOn();
}
}
@@ -675,6 +695,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 +707,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 +745,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();
}

View File

@@ -335,7 +335,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: 3535741662c5b7529e182227a277a8551aed3398
resolved-ref: "60773827434eefe6d01eefa814dca9a032b970b3"
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.6+44
environment:
sdk: '^3.1.0'
@@ -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);
}
}
}

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

@@ -0,0 +1 @@
Under dev.

View File

@@ -9,7 +9,7 @@ edition = "2018"
[dependencies]
flexi_logger = { version = "0.27", features = ["async"] }
protobuf = { version = "3.4", features = ["with-bytes"] }
tokio = { version = "1.37", features = ["full"] }
tokio = { version = "1.38", features = ["full"] }
tokio-util = { version = "0.7", features = ["full"] }
futures = "0.3"
bytes = { version = "1.6", features = ["serde"] }

View File

@@ -912,7 +912,7 @@ impl Config {
#[inline]
fn purify_options(v: &mut HashMap<String, String>) {
v.retain(|k, _| is_option_can_save(&OVERWRITE_SETTINGS, k));
v.retain(|k, v| is_option_can_save(&OVERWRITE_SETTINGS, k, &DEFAULT_SETTINGS, v));
}
pub fn set_options(mut v: HashMap<String, String>) {
@@ -936,7 +936,7 @@ impl Config {
}
pub fn set_option(k: String, v: String) {
if !is_option_can_save(&OVERWRITE_SETTINGS, &k) {
if !is_option_can_save(&OVERWRITE_SETTINGS, &k, &DEFAULT_SETTINGS, &v) {
return;
}
let mut config = CONFIG2.write().unwrap();
@@ -1284,6 +1284,7 @@ impl PeerConfig {
keys::OPTION_TOUCH_MODE,
keys::OPTION_I444,
keys::OPTION_SWAP_LEFT_RIGHT_MOUSE,
keys::OPTION_COLLAPSE_TOOLBAR,
]
.map(|key| {
mp.insert(key.to_owned(), UserDefaultConfig::read(key));
@@ -1449,7 +1450,7 @@ impl LocalConfig {
}
pub fn set_option(k: String, v: String) {
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k) {
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k, &DEFAULT_LOCAL_SETTINGS, &v) {
return;
}
let mut config = LOCAL_CONFIG.write().unwrap();
@@ -1553,40 +1554,6 @@ impl LanPeers {
}
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct HwCodecConfig {
#[serde(default, deserialize_with = "deserialize_string")]
pub ram: String,
#[serde(default, deserialize_with = "deserialize_string")]
pub vram: String,
}
impl HwCodecConfig {
pub fn load() -> HwCodecConfig {
Config::load_::<HwCodecConfig>("_hwcodec")
}
pub fn store(&self) {
Config::store_(self, "_hwcodec");
}
pub fn clear() {
HwCodecConfig::default().store();
}
pub fn clear_ram() {
let mut c = Self::load();
c.ram = Default::default();
c.store();
}
pub fn clear_vram() {
let mut c = Self::load();
c.vram = Default::default();
c.store();
}
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct UserDefaultConfig {
#[serde(default, deserialize_with = "deserialize_hashmap_string_string")]
@@ -1636,7 +1603,12 @@ impl UserDefaultConfig {
}
pub fn set(&mut self, key: String, value: String) {
if !is_option_can_save(&OVERWRITE_DISPLAY_SETTINGS, &key) {
if !is_option_can_save(
&OVERWRITE_DISPLAY_SETTINGS,
&key,
&DEFAULT_DISPLAY_SETTINGS,
&value,
) {
return;
}
if value.is_empty() {
@@ -1959,8 +1931,15 @@ fn get_or(
}
#[inline]
fn is_option_can_save(overwrite: &RwLock<HashMap<String, String>>, k: &str) -> bool {
if overwrite.read().unwrap().contains_key(k) {
fn is_option_can_save(
overwrite: &RwLock<HashMap<String, String>>,
k: &str,
defaults: &RwLock<HashMap<String, String>>,
v: &str,
) -> bool {
if overwrite.read().unwrap().contains_key(k)
|| defaults.read().unwrap().get(k).map_or(false, |x| x == v)
{
return false;
}
true
@@ -2018,6 +1997,23 @@ pub fn is_disable_installation() -> bool {
is_some_hard_opton("disable-installation")
}
// This function must be kept the same as the one in flutter and sciter code.
// flutter: flutter/lib/common.dart -> option2bool()
// sciter: Does not have the function, but it should be kept the same.
pub fn option2bool(option: &str, value: &str) -> bool {
if option.starts_with("enable-") {
value != "N"
} else if option.starts_with("allow-")
|| option == "stop-service"
|| option == keys::OPTION_DIRECT_SERVER
|| option == "force-always-relay"
{
value == "Y"
} else {
value != "N"
}
}
pub mod keys {
pub const OPTION_VIEW_ONLY: &str = "view_only";
pub const OPTION_SHOW_MONITORS_TOOLBAR: &str = "show_monitors_toolbar";
@@ -2083,6 +2079,14 @@ pub mod keys {
pub const OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless";
pub const OPTION_ENABLE_HWCODEC: &str = "enable-hwcodec";
pub const OPTION_APPROVE_MODE: &str = "approve-mode";
pub const OPTION_CUSTOM_RENDEZVOUS_SERVER: &str = "custom-rendezvous-server";
pub const OPTION_API_SERVER: &str = "api-server";
pub const OPTION_KEY: &str = "key";
pub const OPTION_PRESET_ADDRESS_BOOK_NAME: &str = "preset-address-book-name";
pub const OPTION_PRESET_ADDRESS_BOOK_TAG: &str = "preset-address-book-tag";
pub const OPTION_ENABLE_DIRECTX_CAPTURE: &str = "enable-directx-capture";
pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str =
"enable-android-software-encoding-half-scale";
// flutter local options
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
@@ -2093,6 +2097,19 @@ pub mod keys {
pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type";
pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name";
// android floating window options
pub const OPTION_DISABLE_FLOATING_WINDOW: &str = "disable-floating-window";
pub const OPTION_FLOATING_WINDOW_SIZE: &str = "floating-window-size";
pub const OPTION_FLOATING_WINDOW_UNTOUCHABLE: &str = "floating-window-untouchable";
pub const OPTION_FLOATING_WINDOW_TRANSPARENCY: &str = "floating-window-transparency";
pub const OPTION_FLOATING_WINDOW_SVG: &str = "floating-window-svg";
// android keep screen on
pub const OPTION_KEEP_SCREEN_ON: &str = "keep-screen-on";
pub const OPTION_DISABLE_GROUP_PANEL: &str = "disable-group-panel";
pub const OPTION_PRE_ELEVATE_SERVICE: &str = "pre-elevate-service";
// proxy settings
// The following options are not real keys, they are just used for custom client advanced settings.
// The real keys are in Config2::socks.
@@ -2148,6 +2165,14 @@ pub mod keys {
OPTION_FLUTTER_PEER_TAB_VISIBLE,
OPTION_FLUTTER_PEER_CARD_UI_TYLE,
OPTION_FLUTTER_CURRENT_AB_NAME,
OPTION_DISABLE_FLOATING_WINDOW,
OPTION_FLOATING_WINDOW_SIZE,
OPTION_FLOATING_WINDOW_UNTOUCHABLE,
OPTION_FLOATING_WINDOW_TRANSPARENCY,
OPTION_FLOATING_WINDOW_SVG,
OPTION_KEEP_SCREEN_ON,
OPTION_DISABLE_GROUP_PANEL,
OPTION_PRE_ELEVATE_SERVICE,
];
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS
pub const KEYS_SETTINGS: &[&str] = &[
@@ -2179,9 +2204,28 @@ pub mod keys {
OPTION_PROXY_URL,
OPTION_PROXY_USERNAME,
OPTION_PROXY_PASSWORD,
OPTION_CUSTOM_RENDEZVOUS_SERVER,
OPTION_API_SERVER,
OPTION_KEY,
OPTION_PRESET_ADDRESS_BOOK_NAME,
OPTION_PRESET_ADDRESS_BOOK_TAG,
OPTION_ENABLE_DIRECTX_CAPTURE,
OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE,
];
}
pub fn common_load<
T: serde::Serialize + serde::de::DeserializeOwned + Default + std::fmt::Debug,
>(
suffix: &str,
) -> T {
Config::load_::<T>(suffix)
}
pub fn common_store<T: serde::Serialize>(config: &T, suffix: &str) {
Config::store_(config, suffix);
}
#[cfg(test)]
mod tests {
use super::*;
@@ -2254,7 +2298,18 @@ mod tests {
res.insert("c".to_owned(), "d".to_string());
res.insert("d".to_owned(), "cc".to_string());
Config::purify_options(&mut res);
DEFAULT_SETTINGS
.write()
.unwrap()
.insert("f".to_string(), "c".to_string());
Config::purify_options(&mut res);
assert!(res.len() == 2);
DEFAULT_SETTINGS
.write()
.unwrap()
.insert("f".to_string(), "a".to_string());
Config::purify_options(&mut res);
assert!(res.len() == 1);
let res = Config::get_options();
assert!(res["a"] == "b");
assert!(res["c"] == "f");

View File

@@ -79,7 +79,7 @@ pub fn approve_mode() -> ApproveMode {
pub fn hide_cm() -> bool {
approve_mode() == ApproveMode::Password
&& verification_method() == VerificationMethod::OnlyUsePermanentPassword
&& !Config::get_option("allow-hide-cm").is_empty()
&& crate::config::option2bool("allow-hide-cm", &Config::get_option("allow-hide-cm"))
}
const VERSION_LEN: usize = 2;

View File

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

View File

@@ -287,13 +287,17 @@ mod hw {
let mut mid_data = Vec::new();
let mut counter = 0;
let mut time_sum = Duration::ZERO;
let start = std::time::Instant::now();
loop {
match c.frame(std::time::Duration::from_millis(30)) {
Ok(frame) => {
let tmp_timer = Instant::now();
let frame = frame.to(encoder.yuvfmt(), &mut yuv, &mut mid_data).unwrap();
let yuv = frame.yuv().unwrap();
for ref frame in encoder.encode(&yuv).unwrap() {
for ref frame in encoder
.encode(&yuv, start.elapsed().as_millis() as _)
.unwrap()
{
size += frame.data.len();
h26xs.push(frame.data.to_vec());

View File

@@ -209,19 +209,38 @@ pub fn clear_codec_info() {
*MEDIA_CODEC_INFOS.write().unwrap() = None;
}
// another way to fix "reference table overflow" error caused by new_string and call_main_service_pointer_input frequently calld
// is below, but here I change kind from string to int for performance
/*
env.with_local_frame(10, || {
let kind = env.new_string(kind)?;
env.call_method(
ctx,
"rustPointerInput",
"(Ljava/lang/String;III)V",
&[
JValue::Object(&JObject::from(kind)),
JValue::Int(mask),
JValue::Int(x),
JValue::Int(y),
],
)?;
Ok(JObject::null())
})?;
*/
pub fn call_main_service_pointer_input(kind: &str, mask: i32, x: i32, y: i32) -> JniResult<()> {
if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(),
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
) {
let mut env = jvm.attach_current_thread_as_daemon()?;
let kind = env.new_string(kind)?;
let kind = if kind == "touch" { 0 } else { 1 };
env.call_method(
ctx,
"rustPointerInput",
"(Ljava/lang/String;III)V",
"(IIII)V",
&[
JValue::Object(&JObject::from(kind)),
JValue::Int(kind),
JValue::Int(mask),
JValue::Int(x),
JValue::Int(y),
@@ -259,19 +278,22 @@ pub fn call_main_service_get_by_name(name: &str) -> JniResult<String> {
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
) {
let mut env = jvm.attach_current_thread_as_daemon()?;
let name = env.new_string(name)?;
let res = env
.call_method(
ctx,
"rustGetByName",
"(Ljava/lang/String;)Ljava/lang/String;",
&[JValue::Object(&JObject::from(name))],
)?
.l()?;
let res = JString::from(res);
let res = env.get_string(&res)?;
let res = res.to_string_lossy().to_string();
return Ok(res);
let res = env.with_local_frame(10, |env| -> JniResult<String> {
let name = env.new_string(name)?;
let res = env
.call_method(
ctx,
"rustGetByName",
"(Ljava/lang/String;)Ljava/lang/String;",
&[JValue::Object(&JObject::from(name))],
)?
.l()?;
let res = JString::from(res);
let res = env.get_string(&res)?;
let res = res.to_string_lossy().to_string();
Ok(res)
})?;
Ok(res)
} else {
return Err(JniError::ThrowFailed(-1));
}
@@ -287,20 +309,23 @@ pub fn call_main_service_set_by_name(
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
) {
let mut env = jvm.attach_current_thread_as_daemon()?;
let name = env.new_string(name)?;
let arg1 = env.new_string(arg1.unwrap_or(""))?;
let arg2 = env.new_string(arg2.unwrap_or(""))?;
env.with_local_frame(10, |env| -> JniResult<()> {
let name = env.new_string(name)?;
let arg1 = env.new_string(arg1.unwrap_or(""))?;
let arg2 = env.new_string(arg2.unwrap_or(""))?;
env.call_method(
ctx,
"rustSetByName",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
&[
JValue::Object(&JObject::from(name)),
JValue::Object(&JObject::from(arg1)),
JValue::Object(&JObject::from(arg2)),
],
)?;
env.call_method(
ctx,
"rustSetByName",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
&[
JValue::Object(&JObject::from(name)),
JValue::Object(&JObject::from(arg1)),
JValue::Object(&JObject::from(arg2)),
],
)?;
Ok(())
})?;
return Ok(());
} else {
return Err(JniError::ThrowFailed(-1));

View File

@@ -182,3 +182,8 @@ fn get_size() -> Option<(u16, u16, u16)> {
}
None
}
pub fn is_start() -> Option<bool> {
let res = call_main_service_get_by_name("is_start").ok()?;
Some(res == "true")
}

View File

@@ -308,6 +308,8 @@ impl EncoderApi for AomEncoder {
fn is_hardware(&self) -> bool {
false
}
fn disable(&self) {}
}
impl AomEncoder {

View File

@@ -21,8 +21,8 @@ use crate::{
use hbb_common::{
anyhow::anyhow,
bail,
config::PeerConfig,
log,
config::{keys::OPTION_ENABLE_HWCODEC, option2bool, Config, PeerConfig},
lazy_static, log,
message_proto::{
supported_decoding::PreferCodec, video_frame, Chroma, CodecAbility, EncodedVideoFrames,
SupportedDecoding, SupportedEncoding, VideoFrame,
@@ -31,8 +31,6 @@ use hbb_common::{
tokio::time::Instant,
ResultType,
};
#[cfg(any(feature = "hwcodec", feature = "mediacodec", feature = "vram"))]
use hbb_common::{config::Config2, lazy_static};
lazy_static::lazy_static! {
static ref PEER_DECODINGS: Arc<Mutex<HashMap<i32, SupportedDecoding>>> = Default::default();
@@ -76,6 +74,8 @@ pub trait EncoderApi {
fn latency_free(&self) -> bool;
fn is_hardware(&self) -> bool;
fn disable(&self);
}
pub struct Encoder {
@@ -144,11 +144,7 @@ impl Encoder {
}),
Err(e) => {
log::error!("new hw encoder failed: {e:?}, clear config");
#[cfg(target_os = "android")]
crate::android::ffi::clear_codec_info();
#[cfg(not(target_os = "android"))]
hbb_common::config::HwCodecConfig::clear_ram();
Self::update(EncodingUpdate::Check);
HwCodecConfig::clear(false, true);
*ENCODE_CODEC_FORMAT.lock().unwrap() = CodecFormat::VP9;
Err(e)
}
@@ -160,8 +156,7 @@ impl Encoder {
}),
Err(e) => {
log::error!("new vram encoder failed: {e:?}, clear config");
hbb_common::config::HwCodecConfig::clear_vram();
Self::update(EncodingUpdate::Check);
HwCodecConfig::clear(true, true);
*ENCODE_CODEC_FORMAT.lock().unwrap() = CodecFormat::VP9;
Err(e)
}
@@ -204,7 +199,7 @@ impl Encoder {
#[allow(unused_mut)]
let mut h265vram_encoding = false;
#[cfg(feature = "vram")]
if enable_vram_option() {
if enable_vram_option(true) {
if _all_support_h264_decoding {
if VRamEncoder::available(CodecFormat::H264).len() > 0 {
h264vram_encoding = true;
@@ -343,7 +338,7 @@ impl Encoder {
encoding.h265 |= HwRamEncoder::try_get(CodecFormat::H265).is_some();
}
#[cfg(feature = "vram")]
if enable_vram_option() {
if enable_vram_option(true) {
encoding.h264 |= VRamEncoder::available(CodecFormat::H264).len() > 0;
encoding.h265 |= VRamEncoder::available(CodecFormat::H265).len() > 0;
}
@@ -454,7 +449,7 @@ impl Decoder {
};
}
#[cfg(feature = "vram")]
if enable_vram_option() && _use_texture_render {
if enable_vram_option(false) && _use_texture_render {
decoding.ability_h264 |= if VRamDecoder::available(CodecFormat::H264, _luid).len() > 0 {
1
} else {
@@ -533,7 +528,7 @@ impl Decoder {
}
CodecFormat::H264 => {
#[cfg(feature = "vram")]
if !valid && enable_vram_option() && _luid.clone().unwrap_or_default() != 0 {
if !valid && enable_vram_option(false) && _luid.clone().unwrap_or_default() != 0 {
match VRamDecoder::new(format, _luid) {
Ok(v) => h264_vram = Some(v),
Err(e) => log::error!("create H264 vram decoder failed: {}", e),
@@ -559,7 +554,7 @@ impl Decoder {
}
CodecFormat::H265 => {
#[cfg(feature = "vram")]
if !valid && enable_vram_option() && _luid.clone().unwrap_or_default() != 0 {
if !valid && enable_vram_option(false) && _luid.clone().unwrap_or_default() != 0 {
match VRamDecoder::new(format, _luid) {
Ok(v) => h265_vram = Some(v),
Err(e) => log::error!("create H265 vram decoder failed: {}", e),
@@ -842,19 +837,37 @@ impl Decoder {
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
pub fn enable_hwcodec_option() -> bool {
if cfg!(windows) || cfg!(target_os = "linux") || cfg!(target_os = "android") {
if let Some(v) = Config2::get().options.get("enable-hwcodec") {
return v != "N";
}
return true; // default is true
return option2bool(
OPTION_ENABLE_HWCODEC,
&Config::get_option(OPTION_ENABLE_HWCODEC),
);
}
false
}
#[cfg(feature = "vram")]
pub fn enable_vram_option() -> bool {
if let Some(v) = Config2::get().options.get("enable-hwcodec") {
return v != "N";
pub fn enable_vram_option(encode: bool) -> bool {
if cfg!(windows) {
let enable = option2bool(
OPTION_ENABLE_HWCODEC,
&Config::get_option(OPTION_ENABLE_HWCODEC),
);
if encode {
enable && enable_directx_capture()
} else {
enable
}
} else {
false
}
return true; // default is true
}
#[cfg(windows)]
pub fn enable_directx_capture() -> bool {
use hbb_common::config::keys::OPTION_ENABLE_DIRECTX_CAPTURE as OPTION;
option2bool(
OPTION,
&Config::get_option(hbb_common::config::keys::OPTION_ENABLE_DIRECTX_CAPTURE),
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

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