Compare commits

...

338 Commits
1.2.6 ... 1.3.0

Author SHA1 Message Date
flusheDData
2a0fd55af7 Update es.rs (#9114)
New terms added
2024-08-19 14:41:28 +08:00
RustDesk
da70cbcdda Update es.rs 2024-08-19 14:19:24 +08:00
flusheDData
921b64e1e0 Last tips (#9113)
* Update es.rs

New translations

* Update es.rs

Translation correction of 'Version'

* Update es.rs

Last tips

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-08-19 14:14:47 +08:00
XLion
c0de0aa108 Update tw.rs (#9112) 2024-08-19 14:13:31 +08:00
fufesou
715d475f49 fix: privacy mode 2, more error info (#9111)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-19 11:35:35 +08:00
Mr-Update
e3f09b3ec6 Update de.rs (#9107) 2024-08-19 03:12:56 +08:00
rustdesk
0a5fafb84f comment 2024-08-18 10:04:20 +08:00
rustdesk
4e084c5ee0 revert 2024-08-18 09:56:24 +08:00
rustdesk
d1fe617670 fixing https://github.com/rustdesk/rustdesk/issues/9103, maybe 2024-08-18 09:30:53 +08:00
bovirus
7744bdbbe0 Update Italian language (#9093) 2024-08-16 18:39:36 +08:00
FastAct
e1329c8157 Update nl.rs (#9092) 2024-08-16 15:19:18 +08:00
fufesou
f31e60af5b fix: crash, drop tokio RunTime in async context (#9091)
* fix: crash, drop tokio RunTime in async context

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

* chore

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-16 14:55:42 +08:00
21pages
ed18e3c786 file rename (#9089)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-16 12:55:58 +08:00
rustdesk
579e0fac36 fix https://github.com/rustdesk/rustdesk/issues/9088 2024-08-16 12:20:40 +08:00
fufesou
92752765ba fix: resize top edge (#9081)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-15 23:58:19 +08:00
kibeb
071f51cf6f Update cs.rs: more corrections, new phrases translated (#9075) 2024-08-15 17:34:39 +08:00
kibeb
dde3cce120 Update cs.rs: typo corrected (#9074) 2024-08-15 14:55:44 +08:00
21pages
bb1b9858d5 only main window show tabbar border, change cm tabbar color (#9073)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-15 13:25:59 +08:00
fufesou
a31c27be73 fix: windows, remote window, resizing, #9061 (#9062)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-15 13:24:42 +08:00
21pages
85ae3916cb fix get_custom_server_from_string, relay= is not used (#9069)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-15 09:57:12 +08:00
jkh0kr
cc9b7e64eb Update ko.rs (#9067)
Additional Korean translations
2024-08-15 09:25:03 +08:00
flusheDData
5e22a49e49 Translation correction (#9064)
* Update es.rs

New translations

* Update es.rs

Translation correction of 'Version'
2024-08-15 09:24:42 +08:00
21pages
07cf1b4db5 fix wallpaper get sid (#9058)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-14 18:09:47 +08:00
RustDesk
f6ab5cdcb2 Update config.toml (#9057) 2024-08-14 17:32:05 +08:00
Mr-Update
b477aded0b Update de.rs (#9054) 2024-08-14 10:39:05 +08:00
rustdesk
65318efd67 update README 2024-08-13 21:42:04 +08:00
bovirus
dbd195a46e Update Italian language (#9047) 2024-08-13 14:22:08 +08:00
flusheDData
0651ad492f Update es.rs (#9045)
New translations
2024-08-13 14:11:36 +08:00
fufesou
a3c5adb1f4 refact: remove dup translation (#9043)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-13 11:27:39 +08:00
fufesou
a771abcdc2 fix: win, multi monitors, maximize, may fix 9033 (#9038)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-13 00:18:35 +08:00
21pages
b8b3a089f3 android telebot setting (#9035)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-12 22:20:35 +08:00
fufesou
8f00067266 fix: build (#9036)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-12 22:15:59 +08:00
fufesou
83bf067d18 fix: not plug virtual dislay, non win, installed (#9034)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-12 21:53:41 +08:00
21pages
1729ee337f trust this device to skip 2fa (#9012)
* trust this device to skip 2fa

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

* Update connection.rs

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-08-12 18:08:33 +08:00
Xp96
57834840b8 Update ptbr.rs (#9031) 2024-08-12 17:22:00 +08:00
fufesou
99d7b62d79 fix: privacy mode 2, resolution changed (#9027)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-11 23:28:02 +08:00
fufesou
6625aca994 fix: win, virtual display (#9023)
1. Default resolution 1920x1080.
2. Restore on conn & disconn.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-11 19:26:41 +08:00
FastAct
ce56be6507 Update nl.rs (#9022) 2024-08-11 06:49:52 +08:00
mehdi-song
fd69b14623 Update fa.rs (#9021) 2024-08-11 06:49:34 +08:00
bovirus
7521bbe15f Update Italian language (#9016) 2024-08-10 20:52:27 +08:00
fufesou
3c6ddd7403 fix: multi-displays, displays changed, switch idx (#9006)
Use init display index as the primary index.

But when displays changed, the primary display may also changes.

No need to change the old primary index.
But we need to make sure that the old primary index does not exceed the display number.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-09 21:17:06 +08:00
Mr-Update
6820e2f4c7 Update de.rs (#9004) 2024-08-09 08:43:18 +08:00
Ivan Beà
77f3ebaf1a Update ca.rs (#8997)
* Update ca.rs

update catalan translation

* Update ca.rs

minor changes
2024-08-08 23:18:01 +08:00
fufesou
e7e244d4f2 fix: update pub desktop_multi_window (#9002)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-08 23:06:33 +08:00
fufesou
f4c40d733e Fix/exe upgrade options (#9001)
* fix: exe upgrade, use previous options

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

* refact: msi, shortcuts options, swap pos

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-08 22:07:06 +08:00
fufesou
049c334db3 fix: privacy mode, windows vd, init resolution (#8992)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-08 15:31:11 +08:00
rustdesk
6197832317 update hwcodec 2024-08-08 11:58:12 +08:00
Vasyl Gello
2fd53f9825 Pass JVM to ffmpeg (#8985)
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-08-08 11:53:49 +08:00
Yevhen Popok
ae16b8975b Update Ukrainian translation (#8980)
Sync with #8977
2024-08-08 10:24:40 +08:00
solokot
171177c76f Update ru.rs (#8987) 2024-08-08 10:24:28 +08:00
bovirus
ade1d8c0c7 Update Italian language (#8988) 2024-08-08 10:24:15 +08:00
rustdesk
9a194f0850 res/users.py 2024-08-07 23:26:17 +08:00
21pages
76d5a8b205 unlock with PIN (#8977)
* add custom password to unlock settings

* If not set, use admin password; if set, use custom settings password.
* At least 4 characters.
* Set with gui or command line.

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

* Update cn.rs

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-08-07 16:21:38 +08:00
jkh0kr
bc6ce6c7ee Update ko.rs (#8974)
Additional Korean translation
2024-08-07 10:12:43 +08:00
rustdesk
025cdfa25b fix ci 2024-08-07 01:19:29 +08:00
rustdesk
2f432e941d hide-tray option 2024-08-07 01:08:36 +08:00
21pages
96edca8f74 update sysinfo rlim_max, which causing debian 13 pkexec not work (#8968)
* update sysinfo rlim_max, which causing debian 13 pkexec not work

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

* Update Cargo.toml

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-08-06 18:19:35 +08:00
fufesou
51b250435d refact: audio input, combobox instead of radio (#8965)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-06 17:07:05 +08:00
21pages
5a2121501d fix build (#8964)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-06 12:36:39 +08:00
rustdesk
877b3e2ce5 fix ci 2024-08-06 12:19:06 +08:00
rustdesk
421ddc0016 fix https://github.com/rustdesk/rustdesk/issues/4863 2024-08-06 12:11:03 +08:00
fufesou
2662abc5a3 fix: reset voice call state, on conn (#8961)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-06 00:28:31 +08:00
fufesou
b3e1c8a907 Refact/msi more install options (#8949)
* refact: msi, more install options

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

* refact: msi, reg values on upgrade/modify

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

* fix: msi, silent repair/upgrade, RemoveInstallFolder()

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

* Options support both 1/0 and Y/N

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

* refact: msi, preprocess, open file with explicit encoding

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

* fix: msi, read previous options

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

* fix: mis, install folder, read previous option

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

* Comment on Control -> Checkbox

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

* fix: UI, checkbox options, read previous values

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

* fix: shortcuts options, init state

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

* fix: shortcuts, init state

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

* Better shortcuts property conditions

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-05 17:49:48 +08:00
rustdesk
2266fde26f fix address_book_name cli 2024-08-04 19:31:13 +08:00
rustdesk
e58e75eea9 add address_book_name, address_book_tag to cli, https://github.com/rustdesk/rustdesk/discussions/7866, need to use with server Pro 1.4.2 (not ready) 2024-08-04 18:45:49 +08:00
rustdesk
eafebdba21 run universal for upload only 2024-08-04 16:32:05 +08:00
rustdesk
7bf5e69444 revert 2024-08-04 16:23:58 +08:00
rustdesk
cb0dc46d08 turn it off since it does not work, please test it before submit PR 2024-08-04 16:22:42 +08:00
rustdesk
9b8209b61b log::trace audio buffer cleared 2024-08-04 16:17:59 +08:00
21pages
b6ba9978e3 set max audio buffer to 150ms, clear audio buffer if full (#8947)
The device should have the capability to play a sufficient audio buffer during each period to meet the audio config, so the playback speed is not slow.

The audio delay is caused by network jitter. The controlled  side sends audio data every 10ms, but it often happens that multiple packets are sent together. During underrun periods, the controlling side plays extra silence data instead, resulting in the device playing more audio than the configured amount.
2024-08-04 16:11:00 +08:00
Vasyl Gello
0d1d7a9b87 Guard parameters for universal apk (#8943)
* Guard parameters for universal apk

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

* Free space before android builds

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

---------

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-08-04 12:34:08 +08:00
fufesou
508dd5b383 fix: custom client, msi, conn type (#8944)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-03 22:29:30 +08:00
Vasyl Gello
31a1b7a80b Fix copy step for universal apk (#8942)
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-08-03 19:00:27 +08:00
Vasyl Gello
ba43424781 Build universal apk (#8941)
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-08-03 18:39:12 +08:00
rustdesk
f899b2a962 fix https://github.com/rustdesk/rustdesk/issues/8856 2024-08-03 18:05:32 +08:00
Yevhen Popok
2dd3d8c11e Use correct locale code for Ukrainian translation file name (#8932)
Rename Ukrainian translation file ua.rs to uk.rs to avoid confusion.
This complements commits [2333ee2](2333ee2c07) and [b6035fb](b6035fbbdf)
2024-08-02 22:58:45 +08:00
fufesou
6eea425280 fix: clipboard cm ipc data, raw bytes (#8930)
* fix: clipboard cm ipc data, raw bytes

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

* Remove useless check

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-02 21:36:49 +08:00
Vasyl Gello
5e7d4fd2d6 vcpkg deps (#8926)
* vcpkg: bump opus to 1.5.2

Should fix flakes caused by https://github.com/android/ndk/issues/2032

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

* vcpkg: actually use cached artifacts

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

* Print all vcpkg log files on errors

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

---------

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-08-02 18:43:30 +08:00
rustdesk
d9fba50606 fix https://github.com/rustdesk/rustdesk-server-pro/issues/334 again 2024-08-02 15:43:55 +08:00
21pages
b6035fbbdf update supportedLocales (#8925)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-02 15:12:48 +08:00
Yevhen Popok
e67b694f06 Update Ukrainian translation (#8921) 2024-08-02 09:31:37 +08:00
rustdesk
2333ee2c07 fix https://github.com/rustdesk/rustdesk/issues/8923 2024-08-02 09:29:51 +08:00
Vasyl Gello
61ccc2152e Bump Android NDK to r27 (#8918)
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-08-01 21:33:59 +08:00
21pages
f6aca4ca8e fix click cm audio permission button before auth (#8917)
1. When not authenticated, clicking the audio permission button on the cm will send audio data
2. Keep the cursor position code unchanged, because `show_remote_cursor` is false before auth, so subscription will not happen.
3. Keep the clipboard code unchanged, because the keyboard permission will also be determined in `try_sub_services`. If the clipboard permission is clicked before auth and the keyboard permission is clicked after auth, the clipboard service will not be subscribed.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-01 18:17:02 +08:00
Vasyl Gello
1707987a7b Fix typo in android-x86 lib copy step (#8914)
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-08-01 15:09:33 +08:00
fufesou
85604dee79 refact: msi, custom client props (#8913)
* refact: msi, custom client props

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

* format

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-01 15:08:52 +08:00
Hamir Mahal
a12969be30 fix: usage of actions/checkout@v3 (#8912)
* chore: changes from formatting on save

* fix: usage of `actions/checkout@v3`
2024-08-01 09:12:11 +08:00
rustdesk
9f91eada89 fix https://github.com/rustdesk/rustdesk-server-pro/issues/338 2024-08-01 09:09:45 +08:00
Vasyl Gello
cb6a6aa42a flutter-build: Parameterize Android build matrix (#8907)
As @rustdesk noted debug builds are no-go in official RD repo
but this change makes it possible to filter jobs only relevant to
Android from flutter-build.yml to build only Android in a separate
mirror of Rustdesk.

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-31 20:38:00 +08:00
21pages
4fec8abad4 update repo to rustdesk-org (#8905)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-31 17:25:10 +08:00
solokot
35571dc8d7 Update ru.rs (#8901) 2024-07-31 14:16:55 +08:00
FastAct
a103b83647 Update nl.rs (#8892) 2024-07-30 20:23:49 +08:00
21pages
e1e4bf599b add libatomic1 to linux armv7 sciter depends (#8890)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-30 14:52:06 +08:00
rustdesk
cba8aaa410 tooltip for https://github.com/rustdesk/rustdesk/issues/8600, and change
dialog error to richtext with link support
2024-07-30 14:42:21 +08:00
fufesou
8ced4ddaa2 simple refact (#8888)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-30 13:06:37 +08:00
fufesou
15404ecab4 fix: clipboard, windows, controlled side, formats (#8885)
* fix: clipboard, windows, controlled side, formats

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

* Clipboard, reuse ipc conn and send_raw()

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

* Clipboard, merge content buffer

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

* refact: clipboard service, ipc stream

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-30 11:35:39 +08:00
Generalworks Inc.
97772f9ac5 Fix typo on ja.rs (#8886) 2024-07-30 00:27:28 +08:00
rustdesk
764fbe2c9d addressing https://github.com/rustdesk/rustdesk/issues/8883 2024-07-30 00:27:01 +08:00
fufesou
e03344d85b fix: clipboard, read as much as possible (#8881)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-29 21:19:29 +08:00
fufesou
0e98a51775 fix: clipboard, set formats and enable option (#8873)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-29 15:40:02 +08:00
XLion
0a1d3c4afb Update tw.rs and cn.rs (#8877)
* Update tw.rs

* Update cn.rs
2024-07-29 15:34:26 +08:00
rustdesk
fd9b5f3c57 fix https://github.com/rustdesk/rustdesk-server-pro/issues/334 2024-07-28 22:55:14 +08:00
Mr-Update
73f6afd4c0 Update de.rs (#8870) 2024-07-28 21:09:36 +08:00
fufesou
50dd2b3aad chore (#8868)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-28 19:33:00 +08:00
fufesou
c0e9445602 refact: embed crate mouce, uinput (#8836)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-28 17:27:09 +08:00
fufesou
541d9c6b86 feat: clipboard, multi formats (#8733)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-28 17:26:54 +08:00
fufesou
8c91e5c5ca refact: update crate tfc (#8867)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-28 17:26:18 +08:00
jxdv
19d1605d8c Update trs (#8866)
* update sk-tr

* update cs-tr
2024-07-28 17:13:15 +08:00
bovirus
0faf82f109 Update Italian language (#8863) 2024-07-28 17:13:01 +08:00
21pages
ee5314de20 use selectableText for some errors (#8862)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-28 11:13:19 +08:00
21pages
7e8d3bd2ac remove calling refreshCurrentUser when connect status become ready (#8849)
When refreshCurrentUser throw error, show check network in ab and group
tab.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-28 10:15:09 +08:00
Vasyl Gello
9750e1409c Use VCPKG_FORCE_SYSTEM_BINARIES in actual RD sciter build step (#8859)
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-28 08:44:16 +08:00
Vasyl Gello
67d4e061fb Try to detach /opt/artifacts from sciter (#8857)
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-27 23:57:53 +08:00
Vasyl Gello
f67f2be0cb Add built CMake to PATH in actual RD build step (#8855)
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-27 21:28:34 +08:00
Vasyl Gello
aa42bf548e Limit cmake buildstep to 1 threads to check OOM (#8852)
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-27 16:29:53 +08:00
rustdesk
d679e8fa7d WIN_RUST_VERSION -> SCITER_RUST_VERSION 2024-07-27 16:16:31 +08:00
Vasyl Gello
1357ef5d6f Try to fix sciter-armhf build (#8850)
* Bootstrap CMake 3.29.7 for ubuntu/bionic/armhf
  * vcpkg exevutable now needs GCC 8 but all product binaries
    are still OK with GCC 7
  * Remove res/vcpkg/linux.cmake

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-27 15:32:26 +08:00
21pages
30a5d1e0e1 avoid call refreshCurrentUser twice at startup (#8848)
refreshCurrentUser will be called at these 2 position:

1. runMainApp or runMobileApp in main.dart
2. when connect status is ready

Both of these two happens at startup, when connect status is ready and startup time < 5 seconds, not call refreshCurrentUser

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-27 09:50:06 +08:00
FastAct
9f0985c842 Update nl.rs (#8843) 2024-07-26 17:17:05 +08:00
21pages
be06c0d738 tab border for desktop subwindow (#8842)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-26 11:20:16 +08:00
zuiyu
f0f50f0f03 fix: remove getRadio todo and delete contentPadding param (#8839) 2024-07-26 10:31:01 +08:00
bovirus
1850d32f49 Update Italian language (#8838) 2024-07-26 10:00:18 +08:00
fufesou
3999d498be fix: keep window pos after new conn (#8834)
1. Keep window pos
2. Do some init in StatefulWidget constructor.
If try init in its state class, it may be too late. Because I see the
init function is called after building the widget tree.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-25 21:52:57 +08:00
rustdesk
bbdce8d57b Send clipboard keystroke, https://github.com/rustdesk/rustdesk/discussions/5451 2024-07-25 15:21:28 +08:00
dignow
baf70da2fe fix: trackpad, reverse horizontal scroll (#8827)
Signed-off-by: dignow <linlong1265@gmail.com>
2024-07-25 14:44:58 +08:00
dignow
b967d496cc refact: init values from initState to Constractor (#8817)
* refact: init values from initState to Constractor

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

* fix: move RxBool init into Constructor

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

* peer sort option

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

* Remove empty initState()

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

---------

Signed-off-by: dignow <linlong1265@gmail.com>
2024-07-25 10:45:51 +08:00
dignow
2aef79688b fix: mobile, conn page, jump on exiting remote (#8820)
Signed-off-by: dignow <linlong1265@gmail.com>
2024-07-25 00:43:14 +08:00
dignow
0451a1c45f fix: mobile, server page, jumps on loading (#8819)
Signed-off-by: dignow <linlong1265@gmail.com>
2024-07-25 00:13:22 +08:00
dignow
f7e9057a39 fix: video service, do not skip refresh message (#8815)
Signed-off-by: dignow <linlong1265@gmail.com>
2024-07-24 21:59:25 +08:00
dignow
d73e0e1e5a fix: video service (#8812)
1. Unset refresh flag if just refreshed.
2. Reduce the scope of the lock.

Signed-off-by: dignow <linlong1265@gmail.com>
2024-07-24 18:41:52 +08:00
dignow
39dbd89287 fix: siwtching display, clear last old image (#8810)
* fix: siwtching display, clear last old image

1. Clear last old image.
2. Try refresh after switching display.
3. Add an interval check before refresh video service.

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

* simple changes

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

---------

Signed-off-by: dignow <linlong1265@gmail.com>
2024-07-24 17:35:06 +08:00
21pages
c04f460bbd fix more bool options (#8809)
* fix more bool options
* hide sort ab tags because it's already sorted

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-24 17:20:58 +08:00
dignow
79a1f888d6 fix: flutter remove setState in initState (#8807)
Signed-off-by: dignow <linlong1265@gmail.com>
2024-07-24 14:00:49 +08:00
21pages
57d1b1ecc4 fix nextRgba not called when switching to texture render (#8792)
Because rgba buffer render doesn't support multi display, when switch to
multi display, it is possible that rgba.valid has been set to valid but
nextRgab is not called, when switching back to single display,
rgba.valid is still true.

Fix by rgba buffer, rgba texture and  gpu texture using different messages.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-24 12:06:16 +08:00
Vasyl Gello
8a4a2b5732 Remove existing /opt/artifacts/vcpkg in sciter builds (#8804)
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-24 11:23:49 +08:00
Vasyl Gello
51a60a7eed Fix arm32 sciter build (#8803)
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-24 10:16:25 +08:00
bovirus
b26acde450 Update Italian language (#8800) 2024-07-24 09:41:43 +08:00
Mr-Update
2de81045ea Update de.rs (#8801) 2024-07-24 09:41:25 +08:00
Vasyl Gello
a72a8906b0 vcpkg deps (#8764)
* Revert "Revert vcpkg ffmpeg (#8751)"

This reverts commit 5c16a8302e.

* vcpkg: Reland ffmpeg and try to fix sciter build

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

* vcpkg: Detect AVX2 by requiring __m256i

(ubuntu18.04 sciter)

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

* Install nasm from debian buster and python3.7

... from ubuntu universe

[Skip CI]

* vcpkg: Add libyuv port with fix for windows

From
abc59feabf

Found by @deep-soft

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

* Bump vcpkg baseline to 2024.07.12

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

* Fix F-Droid version action

I thought the latest release will be updated by the time hook starts
but it is not the case. Get tag from GITHUB_REF instead if GITHUB_REF_TYPE
is "tag".

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

---------

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2024-07-23 23:31:36 +08:00
dignow
614086a216 fix: mobile, query onlines, on active (#8796)
* fix: mobile, query onlines, on active

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

* Update peer_tab_page.dart

---------

Signed-off-by: dignow <linlong1265@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-07-23 23:28:03 +08:00
rustdesk
2ffc2ad85b enhance https://github.com/rustdesk/rustdesk-server-pro/discussions/325 2024-07-23 22:26:23 +08:00
21pages
eef091d4e8 fix hide ab tag when custom client startup (#8795)
not custom: 'Y', ''
custom: 'Y', 'N'
should not use isNotEmpty

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-07-23 21:35:05 +08:00
rustdesk
97f26f880b default-connect-password option 2024-07-23 10:46:50 +08:00
rustdesk
22c6f5e589 aggresive online query for self-hosting 2024-07-23 10:26:56 +08:00
お餅のCreeeper
b828768fa9 Update ja.rs and Add "About Rustdesk" (#8784)
* Update ja.rs

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

* Update ja.rs

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

* Add 'About RustDesk'

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

---------

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

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

* check revision version

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

* refact: msi, default revision number

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

* Simple refact

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

---------

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

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

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

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

---------

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

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

* Update README-JP.md

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

---------

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

This reverts commit 48464835f5.

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

This reverts commit b68d7a3054.

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

This reverts commit bc672b3367.

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

This reverts commit e283d33f28.

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

This reverts commit a4565bf0da.

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

This reverts commit 188f85b042.

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

This reverts commit 72c96f22b6.

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

This reverts commit 0143eaf601.

* update hwcodec to reverted

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

---------

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

only control with tabKeys

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

* fix preset-user-name typo

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

---------

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

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

* Update ja.rs

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

* Fix README-JP.md

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* inline

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

---------

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

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

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

* Update display_service.rs

---------

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

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

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

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

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

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

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

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

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

* fix: is_last_plain, reset on clipboard event

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

---------

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

New terms added

* Update es.rs

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

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

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

* Restore local port for opus 1.5.1

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

---------

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

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

* res/vcpkg: bump aom to 3.9.1

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

* res/vcpkg: bump libvpx to 1.14.1

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

* Bump vcpkg baseline to 2024.06.15

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

---------

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

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

* res/vcpkg: bump aom to 3.9.1

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

* res/vcpkg: bump libvpx to 1.14.1

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

---------

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

TODO:

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

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

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

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

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

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

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

* fix: compress image

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

* decompress image, use default level

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

* chore

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

* decompress, zstd::decode_all

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

---------

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

Only restore by the display registry settings.

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

Only restore the registry values.

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

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

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

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

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

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

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

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

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

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

* Tidy up configuration files UNIX permissions

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

---------

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

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

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

* Add translations and refact action menus

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

* Remove divider

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

* fix: recover deleted translations

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

---------

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

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

* fix: android init input perm

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

---------

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

* Update ru.rs

* Update ru.rs

* Update ru.rs

* Update ru.rs

* Update ru.rs

* Update ru.rs

* Create be.rs (Belarussian locale)

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update be.rs

* Update lang.rs

* Update lang.rs

* Update be.rs

---------

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

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

* Min scale restriction for mobile cursor

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

---------

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

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

* Incorrect changes

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

---------

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

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

* fix: ThrottledInterval, first next tick

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

---------

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

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-06-24 19:41:15 +08:00
Yevhen Popok
65edd55516 Update Ukrainian translation (#8453) 2024-06-24 17:32:00 +08:00
rustdesk
4947cf8718 fix https://github.com/rustdesk/rustdesk/issues/8452 2024-06-23 22:50:54 +08:00
jxdv
65dd2b8993 update sk.rs (#8449) 2024-06-23 18:05:28 +08:00
jxdv
ef82cfa034 update cs.rs (#8450) 2024-06-23 18:05:06 +08:00
257 changed files with 15696 additions and 5478 deletions

View File

@@ -1,8 +1,14 @@
[target.x86_64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
[target.i686-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"]
[target.'cfg(target_os="macos")']
rustflags = [
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
]
#[target.'cfg(target_os="linux")']
# glibc-static required, this may fix https://github.com/rustdesk/rustdesk/issues/9103, but I do not want this big change
# this is unlikely to help also, because the other so files still use libc dynamically
#rustflags = [
# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic"
#]

View File

@@ -4,9 +4,9 @@ env:
# MIN_SUPPORTED_RUST_VERSION: "1.46.0"
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2023.10.19
# vcpkg version: 2024.06.15
# for multiarch gcc compatibility
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
on:
workflow_dispatch:
@@ -112,6 +112,8 @@ jobs:
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpulse-dev \
libva-dev \
libvdpau-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -10,15 +10,15 @@ 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_VERSION: "3.22.2"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.03.25
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
VERSION: "1.2.6"
# vcpkg version: 2024.06.15
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
VERSION: "1.3.0"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -31,7 +31,207 @@ env:
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs:
build-for-macOS:
name: ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.13.9",
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
bridge: "1.80.1",
date: "20231219"
}
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.10.6",
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
bridge: "1.80.1",
date: "20231219"
}
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.10.6",
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
bridge: "1.80.1",
date: "20231119"
}
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.13.9",
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
bridge: "1.80.1",
date: "20231119"
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
keychain: rustdesk
- name: Check sign and import sign key
if: env.MACOS_P12_BASE64 != null
run: |
security default-keychain -s rustdesk.keychain
security find-identity -v
- name: Import notarize key
if: env.MACOS_P12_BASE64 != null
uses: timheuer/base64-to-file@v1.2
with:
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
fileName: rustdesk.json
fileDir: ${{ github.workspace }}
encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }}
- name: Install rcodesign tool
if: env.MACOS_P12_BASE64 != null
shell: bash
run: |
pushd /tmp
wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz
tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz
mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin
popd
- name: Install build runtime
run: |
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ matrix.job.flutter }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.job.os }}
- name: Install flutter rust bridge deps
shell: bash
run: |
sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml;
cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid"
# below works for mac to make buildable on 3.13.9
# pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd;
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
run: |
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
- name: Restore from cache and install vcpkg
uses: lukka/run-vcpkg@v7
if: false
with:
setupOnly: true
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
if: false
run: |
$VCPKG_ROOT/vcpkg install libvpx libyuv opus aom
- name: Show version information (Rust, cargo, Clang)
shell: bash
run: |
clang --version || true
rustup -V
rustup toolchain list
rustup default
cargo -V
rustc -V
- name: Build rustdesk
run: |
./build.py --flutter ${{ matrix.job.extra-build-args }}
- name: create unsigned dmg
run: |
CREATE_DMG="$(command -v create-dmg)"
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
- name: Codesign app and create signed dmg
if: env.MACOS_P12_BASE64 != null
run: |
# Patch create-dmg to give more attempts to unmount image
CREATE_DMG="$(command -v create-dmg)"
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
# Unlock keychain
security default-keychain -s rustdesk.keychain
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
# start sign the rustdesk.app and dmg
rm -rf *.dmg || true
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
# notarize the rustdesk-${{ env.VERSION }}.dmg
rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg
- name: Rename rustdesk
run: |
for name in rustdesk*??.dmg; do
mv "$name" "${name%%.dmg}-${{ matrix.job.arch }}-flutter${{ matrix.job.flutter }}-flutter${{ matrix.job.date }}.dmg"
done
- name: Publish DMG package
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
rustdesk*-${{ matrix.job.arch }}*.dmg
build-rustdesk-android:
if: false
name: build rustdesk android apk ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
strategy:

View File

@@ -6,7 +6,7 @@ jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
- uses: vedantmgoyal9/winget-releaser@main
with:
identifier: RustDesk.RustDesk
version: ${{ github.event.release.tag_name }}

2699
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk"
version = "1.2.6"
version = "1.3.0"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"
@@ -66,7 +66,7 @@ default-net = "0.14"
wol-rs = "1.0"
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
errno = "0.3"
rdev = { git = "https://github.com/fufesou/rdev" }
rdev = { git = "https://github.com/rustdesk-org/rdev" }
url = { version = "2.3", features = ["serde"] }
crossbeam-queue = "0.3"
hex = "0.4"
@@ -89,7 +89,10 @@ sys-locale = "0.3"
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
clipboard = { path = "libs/clipboard" }
ctrlc = "3.2"
arboard = { git = "https://github.com/fufesou/arboard", branch = "feat/x11_set_conn_timeout", features = ["wayland-data-control"] }
# arboard = { version = "3.4.0", features = ["wayland-data-control"] }
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] }
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
system_shutdown = "4.0"
qrcode-generator = "4.1"
@@ -111,7 +114,7 @@ winapi = { version = "0.3", features = [
winreg = "0.11"
windows-service = "0.6"
virtual_display = { path = "libs/virtual_display" }
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
shared_memory = "0.12"
tauri-winrt-notification = "0.1.2"
runas = "1.2"
@@ -135,7 +138,7 @@ image = "0.24"
keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
wallpaper = { git = "https://github.com/21pages/wallpaper.rs" }
wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" }
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
@@ -149,11 +152,10 @@ psimple = { package = "libpulse-simple-binding", version = "2.27" }
pulse = { package = "libpulse-binding", version = "2.27" }
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
async-process = "1.7"
mouce = { git="https://github.com/fufesou/mouce.git" }
evdev = { git="https://github.com/fufesou/evdev" }
evdev = { git="https://github.com/rustdesk-org/evdev" }
dbus = "0.9"
dbus-crossroads = "0.5"
pam = { git="https://github.com/fufesou/pam" }
pam = { git="https://github.com/rustdesk-org/pam" }
users = { version = "0.11" }
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
@@ -163,7 +165,7 @@ once_cell = {version = "1.18", optional = true}
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13"
jni = "0.21"
android-wakelock = { git = "https://github.com/21pages/android-wakelock" }
android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
[workspace]
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]

View File

@@ -59,19 +59,19 @@ Please download Sciter dynamic library yourself.
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)

View File

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

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.2.6
version: 1.3.0
exec: usr/lib/rustdesk/rustdesk
exec_args: $@
apt:
@@ -37,6 +37,9 @@ AppDir:
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
universe multiverse
include:
# https://github.com/rustdesk/rustdesk/issues/9103
# Because of APPDIR_LIBRARY_PATH, this libc6 is not used, use LD_PRELOAD: $APPDIR/usr/lib/x86_64-linux-gnu/libc.so.6 may help, If you have time, please have a try.
# We modify APPDIR_LIBRARY_PATH to use system lib first because gst crashed if not doing so, but you can try to change it.
- libc6:amd64
- libgtk-3-0
- libxcb-randr0

View File

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

View File

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

View File

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

View File

@@ -37,6 +37,8 @@ object KeyEventConverter {
action = KeyEvent.ACTION_UP
}
// FIXME: The last parameter is the repeat count, not modifiers ?
// https://developer.android.com/reference/android/view/KeyEvent#KeyEvent(long,%20long,%20int,%20int,%20int)
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
}
@@ -112,6 +114,10 @@ object KeyEventConverter {
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
ControlKey.VolumeMute -> KeyEvent.KEYCODE_VOLUME_MUTE
ControlKey.VolumeUp -> KeyEvent.KEYCODE_VOLUME_UP
ControlKey.VolumeDown -> KeyEvent.KEYCODE_VOLUME_DOWN
ControlKey.Power -> KeyEvent.KEYCODE_POWER
else -> 0 // Default to unknown.
}
}

View File

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

563
flutter/build_fdroid.sh Executable file
View File

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

View File

@@ -31,7 +31,6 @@ import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart';
import 'desktop/pages/remote_page.dart' as desktop_remote;
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'models/input_model.dart';
import 'models/model.dart';
import 'models/platform_model.dart';
@@ -630,10 +629,30 @@ List<Locale> supportedLocales = const [
Locale('da'),
Locale('eo'),
Locale('tr'),
Locale('vi'),
Locale('pl'),
Locale('kz'),
Locale('es'),
Locale('nl'),
Locale('nb'),
Locale('et'),
Locale('eu'),
Locale('bg'),
Locale('be'),
Locale('vn'),
Locale('uk'),
Locale('fa'),
Locale('ca'),
Locale('el'),
Locale('sv'),
Locale('sq'),
Locale('sr'),
Locale('th'),
Locale('sl'),
Locale('ro'),
Locale('lt'),
Locale('lv'),
Locale('ar'),
Locale('he'),
Locale('hr'),
];
String formatDurationToTime(Duration duration) {
@@ -647,8 +666,12 @@ String formatDurationToTime(Duration duration) {
closeConnection({String? id}) {
if (isAndroid || isIOS) {
gFFI.chatModel.hideChatOverlay();
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
() async {
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
gFFI.chatModel.hideChatOverlay();
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
}();
} else {
if (isWeb) {
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
@@ -928,13 +951,9 @@ makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
position: draggablePositions.mobileActions,
width: overlayW,
height: overlayH,
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
onRecentPressed: () async {
session.inputModel.sendMouse('down', MouseButtons.wheel);
await Future.delayed(const Duration(milliseconds: 500));
session.inputModel.sendMouse('up', MouseButtons.wheel);
},
onBackPressed: session.inputModel.onMobileBack,
onHomePressed: session.inputModel.onMobileHome,
onRecentPressed: session.inputModel.onMobileApps,
onHidePressed: onHide,
);
}
@@ -1058,6 +1077,49 @@ class CustomAlertDialog extends StatelessWidget {
}
}
Widget createDialogContent(String text) {
final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
final List<TextSpan> spans = [];
int start = 0;
bool hasLink = false;
linkRegExp.allMatches(text).forEach((match) {
hasLink = true;
if (match.start > start) {
spans.add(TextSpan(text: text.substring(start, match.start)));
}
spans.add(TextSpan(
text: match.group(0) ?? '',
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
String linkText = match.group(0) ?? '';
linkText = linkText.replaceAll(RegExp(r'[.,;!?]+$'), '');
launchUrl(Uri.parse(linkText));
},
));
start = match.end;
});
if (start < text.length) {
spans.add(TextSpan(text: text.substring(start)));
}
if (!hasLink) {
return SelectableText(text, style: const TextStyle(fontSize: 15));
}
return SelectableText.rich(
TextSpan(
style: TextStyle(color: Colors.black, fontSize: 15),
children: spans,
),
);
}
void msgBox(SessionID sessionId, String type, String title, String text,
String link, OverlayDialogManager dialogManager,
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
@@ -1066,7 +1128,7 @@ void msgBox(SessionID sessionId, String type, String title, String text,
bool hasOk = false;
submit() {
dialogManager.dismissAll();
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
// https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
if (!type.contains("custom") && desktopType != DesktopType.portForward) {
closeConnection();
}
@@ -1100,21 +1162,33 @@ void msgBox(SessionID sessionId, String type, String title, String text,
dialogManager.dismissAll();
}));
}
if (reconnect != null &&
title == "Connection Error" &&
reconnectTimeout != null) {
if (reconnect != null && title == "Connection Error") {
// `enabled` is used to disable the dialog button once the button is clicked.
final enabled = true.obs;
final button = Obx(() => _ReconnectCountDownButton(
second: reconnectTimeout,
onPressed: enabled.isTrue
? () {
// Disable the button
enabled.value = false;
reconnect(dialogManager, sessionId, false);
}
: null,
));
final button = reconnectTimeout != null
? Obx(() => _ReconnectCountDownButton(
second: reconnectTimeout,
onPressed: enabled.isTrue
? () {
// Disable the button
enabled.value = false;
reconnect(dialogManager, sessionId, false);
}
: null,
))
: Obx(
() => dialogButton(
'Reconnect',
isOutline: true,
onPressed: enabled.isTrue
? () {
// Disable the button
enabled.value = false;
reconnect(dialogManager, sessionId, false);
}
: null,
),
);
buttons.insert(0, button);
}
if (link.isNotEmpty) {
@@ -1203,7 +1277,7 @@ Widget msgboxContent(String type, String title, String text) {
translate(title),
style: TextStyle(fontSize: 21),
).marginOnly(bottom: 10),
Text(translateText(text), style: const TextStyle(fontSize: 15)),
createDialogContent(translateText(text)),
],
),
),
@@ -1398,14 +1472,10 @@ class AndroidPermissionManager {
}
}
// TODO move this to mobile/widgets.
// Used only for mobile, pages remote, settings, dialog
// TODO remove argument contentPadding, its not used, getToggle() has not
RadioListTile<T> getRadio<T>(
Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
{EdgeInsetsGeometry? contentPadding, bool? dense}) {
{bool? dense}) {
return RadioListTile<T>(
contentPadding: contentPadding ?? EdgeInsets.zero,
visualDensity: VisualDensity.compact,
controlAffinity: ListTileControlAffinity.trailing,
title: title,
@@ -1432,7 +1502,7 @@ Future<void> initGlobalFFI() async {
_globalFFI = FFI(null);
debugPrint("_globalFFI init end");
// after `put`, can also be globally found by Get.find<FFI>();
Get.put(_globalFFI, permanent: true);
Get.put<FFI>(_globalFFI, permanent: true);
}
String translate(String name) {
@@ -2719,20 +2789,26 @@ Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
}
typedef WhetherUseRemoteBlock = Future<bool> Function();
Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) {
var block = false.obs;
Widget buildRemoteBlock(
{required Widget child,
required RxBool block,
required bool mask,
WhetherUseRemoteBlock? use}) {
return Obx(() => MouseRegion(
onEnter: (_) async {
await shouldBeBlocked(block, use);
},
onExit: (event) => block.value = false,
child: Stack(children: [
child,
Offstage(
offstage: !block.value,
child: Container(
color: Colors.black.withOpacity(0.5),
)),
// scope block tab
FocusScope(child: child, canRequestFocus: !block.value),
// mask block click, cm not block click and still use check_click_time to avoid block local click
if (mask)
Offstage(
offstage: !block.value,
child: Container(
color: Colors.black.withOpacity(0.5),
)),
]),
));
}
@@ -2800,7 +2876,7 @@ Widget buildErrorBanner(BuildContext context,
alignment: Alignment.centerLeft,
child: Tooltip(
message: translate(err.value),
child: Text(
child: SelectableText(
translate(err.value),
),
)).marginSymmetric(vertical: 2),
@@ -2921,6 +2997,16 @@ openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
final displays = i == kAllDisplayValue
? List.generate(pi.displays.length, (index) => index)
: [i];
// Try clear image model before switching from all displays
// 1. The remote side has multiple displays.
// 2. Do not use texture render.
// 3. Connect to Display 1.
// 4. Switch to multi-displays `kAllDisplayValue`
// 5. Switch to Display 2.
// Then the remote page will display last picture of Display 1 at the beginning.
if (pi.forceTextureRender && i != kAllDisplayValue) {
ffi.imageModel.clearImage();
}
bind.sessionSwitchDisplay(
isDesktop: isDesktop,
sessionId: ffi.sessionId,
@@ -2954,11 +3040,15 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
}
setNewConnectWindowFrame(
int windowId, String peerId, int? display, Rect? screenRect) async {
setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount,
int? display, Rect? screenRect) async {
if (screenRect == null) {
await restoreWindowPosition(WindowType.RemoteDesktop,
windowId: windowId, display: display, peerId: peerId);
// Do not restore window position to new connection if there's a pre-session.
// https://github.com/rustdesk/rustdesk/discussions/8825
if (preSessionCount == 0) {
await restoreWindowPosition(WindowType.RemoteDesktop,
windowId: windowId, display: display, peerId: peerId);
}
} else {
await tryMoveToScreenAndSetFullscreen(screenRect);
}
@@ -3084,9 +3174,16 @@ Future<bool> setServerConfig(
List<RxString>? errMsgs,
ServerConfig config,
) async {
config.idServer = config.idServer.trim();
config.relayServer = config.relayServer.trim();
config.apiServer = config.apiServer.trim();
String removeEndSlash(String input) {
if (input.endsWith('/')) {
return input.substring(0, input.length - 1);
}
return input;
}
config.idServer = removeEndSlash(config.idServer.trim());
config.relayServer = removeEndSlash(config.relayServer.trim());
config.apiServer = removeEndSlash(config.apiServer.trim());
config.key = config.key.trim();
if (controllers != null) {
controllers[0].text = config.idServer;
@@ -3295,6 +3392,42 @@ bool isInHomePage() {
return controller.state.value.selected == 0;
}
Widget _buildPresetPasswordWarning() {
if (bind.mainGetBuildinOption(key: kOptionRemovePresetPasswordWarning) !=
'N') {
return SizedBox.shrink();
}
return Container(
color: Colors.yellow,
child: Column(
children: [
Align(
child: Text(
translate("Security Alert"),
style: TextStyle(
color: Colors.red,
fontSize:
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
fontWeight: FontWeight.bold,
),
)).paddingOnly(bottom: 8),
Text(
translate("preset_password_warning"),
style: TextStyle(color: Colors.red),
)
],
).paddingAll(8),
); // Show a warning message if the Future completed with true
}
Widget buildPresetPasswordWarningMobile() {
if (bind.isPresetPasswordMobileOnly()) {
return _buildPresetPasswordWarning();
} else {
return SizedBox.shrink();
}
}
Widget buildPresetPasswordWarning() {
return FutureBuilder<bool>(
future: bind.isPresetPassword(),
@@ -3305,27 +3438,7 @@ Widget buildPresetPasswordWarning() {
return Text(
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
} else if (snapshot.hasData && snapshot.data == true) {
return Container(
color: Colors.yellow,
child: Column(
children: [
Align(
child: Text(
translate("Security Alert"),
style: TextStyle(
color: Colors.red,
fontSize:
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
fontWeight: FontWeight.bold,
),
)).paddingOnly(bottom: 8),
Text(
translate("preset_password_warning"),
style: TextStyle(color: Colors.red),
)
],
).paddingAll(8),
); // Show a warning message if the Future completed with true
return _buildPresetPasswordWarning();
} else {
return SizedBox
.shrink(); // Show nothing if the Future completed with false or null
@@ -3379,7 +3492,8 @@ Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
);
}
get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize;
get windowResizeEdgeSize =>
isLinux && !_linuxWindowResizable ? 0.0 : kWindowResizeEdgeSize;
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
// See _linuxWindowResizable for more details.
@@ -3397,7 +3511,12 @@ setResizable(bool resizable) {
isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
final isCustomClient = bind.isCustomClient();
bool? _isCustomClient;
bool get isCustomClient {
_isCustomClient ??= bind.isCustomClient();
return _isCustomClient!;
}
get defaultOptionLang => isCustomClient ? 'default' : '';
get defaultOptionTheme => isCustomClient ? 'system' : '';
get defaultOptionYes => isCustomClient ? 'Y' : '';
@@ -3406,6 +3525,12 @@ get defaultOptionWhitelist => isCustomClient ? ',' : '';
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
bool whitelistNotEmpty() {
// https://rustdesk.com/docs/en/self-host/client-configuration/advanced-settings/#whitelist
final v = bind.mainGetOptionSync(key: kOptionWhitelist);
return v != '' && v != ',';
}
// `setMovable()` is only supported on macOS.
//
// On macOS, the window can be dragged by the tab bar by default.
@@ -3429,3 +3554,36 @@ disableWindowMovable(int? windowId) {
WindowController.fromWindowId(windowId).setMovable(false);
}
}
Widget netWorkErrorWidget() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(translate("network_error_tip")),
ElevatedButton(
onPressed: gFFI.userModel.refreshCurrentUser,
child: Text(translate("Retry")))
.marginSymmetric(vertical: 16),
SelectableText(gFFI.userModel.networkError.value,
style: TextStyle(fontSize: 11, color: Colors.red)),
],
));
}
List<ResizeEdge>? get windowManagerEnableResizeEdges => isWindows
? [
ResizeEdge.topLeft,
ResizeEdge.top,
ResizeEdge.topRight,
]
: null;
List<SubWindowResizeEdge>? get subWindowManagerEnableResizeEdges => isWindows
? [
SubWindowResizeEdge.topLeft,
SubWindowResizeEdge.top,
SubWindowResizeEdge.topRight,
]
: null;

View File

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

View File

@@ -35,17 +35,14 @@ class AddressBook extends StatefulWidget {
class _AddressBookState extends State<AddressBook> {
var menuPos = RelativeRect.fill;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) => Obx(() {
if (!gFFI.userModel.isLogin) {
return Center(
child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login"))));
} else if (gFFI.userModel.networkError.isNotEmpty) {
return netWorkErrorWidget();
} else {
return Column(
children: [
@@ -425,7 +422,8 @@ class _AddressBookState extends State<AddressBook> {
if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
sortMenuItem(),
if (gFFI.abModel.legacyMode.value)
sortMenuItem(), // It's already sorted after pulling down
if (canWrite) syncMenuItem(),
filterMenuItem(),
if (!gFFI.abModel.legacyMode.value && canWrite)

View File

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

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
@@ -218,50 +219,53 @@ void changeWhiteList({Function()? callback}) async {
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
if (!isOptFixed)dialogButton("Clear", onPressed: () async {
await bind.mainSetOption(
key: kOptionWhitelist, value: defaultOptionWhitelist);
callback?.call();
close();
}, isOutline: true),
if (!isOptFixed) dialogButton(
"OK",
onPressed: () async {
setState(() {
msg = "";
isInProgress = true;
});
newWhiteListField = controller.text.trim();
var newWhiteList = "";
if (newWhiteListField.isEmpty) {
// pass
} else {
final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
// test ip
final ipMatch = RegExp(
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
final ipv6Match = RegExp(
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
for (final ip in ips) {
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
msg = "${translate("Invalid IP")} $ip";
setState(() {
isInProgress = false;
});
return;
}
}
newWhiteList = ips.join(',');
}
if (newWhiteList.trim().isEmpty) {
newWhiteList = defaultOptionWhitelist;
}
if (!isOptFixed)
dialogButton("Clear", onPressed: () async {
await bind.mainSetOption(
key: kOptionWhitelist, value: newWhiteList);
key: kOptionWhitelist, value: defaultOptionWhitelist);
callback?.call();
close();
},
),
}, isOutline: true),
if (!isOptFixed)
dialogButton(
"OK",
onPressed: () async {
setState(() {
msg = "";
isInProgress = true;
});
newWhiteListField = controller.text.trim();
var newWhiteList = "";
if (newWhiteListField.isEmpty) {
// pass
} else {
final ips =
newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
// test ip
final ipMatch = RegExp(
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
final ipv6Match = RegExp(
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
for (final ip in ips) {
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
msg = "${translate("Invalid IP")} $ip";
setState(() {
isInProgress = false;
});
return;
}
}
newWhiteList = ips.join(',');
}
if (newWhiteList.trim().isEmpty) {
newWhiteList = defaultOptionWhitelist;
}
await bind.mainSetOption(
key: kOptionWhitelist, value: newWhiteList);
callback?.call();
close();
},
),
],
onCancel: close,
);
@@ -675,6 +679,7 @@ class PasswordWidget extends StatefulWidget {
this.reRequestFocus = false,
this.hintText,
this.errorText,
this.title,
}) : super(key: key);
final TextEditingController controller;
@@ -682,6 +687,7 @@ class PasswordWidget extends StatefulWidget {
final bool reRequestFocus;
final String? hintText;
final String? errorText;
final String? title;
@override
State<PasswordWidget> createState() => _PasswordWidgetState();
@@ -725,7 +731,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
@override
Widget build(BuildContext context) {
return DialogTextField(
title: translate(DialogTextField.kPasswordTitle),
title: translate(widget.title ?? DialogTextField.kPasswordTitle),
hintText: translate(widget.hintText ?? 'Enter your password'),
controller: widget.controller,
prefixIcon: DialogTextField.kPasswordIcon,
@@ -1762,9 +1768,70 @@ void renameDialog(
});
}
void changeBot({Function()? callback}) async {
if (bind.mainHasValidBotSync()) {
await bind.mainSetOption(key: "bot", value: "");
callback?.call();
return;
}
String errorText = '';
bool loading = false;
final controller = TextEditingController();
gFFI.dialogManager.show((setState, close, context) {
onVerify() async {
final token = controller.text.trim();
if (token == "") return;
loading = true;
errorText = '';
setState(() {});
final error = await bind.mainVerifyBot(token: token);
if (error == "") {
callback?.call();
close();
} else {
errorText = translate(error);
loading = false;
setState(() {});
}
}
final codeField = TextField(
autofocus: true,
controller: controller,
decoration: InputDecoration(
hintText: translate('Token'),
),
);
return CustomAlertDialog(
title: Text(translate("Telegram bot")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(translate("enable-bot-desc"),
style: TextStyle(fontSize: 12))
.marginOnly(bottom: 12),
Row(children: [Expanded(child: codeField)]),
if (errorText != '')
Text(errorText, style: TextStyle(color: Colors.red))
.marginOnly(top: 12),
],
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
loading
? CircularProgressIndicator()
: dialogButton("OK", onPressed: onVerify),
],
onCancel: close,
);
});
}
void change2fa({Function()? callback}) async {
if (bind.mainHasValid2FaSync()) {
await bind.mainSetOption(key: "2fa", value: "");
await bind.mainClearTrustedDevices();
callback?.call();
return;
}
@@ -1832,6 +1899,7 @@ void enter2FaDialog(
SessionID sessionId, OverlayDialogManager dialogManager) async {
final controller = TextEditingController();
final RxBool submitReady = false.obs;
final RxBool trustThisDevice = false.obs;
dialogManager.dismissAll();
dialogManager.show((setState, close, context) {
@@ -1841,7 +1909,7 @@ void enter2FaDialog(
}
submit() {
gFFI.send2FA(sessionId, controller.text.trim());
gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value);
close();
dialogManager.showLoading(translate('Logging in...'),
onCancel: closeConnection);
@@ -1855,9 +1923,27 @@ void enter2FaDialog(
onChanged: () => submitReady.value = codeField.isReady,
);
final trustField = Obx(() => CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(translate("Trust this device")),
value: trustThisDevice.value,
onChanged: (value) {
if (value == null) return;
trustThisDevice.value = value;
},
));
return CustomAlertDialog(
title: Text(translate('enter-2fa-title')),
content: codeField,
content: Column(
children: [
codeField,
if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId))
trustField,
],
),
actions: [
dialogButton('Cancel',
onPressed: cancel,
@@ -2124,3 +2210,280 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
);
});
}
void CommonConfirmDialog(OverlayDialogManager dialogManager, String content,
VoidCallback onConfirm) {
dialogManager.show((setState, close, context) {
submit() {
close();
onConfirm.call();
}
return CustomAlertDialog(
content: Row(
children: [
Expanded(
child: Text(content,
style: const TextStyle(fontSize: 15),
textAlign: TextAlign.start),
),
],
).marginOnly(bottom: 12),
actions: [
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
dialogButton(translate("OK"), onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
void changeUnlockPinDialog(String oldPin, Function() callback) {
final pinController = TextEditingController(text: oldPin);
final confirmController = TextEditingController(text: oldPin);
String? pinErrorText;
String? confirmationErrorText;
gFFI.dialogManager.show((setState, close, context) {
submit() async {
pinErrorText = null;
confirmationErrorText = null;
final pin = pinController.text.trim();
final confirm = confirmController.text.trim();
if (pin != confirm) {
setState(() {
confirmationErrorText =
translate('The confirmation is not identical.');
});
return;
}
final errorMsg = bind.mainSetUnlockPin(pin: pin);
if (errorMsg != '') {
setState(() {
pinErrorText = translate(errorMsg);
});
return;
}
callback.call();
close();
}
return CustomAlertDialog(
title: Text(translate("Set PIN")),
content: Column(
children: [
DialogTextField(
title: 'PIN',
controller: pinController,
obscureText: true,
errorText: pinErrorText,
),
DialogTextField(
title: translate('Confirmation'),
controller: confirmController,
obscureText: true,
errorText: confirmationErrorText,
)
],
).marginOnly(bottom: 12),
actions: [
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
dialogButton(translate("OK"), onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
void checkUnlockPinDialog(String correctPin, Function() passCallback) {
final controller = TextEditingController();
String? errorText;
gFFI.dialogManager.show((setState, close, context) {
submit() async {
final pin = controller.text.trim();
if (correctPin != pin) {
setState(() {
errorText = translate('Wrong PIN');
});
return;
}
passCallback.call();
close();
}
return CustomAlertDialog(
content: Row(
children: [
Expanded(
child: PasswordWidget(
title: 'PIN',
controller: controller,
errorText: errorText,
hintText: '',
))
],
).marginOnly(bottom: 12),
actions: [
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
dialogButton(translate("OK"), onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
void confrimDeleteTrustedDevicesDialog(
RxList<TrustedDevice> trustedDevices, RxList<Uint8List> selectedDevices) {
CommonConfirmDialog(gFFI.dialogManager, '${translate('Confirm Delete')}?',
() async {
if (selectedDevices.isEmpty) return;
if (selectedDevices.length == trustedDevices.length) {
await bind.mainClearTrustedDevices();
trustedDevices.clear();
selectedDevices.clear();
} else {
final json = jsonEncode(selectedDevices.map((e) => e.toList()).toList());
await bind.mainRemoveTrustedDevices(json: json);
trustedDevices.removeWhere((element) {
return selectedDevices.contains(element.hwid);
});
selectedDevices.clear();
}
});
}
void manageTrustedDeviceDialog() async {
RxList<TrustedDevice> trustedDevices = (await TrustedDevice.get()).obs;
RxList<Uint8List> selectedDevices = RxList.empty();
gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text(translate("Manage trusted devices")),
content: trustedDevicesTable(trustedDevices, selectedDevices),
actions: [
Obx(() => dialogButton(translate("Delete"),
onPressed: selectedDevices.isEmpty
? null
: () {
confrimDeleteTrustedDevicesDialog(
trustedDevices,
selectedDevices,
);
},
isOutline: false)
.marginOnly(top: 12)),
dialogButton(translate("Close"), onPressed: close, isOutline: true)
.marginOnly(top: 12),
],
onCancel: close,
);
});
}
class TrustedDevice {
late final Uint8List hwid;
late final int time;
late final String id;
late final String name;
late final String platform;
TrustedDevice.fromJson(Map<String, dynamic> json) {
final hwidList = json['hwid'] as List<dynamic>;
hwid = Uint8List.fromList(hwidList.cast<int>());
time = json['time'];
id = json['id'];
name = json['name'];
platform = json['platform'];
}
String daysRemaining() {
final expiry = time + 90 * 24 * 60 * 60 * 1000;
final remaining = expiry - DateTime.now().millisecondsSinceEpoch;
if (remaining < 0) {
return '0';
}
return (remaining / (24 * 60 * 60 * 1000)).toStringAsFixed(0);
}
static Future<List<TrustedDevice>> get() async {
final List<TrustedDevice> devices = List.empty(growable: true);
try {
final devicesJson = await bind.mainGetTrustedDevices();
if (devicesJson.isNotEmpty) {
final devicesList = json.decode(devicesJson);
if (devicesList is List) {
for (var device in devicesList) {
devices.add(TrustedDevice.fromJson(device));
}
}
}
} catch (e) {
print(e.toString());
}
devices.sort((a, b) => b.time.compareTo(a.time));
return devices;
}
}
Widget trustedDevicesTable(
RxList<TrustedDevice> devices, RxList<Uint8List> selectedDevices) {
RxBool selectAll = false.obs;
setSelectAll() {
if (selectedDevices.isNotEmpty &&
selectedDevices.length == devices.length) {
selectAll.value = true;
} else {
selectAll.value = false;
}
}
devices.listen((_) {
setSelectAll();
});
selectedDevices.listen((_) {
setSelectAll();
});
return FittedBox(
child: Obx(() => DataTable(
columns: [
DataColumn(
label: Checkbox(
value: selectAll.value,
onChanged: (value) {
if (value == true) {
selectedDevices.clear();
selectedDevices.addAll(devices.map((e) => e.hwid));
} else {
selectedDevices.clear();
}
},
)),
DataColumn(label: Text(translate('Platform'))),
DataColumn(label: Text(translate('ID'))),
DataColumn(label: Text(translate('Username'))),
DataColumn(label: Text(translate('Days remaining'))),
],
rows: devices.map((device) {
return DataRow(cells: [
DataCell(Checkbox(
value: selectedDevices.contains(device.hwid),
onChanged: (value) {
if (value == null) return;
if (value) {
selectedDevices.remove(device.hwid);
selectedDevices.add(device.hwid);
} else {
selectedDevices.remove(device.hwid);
}
},
)),
DataCell(Text(device.platform)),
DataCell(Text(device.id)),
DataCell(Text(device.name)),
DataCell(Text(device.daysRemaining())),
]);
}).toList(),
)),
);
}

View File

@@ -142,11 +142,6 @@ class _WidgetOPState extends State<WidgetOP> {
String _failedMsg = '';
String _url = '';
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();

View File

@@ -23,11 +23,6 @@ class _MyGroupState extends State<MyGroup> {
RxString get searchUserText => gFFI.groupModel.searchUserText;
static TextEditingController searchUserController = TextEditingController();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Obx(() {
@@ -35,6 +30,8 @@ class _MyGroupState extends State<MyGroup> {
return Center(
child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login"))));
} else if (gFFI.userModel.networkError.isNotEmpty) {
return netWorkErrorWidget();
} else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
return const Center(
child: CircularProgressIndicator(),

View File

@@ -353,7 +353,7 @@ class Draggable extends StatefulWidget {
final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
@override
State<StatefulWidget> createState() => _DraggableState();
State<StatefulWidget> createState() => _DraggableState(chatModel);
}
class _DraggableState extends State<Draggable> {
@@ -362,10 +362,8 @@ class _DraggableState extends State<Draggable> {
double _saveHeight = 0;
double _lastBottomHeight = 0;
@override
void initState() {
super.initState();
_chatModel = widget.chatModel;
_DraggableState(ChatModel? chatModel) {
_chatModel = chatModel;
}
get position => widget.position.pos;
@@ -467,7 +465,8 @@ class IOSDraggable extends StatefulWidget {
final Widget Function(BuildContext) builder;
@override
IOSDraggableState createState() => IOSDraggableState();
IOSDraggableState createState() =>
IOSDraggableState(chatModel, width, height);
}
class IOSDraggableState extends State<IOSDraggable> {
@@ -478,15 +477,13 @@ class IOSDraggableState extends State<IOSDraggable> {
double _saveHeight = 0;
double _lastBottomHeight = 0;
@override
void initState() {
super.initState();
_chatModel = widget.chatModel;
_width = widget.width;
_height = widget.height;
IOSDraggableState(ChatModel? chatModel, double w, double h) {
_chatModel = chatModel;
_width = w;
_height = h;
}
get position => widget.position;
DraggableKeyPosition get position => widget.position;
checkKeyboard() {
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
@@ -494,13 +491,13 @@ class IOSDraggableState extends State<IOSDraggable> {
// save
if (!_keyboardVisible && currentVisible) {
_saveHeight = position.value.dy;
_saveHeight = position.pos.dy;
}
// reset
if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() {
position.value = Offset(position.value.dx, _saveHeight);
position.update(Offset(position.pos.dx, _saveHeight));
});
}
@@ -508,10 +505,10 @@ class IOSDraggableState extends State<IOSDraggable> {
if (_keyboardVisible && currentVisible) {
final sumHeight = bottomHeight + _height;
final contextHeight = MediaQuery.of(context).size.height;
if (sumHeight + position.value.dy > contextHeight) {
if (sumHeight + position.pos.dy > contextHeight) {
final y = contextHeight - sumHeight;
setState(() {
position.value = Offset(position.value.dx, y);
position.update(Offset(position.pos.dx, y));
});
}
}
@@ -526,14 +523,14 @@ class IOSDraggableState extends State<IOSDraggable> {
return Stack(
children: [
Positioned(
left: position.value.dx,
top: position.value.dy,
left: position.pos.dx,
top: position.pos.dy,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
position.value += details.delta;
position.update(position.pos + details.delta);
});
_chatModel?.setChatWindowPosition(position.value);
_chatModel?.setChatWindowPosition(position.pos);
},
child: Material(
child: Container(

View File

@@ -22,6 +22,8 @@ enum PeerUiType { grid, tile, list }
final peerCardUiType = PeerUiType.grid.obs;
bool? hideUsernameOnCard;
class _PeerCard extends StatefulWidget {
final Peer peer;
final PeerTabIndex tab;
@@ -130,8 +132,11 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildPeerTile(
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
hideUsernameOnCard ??=
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
final name = hideUsernameOnCard == true
? peer.hostname
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final greyStyle = TextStyle(
fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
@@ -239,8 +244,11 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildPeerCard(
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
hideUsernameOnCard ??=
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
final name = hideUsernameOnCard == true
? peer.hostname
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final child = Card(
color: Colors.transparent,
elevation: 0,
@@ -628,8 +636,8 @@ abstract class BasePeerCard extends StatelessWidget {
@protected
Future<bool> _isForceAlwaysRelay(String id) async {
return (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay))
.isNotEmpty;
return option2bool(kOptionForceAlwaysRelay,
(await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)));
}
@protected

View File

@@ -76,8 +76,11 @@ class _PeerTabPageState extends State<PeerTabPage>
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
@override
void initState() {
_PeerTabPageState() {
_loadLocalOptions();
}
void _loadLocalOptions() {
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
if (uiType != '') {
peerCardUiType.value = int.parse(uiType) == 0
@@ -87,8 +90,7 @@ class _PeerTabPageState extends State<PeerTabPage>
: PeerUiType.list;
}
hideAbTagsPanel.value =
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel).isNotEmpty;
super.initState();
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel) == 'Y';
}
Future<void> handleTabSelection(int tabIndex) async {
@@ -399,9 +401,9 @@ class _PeerTabPageState extends State<PeerTabPage>
final peers = model.selectedPeers;
switch (model.currentTab) {
case 0:
peers.map((p) async {
for (var p in peers) {
await bind.mainRemovePeer(id: p.id);
}).toList();
}
await bind.mainLoadRecentPeers();
break;
case 1:
@@ -413,9 +415,9 @@ class _PeerTabPageState extends State<PeerTabPage>
await bind.mainLoadFavPeers();
break;
case 2:
peers.map((p) async {
for (var p in peers) {
await bind.mainRemoveDiscovered(id: p.id);
}).toList();
}
await bind.mainLoadLanPeers();
break;
case 3:
@@ -872,16 +874,18 @@ class PeerSortDropdown extends StatefulWidget {
}
class _PeerSortDropdownState extends State<PeerSortDropdown> {
@override
void initState() {
_PeerSortDropdownState() {
if (!PeerSortType.values.contains(peerSort.value)) {
peerSort.value = PeerSortType.remoteId;
bind.setLocalFlutterOption(
k: kOptionPeerSorting,
v: peerSort.value,
);
_loadLocalOptions();
}
super.initState();
}
void _loadLocalOptions() {
peerSort.value = PeerSortType.remoteId;
bind.setLocalFlutterOption(
k: kOptionPeerSorting,
v: peerSort.value,
);
}
@override

View File

@@ -45,10 +45,14 @@ class LoadEvent {
final peerSearchText = "".obs;
/// for peer sort, global obs value
final peerSort = bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
RxString? _peerSort;
RxString get peerSort {
_peerSort ??= bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
return _peerSort!;
}
// list for listener
final obslist = [peerSearchText, peerSort].obs;
RxList<RxString> get obslist => [peerSearchText, peerSort].obs;
final peerSearchTextController =
TextEditingController(text: peerSearchText.value);
@@ -70,7 +74,8 @@ class _PeersView extends StatefulWidget {
}
/// State for the peer widget.
class _PeersViewState extends State<_PeersView> with WindowListener {
class _PeersViewState extends State<_PeersView>
with WindowListener, WidgetsBindingObserver {
static const int _maxQueryCount = 3;
final HashMap<String, String> _emptyMessages = HashMap.from({
LoadEvent.recent: 'empty_recent_tip',
@@ -82,9 +87,10 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
final _curPeers = <String>{};
var _lastChangeTime = DateTime.now();
var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now().add(const Duration(seconds: 30));
var _lastQueryTime = DateTime.now();
var _queryCount = 0;
var _exit = false;
bool _isActive = true;
final _scrollController = ScrollController();
@@ -95,12 +101,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
@override
void initState() {
windowManager.addListener(this);
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
void dispose() {
windowManager.removeListener(this);
WidgetsBinding.instance.removeObserver(this);
_exit = true;
super.dispose();
}
@@ -115,6 +123,20 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
_queryCount = _maxQueryCount;
}
// This function is required for mobile.
// `onWindowFocus` works fine for desktop.
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (isDesktop) return;
if (state == AppLifecycleState.resumed) {
_isActive = true;
_queryCount = 0;
} else if (state == AppLifecycleState.inactive) {
_isActive = false;
}
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Peers>(
@@ -253,10 +275,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
return body;
}
final _queryInterval = const Duration(seconds: 20);
var _queryInterval = const Duration(seconds: 20);
void _startCheckOnlines() {
() async {
final p = await bind.mainIsUsingPublicServer();
if (!p) {
_queryInterval = const Duration(seconds: 6);
}
while (!_exit) {
final now = DateTime.now();
if (!setEquals(_curPeers, _lastQueryPeers)) {
@@ -264,7 +290,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
_queryOnlines(false);
}
} else {
if (_queryCount < _maxQueryCount) {
if (_isActive && (_queryCount < _maxQueryCount || !p)) {
if (now.difference(_lastQueryTime) >= _queryInterval) {
if (_curPeers.isNotEmpty) {
bind.queryOnlines(ids: _curPeers.toList(growable: false));
@@ -282,14 +308,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
_queryOnlines(bool isLoadEvent) {
if (_curPeers.isNotEmpty) {
bind.queryOnlines(ids: _curPeers.toList(growable: false));
_lastQueryPeers = {..._curPeers};
if (isLoadEvent) {
_lastChangeTime = DateTime.now();
} else {
_lastQueryTime = DateTime.now().subtract(_queryInterval);
}
_queryCount = 0;
}
_lastQueryPeers = {..._curPeers};
if (isLoadEvent) {
_lastChangeTime = DateTime.now();
} else {
_lastQueryTime = DateTime.now().subtract(_queryInterval);
}
}
Future<List<Peer>>? matchPeers(

View File

@@ -6,6 +6,7 @@ import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
@@ -22,6 +23,20 @@ class TTextMenu {
required this.onPressed,
this.trailingIcon,
this.divider = false});
Widget getChild() {
if (trailingIcon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
child,
trailingIcon!,
],
);
} else {
return child;
}
}
}
class TRadioMenu<T> {
@@ -115,12 +130,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// paste
if (isMobile &&
pi.platform != kPeerPlatformAndroid &&
perms['keyboard'] != false &&
perms['clipboard'] != false) {
if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) {
v.add(TTextMenu(
child: Text(translate('Paste')),
child: Text(translate('Send clipboard keystrokes')),
onPressed: () async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
@@ -636,6 +648,18 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
v.addAll(toolbarKeyboardToggles(ffi));
}
// view mode (mobile only, desktop is in keyboard menu)
if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
v.add(TToggleMenu(
value: ffiModel.viewOnly,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, value);
},
child: Text(translate('View Mode'))));
}
return v;
}
@@ -776,3 +800,106 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
}
return v;
}
bool showVirtualDisplayMenu(FFI ffi) {
if (ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
return false;
}
if (!ffi.ffiModel.pi.isInstalled) {
return false;
}
if (ffi.ffiModel.pi.isRustDeskIdd || ffi.ffiModel.pi.isAmyuniIdd) {
return true;
}
return false;
}
List<Widget> getVirtualDisplayMenuChildren(
FFI ffi, String id, VoidCallback? clickCallBack) {
if (!showVirtualDisplayMenu(ffi)) {
return [];
}
final pi = ffi.ffiModel.pi;
final privacyModeState = PrivacyModeState.find(id);
if (pi.isRustDeskIdd) {
final virtualDisplays = ffi.ffiModel.pi.RustDeskVirtualDisplays;
final children = <Widget>[];
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
children.add(Obx(() => CkbMenuButton(
value: virtualDisplays.contains(i + 1),
onChanged: privacyModeState.isNotEmpty
? null
: (bool? value) async {
if (value != null) {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId, index: i + 1, on: value);
clickCallBack?.call();
}
},
child: Text('${translate('Virtual display')} ${i + 1}'),
ffi: ffi,
)));
}
children.add(Divider());
children.add(Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
clickCallBack?.call();
},
ffi: ffi,
child: Text(translate('Plug out all')),
)));
return children;
}
if (pi.isAmyuniIdd) {
final count = ffi.ffiModel.pi.amyuniVirtualDisplayCount;
final children = <Widget>[
Obx(() => Row(
children: [
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId, index: 0, on: false);
clickCallBack?.call();
},
child: Icon(Icons.remove),
),
Text(count.toString()),
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 4
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId, index: 0, on: true);
clickCallBack?.call();
},
child: Icon(Icons.add),
),
],
)),
Divider(),
Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
clickCallBack?.call();
},
ffi: ffi,
child: Text(translate('Plug out all')),
)),
];
return children;
}
return [];
}

View File

@@ -135,6 +135,18 @@ const String kOptionAllowLinuxHeadless = "allow-linux-headless";
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
const String kOptionStopService = "stop-service";
const String kOptionDirectxCapture = "enable-directx-capture";
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
// buildin opitons
const String kOptionHideServerSetting = "hide-server-settings";
const String kOptionHideProxySetting = "hide-proxy-settings";
const String kOptionHideSecuritySetting = "hide-security-settings";
const String kOptionHideNetworkSetting = "hide-network-settings";
const String kOptionRemovePresetPasswordWarning =
"remove-preset-password-warning";
const kHideUsernameOnCard = "hide-username-on-card";
const String kOptionHideHelpCards = "hide-help-cards";
const String kOptionToggleViewOnly = "view-only";
@@ -155,6 +167,8 @@ const int kWindowMainId = 0;
const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse";
const String kKeyFlutterKey = "flutter_key";
const String kKeyShowDisplaysAsIndividualWindows =
'displays_as_individual_windows';
const String kKeyUseAllMyDisplaysForTheRemoteSession =
@@ -227,9 +241,9 @@ const kDefaultScrollDuration = Duration(milliseconds: 50);
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
const kFullScreenEdgeSize = 0.0;
const kMaximizeEdgeSize = 0.0;
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
final kWindowBorderWidth = 1.0;
// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
const kWindowResizeEdgeSize = 5.0;
const kWindowBorderWidth = 1.0;
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
const kFrameBorderRadius = 12.0;
const kFrameClipRRectBorderRadius = 12.0;

View File

@@ -169,16 +169,12 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
final status =
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
final statusNum = status['status_num'] as int;
final preStatus = stateGlobal.svcStatus.value;
if (statusNum == 0) {
stateGlobal.svcStatus.value = SvcStatus.connecting;
} else if (statusNum == -1) {
stateGlobal.svcStatus.value = SvcStatus.notReady;
} else if (statusNum == 1) {
stateGlobal.svcStatus.value = SvcStatus.ready;
if (preStatus != SvcStatus.ready) {
gFFI.userModel.refreshCurrentUser();
}
} else {
stateGlobal.svcStatus.value = SvcStatus.notReady;
}
@@ -212,14 +208,14 @@ class _ConnectionPageState extends State<ConnectionPage>
void initState() {
super.initState();
if (_idController.text.isEmpty) {
() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final lastRemoteId = await bind.mainGetLastRemoteId();
if (lastRemoteId != _idController.id) {
setState(() {
_idController.id = lastRemoteId;
});
}
}();
});
}
Get.put<IDTextEditingController>(_idController);
windowManager.addListener(this);
@@ -261,8 +257,9 @@ class _ConnectionPageState extends State<ConnectionPage>
@override
void onWindowLeaveFullScreen() {
// Restore edge border to default edge size.
stateGlobal.resizeEdgeSize.value =
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize;
stateGlobal.resizeEdgeSize.value = stateGlobal.isMaximized.isTrue
? kMaximizeEdgeSize
: windowResizeEdgeSize;
}
@override
@@ -340,7 +337,7 @@ class _ConnectionPageState extends State<ConnectionPage>
?.merge(TextStyle(height: 1)),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 0),
waitDuration: Duration(milliseconds: 300),
message: translate("id_input_tip"),
child: Icon(
Icons.help_outline_outlined,

View File

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

View File

@@ -61,9 +61,13 @@ class DesktopSettingPage extends StatefulWidget {
final SettingsTabKey initialTabkey;
static final List<SettingsTabKey> tabKeys = [
SettingsTabKey.general,
if (!bind.isOutgoingOnly() && !bind.isDisableSettings())
if (!bind.isOutgoingOnly() &&
!bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
SettingsTabKey.safety,
if (!bind.isDisableSettings()) SettingsTabKey.network,
if (!bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y')
SettingsTabKey.network,
if (!bind.isIncomingOnly()) SettingsTabKey.display,
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
SettingsTabKey.plugin,
@@ -74,7 +78,8 @@ class DesktopSettingPage extends StatefulWidget {
DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key);
@override
State<DesktopSettingPage> createState() => _DesktopSettingPageState();
State<DesktopSettingPage> createState() =>
_DesktopSettingPageState(initialTabkey);
static void switch2page(SettingsTabKey page) {
try {
@@ -84,8 +89,10 @@ class DesktopSettingPage extends StatefulWidget {
}
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
DesktopTabPage.onAddSetting(initialPage: page);
PageController controller = Get.find(tag: _kSettingPageControllerTag);
Rx<SettingsTabKey> selected = Get.find(tag: _kSettingPageTabKeyTag);
PageController controller =
Get.find<PageController>(tag: _kSettingPageControllerTag);
Rx<SettingsTabKey> selected =
Get.find<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag);
selected.value = page;
controller.jumpToPage(index);
} else {
@@ -105,10 +112,8 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
var initialIndex = DesktopSettingPage.tabKeys.indexOf(widget.initialTabkey);
_DesktopSettingPageState(SettingsTabKey initialTabkey) {
var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey);
if (initialIndex == -1) {
initialIndex = 0;
}
@@ -171,16 +176,32 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
}
List<Widget> _children() {
final children = [
_General(),
if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) _Safety(),
if (!bind.isDisableSettings()) _Network(),
if (!bind.isIncomingOnly()) _Display(),
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
_Plugin(),
if (!bind.isDisableAccount()) _Account(),
_About(),
];
final children = List<Widget>.empty(growable: true);
for (final tab in DesktopSettingPage.tabKeys) {
switch (tab) {
case SettingsTabKey.general:
children.add(const _General());
break;
case SettingsTabKey.safety:
children.add(const _Safety());
break;
case SettingsTabKey.network:
children.add(const _Network());
break;
case SettingsTabKey.display:
children.add(const _Display());
break;
case SettingsTabKey.plugin:
children.add(const _Plugin());
break;
case SettingsTabKey.account:
children.add(const _Account());
break;
case SettingsTabKey.about:
children.add(const _About());
break;
}
}
return children;
}
@@ -493,18 +514,20 @@ class _GeneralState extends State<_General> {
return const Offstage();
}
return AudioInput(builder: (devices, currentDevice, setDevice) {
return _Card(title: 'Audio Input Device', children: [
...devices.map((device) => _Radio<String>(context,
value: device,
groupValue: currentDevice,
autoNewLine: false,
label: device, onChanged: (value) {
setDevice(value);
setState(() {});
}))
]);
});
builder(devices, currentDevice, setDevice) {
final child = ComboBox(
keys: devices,
values: devices,
initialKey: currentDevice,
onChanged: (key) async {
setDevice(key);
setState(() {});
},
).marginOnly(left: _kContentHMargin);
return _Card(title: 'Audio Input Device', children: [child]);
}
return AudioInput(builder: builder, isCm: false, isVoiceCall: false);
}
Widget record(BuildContext context) {
@@ -679,15 +702,24 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
// Simple temp wrapper for PR check
tmpWrapper() {
RxBool has2fa = bind.mainHasValid2FaSync().obs;
RxBool hasBot = bind.mainHasValidBotSync().obs;
update() async {
has2fa.value = bind.mainHasValid2FaSync();
setState(() {});
}
onChanged(bool? checked) async {
change2fa(callback: update);
if (checked == false) {
CommonConfirmDialog(
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
change2fa(callback: update);
});
} else {
change2fa(callback: update);
}
}
return GestureDetector(
final tfa = GestureDetector(
child: InkWell(
child: Obx(() => Row(
children: [
@@ -708,6 +740,77 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
onChanged(!has2fa.value);
},
).marginOnly(left: _kCheckBoxLeftMargin);
if (!has2fa.value) {
return tfa;
}
updateBot() async {
hasBot.value = bind.mainHasValidBotSync();
setState(() {});
}
onChangedBot(bool? checked) async {
if (checked == false) {
CommonConfirmDialog(
gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
changeBot(callback: updateBot);
});
} else {
changeBot(callback: updateBot);
}
}
final bot = GestureDetector(
child: Tooltip(
waitDuration: Duration(milliseconds: 300),
message: translate("enable-bot-tip"),
child: InkWell(
child: Obx(() => Row(
children: [
Checkbox(
value: hasBot.value,
onChanged: enabled ? onChangedBot : null)
.marginOnly(right: 5),
Expanded(
child: Text(
translate('Telegram bot'),
style: TextStyle(
color: disabledTextColor(context, enabled)),
))
],
))),
),
onTap: () {
onChangedBot(!hasBot.value);
},
).marginOnly(left: _kCheckBoxLeftMargin + 30);
final trust = Row(
children: [
Flexible(
child: Tooltip(
waitDuration: Duration(milliseconds: 300),
message: translate("enable-trusted-devices-tip"),
child: _OptionCheckBox(context, "Enable trusted devices",
kOptionEnableTrustedDevices,
enabled: !locked, update: (v) {
setState(() {});
}),
),
),
if (mainGetBoolOptionSync(kOptionEnableTrustedDevices))
ElevatedButton(
onPressed: locked
? null
: () {
manageTrustedDeviceDialog();
},
child: Text(translate('Manage trusted devices')))
],
).marginOnly(left: 30);
return Column(
children: [tfa, bot, trust],
);
}
return tmpWrapper();
@@ -832,12 +935,22 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
label: value,
onChanged: locked
? null
: ((value) {
() async {
: ((value) async {
callback() async {
await model.setVerificationMethod(
passwordKeys[passwordValues.indexOf(value)]);
await model.updatePasswordModel();
}();
}
if (value ==
passwordValues[passwordKeys
.indexOf(kUsePermanentPassword)] &&
(await bind.mainGetPermanentPassword())
.isEmpty) {
setPasswordDialog(notEmptyCallback: callback);
} else {
await callback();
}
}),
))
.toList();
@@ -930,6 +1043,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
_OptionCheckBox(context, 'allow-only-conn-window-open-tip',
'allow-only-conn-window-open',
reverse: false, enabled: enabled),
if (bind.mainIsInstalled()) unlockPin()
]);
}
@@ -1030,12 +1144,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
bool enabled = !locked;
// Simple temp wrapper for PR check
tmpWrapper() {
RxBool hasWhitelist = (bind.mainGetOptionSync(key: kOptionWhitelist) !=
defaultOptionWhitelist)
.obs;
RxBool hasWhitelist = whitelistNotEmpty().obs;
update() async {
hasWhitelist.value = bind.mainGetOptionSync(key: kOptionWhitelist) !=
defaultOptionWhitelist;
hasWhitelist.value = whitelistNotEmpty();
}
onChanged(bool? checked) async {
@@ -1146,7 +1257,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
width: 95,
child: TextField(
controller: controller,
enabled: enabled && !locked && isOptFixed,
enabled: enabled && !locked && !isOptFixed,
onChanged: (_) => applyEnabled.value = true,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(
@@ -1180,6 +1291,40 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
}(),
];
}
Widget unlockPin() {
bool enabled = !locked;
RxString unlockPin = bind.mainGetUnlockPin().obs;
update() async {
unlockPin.value = bind.mainGetUnlockPin();
}
onChanged(bool? checked) async {
changeUnlockPinDialog(unlockPin.value, update);
}
final isOptFixed = isOptionFixed(kOptionWhitelist);
return GestureDetector(
child: Obx(() => Row(
children: [
Checkbox(
value: unlockPin.isNotEmpty,
onChanged: enabled && !isOptFixed ? onChanged : null)
.marginOnly(right: 5),
Expanded(
child: Text(
translate('Unlock with PIN'),
style: TextStyle(color: disabledTextColor(context, enabled)),
))
],
)),
onTap: enabled
? () {
onChanged(!unlockPin.isNotEmpty);
}
: null,
).marginOnly(left: _kCheckBoxLeftMargin);
}
}
class _Network extends StatefulWidget {
@@ -1199,6 +1344,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
super.build(context);
bool enabled = !locked;
final scrollController = ScrollController();
final hideServer =
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
final hideProxy =
bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
@@ -1212,11 +1361,12 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
AbsorbPointer(
absorbing: locked,
child: Column(children: [
server(enabled),
_Card(title: 'Proxy', children: [
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
enabled: enabled),
]),
if (!hideServer) server(enabled),
if (!hideProxy)
_Card(title: 'Proxy', children: [
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
enabled: enabled),
]),
]),
),
]).marginOnly(bottom: _kListViewBottomMargin));
@@ -1715,7 +1865,7 @@ class _AboutState extends State<_About> {
child: SingleChildScrollView(
controller: scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
child: _Card(title: '${translate('About')} RustDesk', children: [
child: _Card(title: translate('About RustDesk'), children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -2070,9 +2220,14 @@ Widget _lock(
Text(translate(label)).marginOnly(left: 5),
]).marginSymmetric(vertical: 2)),
onPressed: () async {
bool checked = await callMainCheckSuperUserPermission();
if (checked) {
onUnlock();
final unlockPin = bind.mainGetUnlockPin();
if (unlockPin.isEmpty) {
bool checked = await callMainCheckSuperUserPermission();
if (checked) {
onUnlock();
}
} else {
checkUnlockPinDialog(unlockPin, onUnlock);
}
},
).marginSymmetric(horizontal: 2, vertical: 4),
@@ -2249,35 +2404,40 @@ void changeSocks5Proxy() async {
children: [
Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Align(
alignment: Alignment.centerRight,
child: Row(
children: [
Text(
translate('Server'),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 0),
message: translate("default_proxy_tip"),
child: Icon(
Icons.help_outline_outlined,
size: 16,
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5),
if (!isMobile)
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Align(
alignment: Alignment.centerRight,
child: Row(
children: [
Text(
translate('Server'),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 0),
message: translate("default_proxy_tip"),
child: Icon(
Icons.help_outline_outlined,
size: 16,
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5),
),
),
),
],
)).marginOnly(right: 10),
),
],
)).marginOnly(right: 10),
),
Expanded(
child: TextField(
decoration: InputDecoration(
errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
labelText: isMobile ? translate('Server') : null,
helperText:
isMobile ? translate("default_proxy_tip") : null,
helperMaxLines: isMobile ? 3 : null,
),
controller: proxyController,
autofocus: true,
@@ -2288,15 +2448,19 @@ void changeSocks5Proxy() async {
).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
'${translate("Username")}:',
textAlign: TextAlign.right,
).marginOnly(right: 10)),
if (!isMobile)
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
'${translate("Username")}:',
textAlign: TextAlign.right,
).marginOnly(right: 10)),
Expanded(
child: TextField(
controller: userController,
decoration: InputDecoration(
labelText: isMobile ? translate('Username') : null,
),
enabled: !isOptFixed,
),
),
@@ -2304,16 +2468,18 @@ void changeSocks5Proxy() async {
).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
'${translate("Password")}:',
textAlign: TextAlign.right,
).marginOnly(right: 10)),
if (!isMobile)
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
'${translate("Password")}:',
textAlign: TextAlign.right,
).marginOnly(right: 10)),
Expanded(
child: Obx(() => TextField(
obscureText: obscure.value,
decoration: InputDecoration(
labelText: isMobile ? translate('Password') : null,
suffixIcon: IconButton(
onPressed: () => obscure.value = !obscure.value,
icon: Icon(obscure.value

View File

@@ -8,6 +8,7 @@ import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
// import 'package:flutter/services.dart';
import '../../common/shared_state.dart';
@@ -20,7 +21,7 @@ class DesktopTabPage extends StatefulWidget {
static void onAddSetting(
{SettingsTabKey initialPage = SettingsTabKey.general}) {
try {
DesktopTabController tabController = Get.find();
DesktopTabController tabController = Get.find<DesktopTabController>();
tabController.add(TabInfo(
key: kTabLabelSettingPage,
label: kTabLabelSettingPage,
@@ -41,21 +42,11 @@ class _DesktopTabPageState extends State<DesktopTabPage>
final tabController = DesktopTabController(tabType: DesktopTabType.main);
final RxBool _block = false.obs;
// bool mouseIn = false;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
shouldBeBlocked(_block, canBeBlocked);
} else if (state == AppLifecycleState.inactive) {}
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
Get.put<DesktopTabController>(tabController);
_DesktopTabPageState() {
RemoteCountState.init();
Get.put<DesktopTabController>(tabController);
tabController.add(TabInfo(
key: kTabLabelHomePage,
label: kTabLabelHomePage,
@@ -78,8 +69,34 @@ class _DesktopTabPageState extends State<DesktopTabPage>
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
shouldBeBlocked(_block, canBeBlocked);
} else if (state == AppLifecycleState.inactive) {}
}
@override
void initState() {
super.initState();
// HardwareKeyboard.instance.addHandler(_handleKeyEvent);
WidgetsBinding.instance.addObserver(this);
}
/*
bool _handleKeyEvent(KeyEvent event) {
if (!mouseIn && event is KeyDownEvent) {
print('key down: ${event.logicalKey}');
shouldBeBlocked(_block, canBeBlocked);
}
return false; // allow it to propagate
}
*/
@override
void dispose() {
// HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
WidgetsBinding.instance.removeObserver(this);
Get.delete<DesktopTabController>();
@@ -102,18 +119,15 @@ class _DesktopTabPageState extends State<DesktopTabPage>
isClose: false,
),
),
blockTab: _block,
)));
widget() => MouseRegion(
onEnter: (_) async {
await shouldBeBlocked(_block, canBeBlocked);
},
child: FocusScope(child: tabWidget, canRequestFocus: !_block.value));
return isMacOS || kUseCompatibleUiMode
? Obx(() => widget())
? tabWidget
: Obx(
() => DragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
child: widget(),
enableResizeEdges: windowManagerEnableResizeEdges,
child: tabWidget,
),
);
}

View File

@@ -92,13 +92,16 @@ class _FileManagerPageState extends State<FileManagerPage>
_ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
});
Get.put(_ffi, tag: 'ft_${widget.id}');
Get.put<FFI>(_ffi, tag: 'ft_${widget.id}');
if (!isLinux) {
WakelockPlus.enable();
}
debugPrint("File manager page init success with id ${widget.id}");
_ffi.dialogManager.setOverlayState(_overlayKeyState);
widget.tabController.onSelected?.call(widget.id);
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id);
});
}
@override
@@ -259,6 +262,7 @@ class _FileManagerPageState extends State<FileManagerPage>
Offstage(
offstage: item.state != JobState.paused,
child: MenuButton(
tooltip: translate("Resume"),
onPressed: () {
jobController.resumeJob(item.id);
},
@@ -271,6 +275,7 @@ class _FileManagerPageState extends State<FileManagerPage>
),
),
MenuButton(
tooltip: translate("Delete"),
padding: EdgeInsets.only(right: 15),
child: SvgPicture.asset(
"assets/close.svg",
@@ -518,6 +523,7 @@ class _FileManagerViewState extends State<FileManagerView> {
Row(
children: [
MenuButton(
tooltip: translate('Back'),
padding: EdgeInsets.only(
right: 3,
),
@@ -537,6 +543,7 @@ class _FileManagerViewState extends State<FileManagerView> {
},
),
MenuButton(
tooltip: translate('Parent directory'),
child: RotatedBox(
quarterTurns: 3,
child: SvgPicture.asset(
@@ -601,6 +608,7 @@ class _FileManagerViewState extends State<FileManagerView> {
switch (_locationStatus.value) {
case LocationStatus.bread:
return MenuButton(
tooltip: translate('Search'),
onPressed: () {
_locationStatus.value = LocationStatus.fileSearchBar;
Future.delayed(
@@ -627,6 +635,7 @@ class _FileManagerViewState extends State<FileManagerView> {
);
case LocationStatus.fileSearchBar:
return MenuButton(
tooltip: translate('Clear'),
onPressed: () {
onSearchText("", isLocal);
_locationStatus.value = LocationStatus.bread;
@@ -642,6 +651,7 @@ class _FileManagerViewState extends State<FileManagerView> {
}
}),
MenuButton(
tooltip: translate('Refresh File'),
padding: EdgeInsets.only(
left: 3,
),
@@ -667,6 +677,7 @@ class _FileManagerViewState extends State<FileManagerView> {
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
children: [
MenuButton(
tooltip: translate('Home'),
padding: EdgeInsets.only(
right: 3,
),
@@ -682,11 +693,27 @@ class _FileManagerViewState extends State<FileManagerView> {
hoverColor: Theme.of(context).hoverColor,
),
MenuButton(
tooltip: translate('Create Folder'),
onPressed: () {
final name = TextEditingController();
String? errorText;
_ffi.dialogManager.show((setState, close, context) {
name.addListener(() {
if (errorText != null) {
setState(() {
errorText = null;
});
}
});
submit() {
if (name.value.text.isNotEmpty) {
if (!PathUtil.validName(name.value.text,
controller.options.value.isWindows)) {
setState(() {
errorText = translate("Invalid folder name");
});
return;
}
controller.createDir(PathUtil.join(
controller.directory.value.path,
name.value.text,
@@ -718,6 +745,7 @@ class _FileManagerViewState extends State<FileManagerView> {
labelText: translate(
"Please enter the folder name",
),
errorText: errorText,
),
controller: name,
autofocus: true,
@@ -751,6 +779,7 @@ class _FileManagerViewState extends State<FileManagerView> {
hoverColor: Theme.of(context).hoverColor,
),
Obx(() => MenuButton(
tooltip: translate('Delete'),
onPressed: SelectedItems.valid(selectedItems.items)
? () async {
await (controller
@@ -882,6 +911,7 @@ class _FileManagerViewState extends State<FileManagerView> {
menuPos = RelativeRect.fromLTRB(x, y, x, y);
},
child: MenuButton(
tooltip: translate('More'),
onPressed: () => mod_menu.showMenu(
context: context,
position: menuPos,
@@ -971,6 +1001,7 @@ class _FileManagerViewState extends State<FileManagerView> {
final lastModifiedStr = entry.isDrive
? " "
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
return Padding(
padding: EdgeInsets.symmetric(vertical: 1),
child: Obx(() => Container(
@@ -1035,6 +1066,35 @@ class _FileManagerViewState extends State<FileManagerView> {
_onSelectedChanged(
items, filteredEntries, entry, isLocal);
},
onSecondaryTap: () {
final items = [
if (!entry.isDrive &&
versionCmp(_ffi.ffiModel.pi.version,
"1.3.0") >=
0)
mod_menu.PopupMenuItem(
child: Text("Rename"),
height: CustomPopupMenuTheme.height,
onTap: () {
controller.renameAction(entry, isLocal);
},
)
];
if (items.isNotEmpty) {
mod_menu.showMenu(
context: context,
position: secondaryPosition,
items: items,
);
}
},
onSecondaryTapDown: (details) {
secondaryPosition = RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy);
},
),
SizedBox(
width: 2.0,

View File

@@ -34,6 +34,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
WindowController.fromWindowId(windowId())
.setTitle(getWindowNameWithId(id));
};
tabController.onRemoved = (_, id) => onRemoveId(id);
tabController.add(TabInfo(
key: params['id'],
label: params['id'],
@@ -54,8 +55,6 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
void initState() {
super.initState();
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print(
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
@@ -97,6 +96,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton(),
selectedBorderColor: MyTheme.accent,
labelGetter: DesktopTab.tablabelGetter,
));
final tabWidget = isLinux
@@ -111,6 +111,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
: SubWindowDragToResizeArea(
child: tabWidget,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId,
);
}

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
@@ -19,9 +21,7 @@ class InstallPage extends StatefulWidget {
class _InstallPageState extends State<InstallPage> {
final tabController = DesktopTabController(tabType: DesktopTabType.main);
@override
void initState() {
super.initState();
_InstallPageState() {
Get.put<DesktopTabController>(tabController);
const label = "install";
tabController.add(TabInfo(
@@ -43,6 +43,7 @@ class _InstallPageState extends State<InstallPage> {
Widget build(BuildContext context) {
return DragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: windowManagerEnableResizeEdges,
child: Container(
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
@@ -73,10 +74,16 @@ class _InstallPageBodyState extends State<_InstallPageBody>
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12),
);
_InstallPageBodyState() {
controller = TextEditingController(text: bind.installInstallPath());
final installOptions = jsonDecode(bind.installInstallOptions());
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
}
@override
void initState() {
windowManager.addListener(this);
controller = TextEditingController(text: bind.installInstallPath());
super.initState();
}
@@ -248,6 +255,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
if (desktopicon.value) args += ' desktopicon';
bind.installInstallMe(options: args, path: controller.text);
}
do_install();
}

View File

@@ -63,9 +63,12 @@ class _PortForwardPageState extends State<PortForwardPage>
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay,
isRdp: widget.isRDP);
Get.put(_ffi, tag: 'pf_${widget.id}');
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
debugPrint("Port forward page init success with id ${widget.id}");
widget.tabController.onSelected?.call(widget.id);
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id);
});
}
@override

View File

@@ -34,6 +34,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
WindowController.fromWindowId(windowId())
.setTitle(getWindowNameWithId(id));
};
tabController.onRemoved = (_, id) => onRemoveId(id);
tabController.add(TabInfo(
key: params['id'],
label: params['id'],
@@ -54,8 +55,6 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
void initState() {
super.initState();
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
debugPrint(
"[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId");
@@ -106,6 +105,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
return true;
},
tail: AddButton(),
selectedBorderColor: MyTheme.accent,
labelGetter: DesktopTab.tablabelGetter,
),
);
@@ -127,6 +127,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
() => SubWindowDragToResizeArea(
child: tabWidget,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId,
),
);

View File

@@ -45,7 +45,9 @@ class RemotePage extends StatefulWidget {
this.switchUuid,
this.forceRelay,
this.isSharedPassword,
}) : super(key: key);
}) : super(key: key) {
initSharedStates(id);
}
final String id;
final SessionID? sessionId;
@@ -64,7 +66,7 @@ class RemotePage extends StatefulWidget {
@override
State<RemotePage> createState() {
final state = _RemotePageState();
final state = _RemotePageState(id);
_lastState.value = state;
return state;
}
@@ -94,8 +96,11 @@ class _RemotePageState extends State<RemotePage>
SessionID get sessionId => _ffi.sessionId;
_RemotePageState(String id) {
_initStates(id);
}
void _initStates(String id) {
initSharedStates(id);
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
_showRemoteCursor = ShowRemoteCursorState.find(id);
_keyboardEnabled = KeyboardEnabledState.find(id);
@@ -105,9 +110,8 @@ class _RemotePageState extends State<RemotePage>
@override
void initState() {
super.initState();
_initStates(widget.id);
_ffi = FFI(widget.sessionId);
Get.put(_ffi, tag: widget.id);
Get.put<FFI>(_ffi, tag: widget.id);
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
@@ -135,11 +139,13 @@ class _RemotePageState extends State<RemotePage>
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');
_zoomCursor.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: kOptionZoomCursor);
WidgetsBinding.instance.addPostFrameCallback((_) {
// Session option should be set after models.dart/FFI.start
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: 'show-remote-cursor');
_zoomCursor.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: kOptionZoomCursor);
});
DesktopMultiWindow.addListener(this);
// if (!_isCustomCursorInited) {
// customCursorController.registerNeedUpdateCursorCallback(
@@ -154,7 +160,10 @@ class _RemotePageState extends State<RemotePage>
// }
_blockableOverlayState.applyFfi(_ffi);
widget.tabController?.onSelected?.call(widget.id);
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController?.onSelected?.call(widget.id);
});
}
@override
@@ -506,12 +515,13 @@ class _RemotePageState extends State<RemotePage>
];
if (!_ffi.canvasModel.cursorEmbedded) {
paints.add(Obx(() => Offstage(
offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse,
child: CursorPaint(
id: widget.id,
zoomCursor: _zoomCursor,
))));
paints
.add(Obx(() => _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse
? Offstage()
: CursorPaint(
id: widget.id,
zoomCursor: _zoomCursor,
)));
}
paints.add(
Positioned(
@@ -564,11 +574,6 @@ class _ImagePaintState extends State<ImagePaint> {
RxBool get remoteCursorMoved => widget.remoteCursorMoved;
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final m = Provider.of<ImageModel>(context);

View File

@@ -71,7 +71,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final ffi = remotePage.ffi;
bind.setCurSessionId(sessionId: ffi.sessionId);
}
WindowController.fromWindowId(windowId())
WindowController.fromWindowId(params['windowId'])
.setTitle(getWindowNameWithId(id));
UnreadChatCountState.find(id).value = 0;
};
@@ -98,15 +98,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
));
_update_remote_count();
}
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
}
@override
void initState() {
super.initState();
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
if (!_isScreenRectSet) {
Future.delayed(Duration.zero, () {
restoreWindowPosition(
@@ -121,11 +120,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
}
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
final child = Scaffold(
@@ -134,6 +128,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton(),
selectedBorderColor: MyTheme.accent,
pageViewBuilder: (pageView) => pageView,
labelGetter: DesktopTab.tablabelGetter,
tabBuilder: (key, icon, label, themeConf) => Obx(() {
@@ -233,6 +228,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
// Specially configured for a better resize area and remote control.
childPadding: kDragToResizeAreaPadding,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId,
));
}
@@ -413,12 +409,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final display = args['display'];
final displays = args['displays'];
final screenRect = parseParamScreenRect(args);
final prePeerCount = tabController.length;
Future.delayed(Duration.zero, () async {
if (stateGlobal.fullscreen.isTrue) {
await WindowController.fromWindowId(windowId()).setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
}
await setNewConnectWindowFrame(windowId(), id!, display, screenRect);
await setNewConnectWindowFrame(
windowId(), id!, prePeerCount, display, screenRect);
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
await windowOnTop(windowId());
});

View File

@@ -32,14 +32,18 @@ class DesktopServerPage extends StatefulWidget {
class _DesktopServerPageState extends State<DesktopServerPage>
with WindowListener, AutomaticKeepAliveClientMixin {
final tabController = gFFI.serverModel.tabController;
@override
void initState() {
_DesktopServerPageState() {
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
windowManager.addListener(this);
Get.put(tabController);
Get.put<DesktopTabController>(tabController);
tabController.onRemoved = (_, id) {
onRemoveId(id);
};
}
@override
void initState() {
windowManager.addListener(this);
super.initState();
}
@@ -79,7 +83,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
child: Consumer<ServerModel>(
builder: (context, serverModel, child) {
final body = Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
backgroundColor: Theme.of(context).colorScheme.background,
body: ConnectionManager(),
);
return isLinux
@@ -104,10 +108,11 @@ class ConnectionManager extends StatefulWidget {
State<StatefulWidget> createState() => ConnectionManagerState();
}
class ConnectionManagerState extends State<ConnectionManager> {
@override
void initState() {
gFFI.serverModel.updateClientState();
class ConnectionManagerState extends State<ConnectionManager>
with WidgetsBindingObserver {
final RxBool _block = false.obs;
ConnectionManagerState() {
gFFI.serverModel.tabController.onSelected = (client_id_str) {
final client_id = int.tryParse(client_id_str);
if (client_id != null) {
@@ -116,7 +121,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
if (client != null) {
gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
if (client.unreadChatMessageCount.value > 0) {
Future.delayed(Duration.zero, () {
WidgetsBinding.instance.addPostFrameCallback((_) {
client.unreadChatMessageCount.value = 0;
gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
});
@@ -127,9 +132,31 @@ class ConnectionManagerState extends State<ConnectionManager> {
}
};
gFFI.chatModel.isConnManager = true;
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
if (!allowRemoteCMModification()) {
shouldBeBlocked(_block, null);
}
}
}
@override
void initState() {
gFFI.serverModel.updateClientState();
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
final serverModel = Provider.of<ServerModel>(context);
@@ -165,8 +192,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
selectedBorderColor: MyTheme.accent,
maxLabelWidth: 100,
tail: null, //buildScrollJumper(),
selectedTabBackgroundColor:
Theme.of(context).hintColor.withOpacity(0),
blockTab: allowRemoteCMModification() ? null : _block,
tabBuilder: (key, icon, label, themeConf) {
final client = serverModel.clients
.firstWhereOrNull((client) => client.id.toString() == key);
@@ -201,27 +227,28 @@ class ConnectionManagerState extends State<ConnectionManager> {
borderWidth;
final realChatPageWidth =
constrains.maxWidth - realClosedWidth;
return Row(children: [
final row = Row(children: [
if (constrains.maxWidth >
kConnectionManagerWindowSizeClosedChat.width)
Consumer<ChatModel>(
builder: (_, model, child) => SizedBox(
width: realChatPageWidth,
child: buildRemoteBlock(
child: Container(
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Theme.of(context)
.dividerColor))),
child: buildSidePage()),
),
child: allowRemoteCMModification()
? buildSidePage()
: buildRemoteBlock(
child: buildSidePage(),
block: _block,
mask: true),
)),
SizedBox(
width: realClosedWidth,
child:
SizedBox(width: realClosedWidth, child: pageView)),
]);
return Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: row,
);
},
),
),
@@ -381,7 +408,10 @@ class _CmHeaderState extends State<_CmHeader>
_time.value = _time.value + 1;
}
});
gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
});
}
@override
@@ -714,7 +744,8 @@ class _CmControlPanel extends StatelessWidget {
child: buildButton(context,
color: MyTheme.accent,
onClick: null, onTapDown: (details) async {
final devicesInfo = await AudioInput.getDevicesInfo();
final devicesInfo =
await AudioInput.getDevicesInfo(true, true);
List<String> devices = devicesInfo['devices'] as List<String>;
if (devices.isEmpty) {
msgBox(
@@ -740,13 +771,14 @@ class _CmControlPanel extends StatelessWidget {
value: d,
height: 18,
padding: EdgeInsets.zero,
onTap: () => AudioInput.setDevice(d),
onTap: () => AudioInput.setDevice(d, true, true),
child: IgnorePointer(
child: RadioMenuButton(
value: d,
groupValue: currentDevice,
onChanged: (v) {
if (v != null) AudioInput.setDevice(v);
if (v != null)
AudioInput.setDevice(v, true, true);
},
child: Container(
child: Text(
@@ -1043,6 +1075,10 @@ class _CmControlPanel extends StatelessWidget {
}
void checkClickTime(int id, Function() callback) async {
if (allowRemoteCMModification()) {
callback();
return;
}
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
await bind.cmCheckClickTime(connId: id);
Timer(const Duration(milliseconds: 120), () async {
@@ -1051,6 +1087,11 @@ void checkClickTime(int id, Function() callback) async {
});
}
bool allowRemoteCMModification() {
return option2bool(kOptionAllowRemoteCmModification,
bind.mainGetLocalOption(key: kOptionAllowRemoteCmModification));
}
class _FileTransferLogPage extends StatefulWidget {
_FileTransferLogPage({Key? key}) : super(key: key);
@@ -1116,6 +1157,16 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
Text(translate('Create Folder'))
],
);
case CmFileAction.rename:
return Column(
children: [
Icon(
Icons.drive_file_move_outlined,
color: Theme.of(context).tabBarTheme.labelColor,
),
Text(translate('Rename'))
],
);
}
}

View File

@@ -34,6 +34,7 @@ class _MenuButtonState extends State<MenuButton> {
return Padding(
padding: widget.padding,
child: Tooltip(
waitDuration: Duration(milliseconds: 300),
message: widget.tooltip,
child: Material(
type: MaterialType.transparency,

View File

@@ -38,18 +38,16 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
@override
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>();
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>(enabled?.value);
}
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
extends State<W> {
RxBool enabled = true.obs;
@override
void initState() {
super.initState();
if (widget.enabled != null) {
enabled.value = widget.enabled!.value;
MyPopupMenuItemState(bool? e) {
if (e != null) {
enabled.value = e;
}
}

View File

@@ -372,7 +372,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
initState() {
super.initState();
Future.delayed(Duration.zero, () async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
_fractionX.value = double.tryParse(await bind.sessionGetOption(
sessionId: widget.ffi.sessionId,
arg: 'remote-menubar-drag-x') ??
@@ -1032,11 +1032,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
FFI get ffi => widget.ffi;
String get id => widget.id;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
_screenAdjustor.updateScreen();
@@ -1052,15 +1047,11 @@ class _DisplayMenuState extends State<_DisplayMenu> {
ffi: widget.ffi,
screenAdjustor: _screenAdjustor,
),
if (pi.isRustDeskIdd)
_RustDeskVirtualDisplayMenu(
id: widget.id,
ffi: widget.ffi,
),
if (pi.isAmyuniIdd)
_AmyuniVirtualDisplayMenu(
id: widget.id,
if (showVirtualDisplayMenu(ffi))
_SubmenuButton(
ffi: widget.ffi,
menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
child: Text(translate("Virtual display")),
),
cursorToggles(),
Divider(),
@@ -1282,7 +1273,9 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
@override
void initState() {
super.initState();
_getLocalResolutionWayland();
WidgetsBinding.instance.addPostFrameCallback((_) {
_getLocalResolutionWayland();
});
}
Rect? scaledRect() {
@@ -1559,155 +1552,6 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
}
}
class _RustDeskVirtualDisplayMenu extends StatefulWidget {
final String id;
final FFI ffi;
_RustDeskVirtualDisplayMenu({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
State<_RustDeskVirtualDisplayMenu> createState() =>
_RustDeskVirtualDisplayMenuState();
}
class _RustDeskVirtualDisplayMenuState
extends State<_RustDeskVirtualDisplayMenu> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
return Offstage();
}
if (!widget.ffi.ffiModel.pi.isInstalled) {
return Offstage();
}
final virtualDisplays = widget.ffi.ffiModel.pi.RustDeskVirtualDisplays;
final privacyModeState = PrivacyModeState.find(widget.id);
final children = <Widget>[];
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
children.add(Obx(() => CkbMenuButton(
value: virtualDisplays.contains(i + 1),
onChanged: privacyModeState.isNotEmpty
? null
: (bool? value) async {
if (value != null) {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: i + 1,
on: value);
}
},
child: Text('${translate('Virtual display')} ${i + 1}'),
ffi: widget.ffi,
)));
}
children.add(Divider());
children.add(Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
},
ffi: widget.ffi,
child: Text(translate('Plug out all')),
)));
return _SubmenuButton(
ffi: widget.ffi,
menuChildren: children,
child: Text(translate("Virtual display")),
);
}
}
class _AmyuniVirtualDisplayMenu extends StatefulWidget {
final String id;
final FFI ffi;
_AmyuniVirtualDisplayMenu({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
State<_AmyuniVirtualDisplayMenu> createState() =>
_AmiyuniVirtualDisplayMenuState();
}
class _AmiyuniVirtualDisplayMenuState extends State<_AmyuniVirtualDisplayMenu> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
return Offstage();
}
if (!widget.ffi.ffiModel.pi.isInstalled) {
return Offstage();
}
final count = widget.ffi.ffiModel.pi.amyuniVirtualDisplayCount;
final privacyModeState = PrivacyModeState.find(widget.id);
final children = <Widget>[
Obx(() => Row(
children: [
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () => bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId, index: 0, on: false),
child: Icon(Icons.remove),
),
Text(count.toString()),
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 4
? null
: () => bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId, index: 0, on: true),
child: Icon(Icons.add),
),
],
)),
Divider(),
Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
},
ffi: widget.ffi,
child: Text(translate('Plug out all')),
)),
];
return _SubmenuButton(
ffi: widget.ffi,
menuChildren: children,
child: Text(translate("Virtual display")),
);
}
}
class _KeyboardMenu extends StatelessWidget {
final String id;
final FFI ffi;
@@ -1741,6 +1585,7 @@ class _KeyboardMenu extends StatelessWidget {
viewMode(),
Divider(),
...toolbarToggles(),
...mobileActions(),
]);
}
@@ -1877,6 +1722,39 @@ class _KeyboardMenu extends StatelessWidget {
ffi: ffi,
child: Text(translate('View Mode')));
}
mobileActions() {
if (pi.platform != kPeerPlatformAndroid) return [];
final enabled = versionCmp(pi.version, '1.2.7') >= 0;
if (!enabled) return [];
return [
Divider(),
MenuButton(
child: Text(translate('Back')),
onPressed: () => ffi.inputModel.onMobileBack(),
ffi: ffi),
MenuButton(
child: Text(translate('Home')),
onPressed: () => ffi.inputModel.onMobileHome(),
ffi: ffi),
MenuButton(
child: Text(translate('Apps')),
onPressed: () => ffi.inputModel.onMobileApps(),
ffi: ffi),
MenuButton(
child: Text(translate('Volume up')),
onPressed: () => ffi.inputModel.onMobileVolumeUp(),
ffi: ffi),
MenuButton(
child: Text(translate('Volume down')),
onPressed: () => ffi.inputModel.onMobileVolumeDown(),
ffi: ffi),
MenuButton(
child: Text(translate('Power')),
onPressed: () => ffi.inputModel.onMobilePower(),
ffi: ffi),
];
}
}
class _ChatMenu extends StatefulWidget {
@@ -1950,28 +1828,31 @@ class _VoiceCallMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
menuChildrenGetter() {
final audioInput =
AudioInput(builder: (devices, currentDevice, setDevice) {
return Column(
children: devices
.map((d) => RdoMenuButton<String>(
child: Container(
child: Text(
d,
overflow: TextOverflow.ellipsis,
final audioInput = AudioInput(
builder: (devices, currentDevice, setDevice) {
return Column(
children: devices
.map((d) => RdoMenuButton<String>(
child: Container(
child: Text(
d,
overflow: TextOverflow.ellipsis,
),
constraints: BoxConstraints(maxWidth: 250),
),
constraints: BoxConstraints(maxWidth: 250),
),
value: d,
groupValue: currentDevice,
onChanged: (v) {
if (v != null) setDevice(v);
},
ffi: ffi,
))
.toList(),
);
});
value: d,
groupValue: currentDevice,
onChanged: (v) {
if (v != null) setDevice(v);
},
ffi: ffi,
))
.toList(),
);
},
isCm: false,
isVoiceCall: true,
);
return [
audioInput,
Divider(),

View File

@@ -227,11 +227,9 @@ typedef TabMenuBuilder = Widget Function(String key);
typedef LabelGetter = Rx<String> Function(String key);
/// [_lastClickTime], help to handle double click
int _lastClickTime =
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
int _lastClickTime = 0;
// ignore: must_be_immutable
class DesktopTab extends StatelessWidget {
class DesktopTab extends StatefulWidget {
final bool showLogo;
final bool showTitle;
final bool showMinimize;
@@ -248,15 +246,12 @@ class DesktopTab extends StatelessWidget {
final Color? selectedTabBackgroundColor;
final Color? unSelectedTabBackgroundColor;
final Color? selectedBorderColor;
final RxBool? blockTab;
final DesktopTabController controller;
Rx<DesktopTabState> get state => controller.state;
final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
late final DesktopTabType tabType;
late final bool isMainWindow;
final RxList<String> invisibleTabKeys = RxList.empty();
DesktopTab({
@@ -277,38 +272,260 @@ class DesktopTab extends StatelessWidget {
this.selectedTabBackgroundColor,
this.unSelectedTabBackgroundColor,
this.selectedBorderColor,
}) : super(key: key) {
tabType = controller.tabType;
isMainWindow = tabType == DesktopTabType.main ||
tabType == DesktopTabType.cm ||
tabType == DesktopTabType.install;
}
this.blockTab,
}) : super(key: key);
static RxString tablabelGetter(String peerId) {
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
return RxString(getDesktopTabLabel(peerId, alias));
}
@override
State<DesktopTab> createState() {
return _DesktopTabState();
}
}
// ignore: must_be_immutable
class _DesktopTabState extends State<DesktopTab>
with MultiWindowListener, WindowListener {
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
Timer? _macOSCheckRestoreTimer;
int _macOSCheckRestoreCounter = 0;
bool get showLogo => widget.showLogo;
bool get showTitle => widget.showTitle;
bool get showMinimize => widget.showMinimize;
bool get showMaximize => widget.showMaximize;
bool get showClose => widget.showClose;
Widget Function(Widget pageView)? get pageViewBuilder =>
widget.pageViewBuilder;
TabMenuBuilder? get tabMenuBuilder => widget.tabMenuBuilder;
Widget? get tail => widget.tail;
Future<bool> Function()? get onWindowCloseButton =>
widget.onWindowCloseButton;
TabBuilder? get tabBuilder => widget.tabBuilder;
LabelGetter? get labelGetter => widget.labelGetter;
double? get maxLabelWidth => widget.maxLabelWidth;
Color? get selectedTabBackgroundColor => widget.selectedTabBackgroundColor;
Color? get unSelectedTabBackgroundColor =>
widget.unSelectedTabBackgroundColor;
Color? get selectedBorderColor => widget.selectedBorderColor;
RxBool? get blockTab => widget.blockTab;
DesktopTabController get controller => widget.controller;
RxList<String> get invisibleTabKeys => widget.invisibleTabKeys;
Debouncer get _scrollDebounce => widget._scrollDebounce;
Rx<DesktopTabState> get state => controller.state;
DesktopTabType get tabType => controller.tabType;
bool get isMainWindow =>
tabType == DesktopTabType.main ||
tabType == DesktopTabType.cm ||
tabType == DesktopTabType.install;
_DesktopTabState() : super();
static RxString tablabelGetter(String peerId) {
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
return RxString(getDesktopTabLabel(peerId, alias));
}
@override
void initState() {
super.initState();
DesktopMultiWindow.addListener(this);
windowManager.addListener(this);
Future.delayed(Duration(milliseconds: 500), () {
if (isMainWindow) {
windowManager.isMaximized().then((maximized) {
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
} else {
final wc = WindowController.fromWindowId(kWindowId!);
wc.isMaximized().then((maximized) {
debugPrint("isMaximized $maximized");
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
}
});
}
@override
void dispose() {
DesktopMultiWindow.removeListener(this);
windowManager.removeListener(this);
_macOSCheckRestoreTimer?.cancel();
super.dispose();
}
void _setMaximized(bool maximize) {
stateGlobal.setMaximized(maximize);
_saveFrameDebounce.call(_saveFrame);
setState(() {});
}
@override
void onWindowFocus() {
stateGlobal.isFocused.value = true;
}
@override
void onWindowBlur() {
stateGlobal.isFocused.value = false;
}
@override
void onWindowMinimize() {
stateGlobal.setMinimized(true);
stateGlobal.setMaximized(false);
super.onWindowMinimize();
}
@override
void onWindowMaximize() {
stateGlobal.setMinimized(false);
_setMaximized(true);
super.onWindowMaximize();
}
@override
void onWindowUnmaximize() {
stateGlobal.setMinimized(false);
_setMaximized(false);
super.onWindowUnmaximize();
}
_saveFrame() async {
if (tabType == DesktopTabType.main) {
await saveWindowPosition(WindowType.Main);
} else if (kWindowType != null && kWindowId != null) {
await saveWindowPosition(kWindowType!, windowId: kWindowId);
}
}
@override
void onWindowMoved() {
_saveFrameDebounce.call(_saveFrame);
super.onWindowMoved();
}
@override
void onWindowResized() {
_saveFrameDebounce.call(_saveFrame);
super.onWindowMoved();
}
@override
void onWindowClose() async {
mainWindowClose() async => await windowManager.hide();
notMainWindowClose(WindowController windowController) async {
if (controller.length != 0) {
debugPrint("close not empty multiwindow from taskbar");
if (isWindows) {
await windowController.show();
await windowController.focus();
final res = await onWindowCloseButton?.call() ?? true;
if (!res) return;
}
controller.clear();
}
await windowController.hide();
await rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
}
macOSWindowClose(
Future<bool> Function() checkFullscreen,
Future<void> Function() closeFunc,
) async {
_macOSCheckRestoreCounter = 0;
_macOSCheckRestoreTimer =
Timer.periodic(Duration(milliseconds: 30), (timer) async {
_macOSCheckRestoreCounter++;
if (!await checkFullscreen() || _macOSCheckRestoreCounter >= 30) {
_macOSCheckRestoreTimer?.cancel();
_macOSCheckRestoreTimer = null;
Timer(Duration(milliseconds: 700), () async => await closeFunc());
}
});
}
// hide window on close
if (isMainWindow) {
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
}
// macOS specific workaround, the window is not hiding when in fullscreen.
if (isMacOS && await windowManager.isFullScreen()) {
await windowManager.setFullScreen(false);
await macOSWindowClose(
() async => await windowManager.isFullScreen(),
mainWindowClose,
);
} else {
await mainWindowClose();
}
} else {
// it's safe to hide the subwindow
final controller = WindowController.fromWindowId(kWindowId!);
if (isMacOS) {
// onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
// use ??= to make sure the value is set on first call.
if (await onWindowCloseButton?.call() ?? true) {
if (await controller.isFullScreen()) {
await controller.setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
await macOSWindowClose(
() async => await controller.isFullScreen(),
() async => await notMainWindowClose(controller),
);
} else {
await notMainWindowClose(controller);
}
}
} else {
await notMainWindowClose(controller);
}
}
super.onWindowClose();
}
@override
Widget build(BuildContext context) {
return Column(children: [
Obx(() => Offstage(
offstage: !stateGlobal.showTabBar.isTrue ||
(kUseCompatibleUiMode && isHideSingleItem()),
child: SizedBox(
Obx(() {
if (stateGlobal.showTabBar.isTrue &&
!(kUseCompatibleUiMode && isHideSingleItem())) {
final showBottomDivider = _showTabBarBottomDivider(tabType);
return SizedBox(
height: _kTabBarHeight,
child: Column(
children: [
SizedBox(
height: _kTabBarHeight - 1,
height:
showBottomDivider ? _kTabBarHeight - 1 : _kTabBarHeight,
child: _buildBar(),
),
const Divider(
height: 1,
),
if (showBottomDivider)
const Divider(
height: 1,
),
],
),
))),
);
} else {
return Offstage();
}
}),
Expanded(
child: pageViewBuilder != null
? pageViewBuilder!(_buildPageView())
@@ -317,10 +534,15 @@ class DesktopTab extends StatelessWidget {
}
Widget _buildBlock({required Widget child}) {
if (tabType != DesktopTabType.main) {
if (blockTab != null) {
return buildRemoteBlock(
child: child,
block: blockTab!,
use: canBeBlocked,
mask: tabType == DesktopTabType.main);
} else {
return child;
}
return buildRemoteBlock(child: child, use: canBeBlocked);
}
List<Widget> _tabWidgets = [];
@@ -457,7 +679,6 @@ class DesktopTab extends StatelessWidget {
// hide simulated action buttons when we in compatible ui mode, because of reusing system title bar.
WindowActionPanel(
isMainWindow: isMainWindow,
tabType: tabType,
state: state,
tabController: controller,
invisibleTabKeys: invisibleTabKeys,
@@ -475,7 +696,6 @@ class DesktopTab extends StatelessWidget {
class WindowActionPanel extends StatefulWidget {
final bool isMainWindow;
final DesktopTabType tabType;
final Rx<DesktopTabState> state;
final DesktopTabController tabController;
@@ -491,7 +711,6 @@ class WindowActionPanel extends StatefulWidget {
const WindowActionPanel(
{Key? key,
required this.isMainWindow,
required this.tabType,
required this.state,
required this.tabController,
required this.invisibleTabKeys,
@@ -509,180 +728,7 @@ class WindowActionPanel extends StatefulWidget {
}
}
class WindowActionPanelState extends State<WindowActionPanel>
with MultiWindowListener, WindowListener {
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
Timer? _macOSCheckRestoreTimer;
int _macOSCheckRestoreCounter = 0;
@override
void initState() {
super.initState();
DesktopMultiWindow.addListener(this);
windowManager.addListener(this);
Future.delayed(Duration(milliseconds: 500), () {
if (widget.isMainWindow) {
windowManager.isMaximized().then((maximized) {
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
} else {
final wc = WindowController.fromWindowId(kWindowId!);
wc.isMaximized().then((maximized) {
debugPrint("isMaximized $maximized");
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
}
});
}
@override
void dispose() {
DesktopMultiWindow.removeListener(this);
windowManager.removeListener(this);
_macOSCheckRestoreTimer?.cancel();
super.dispose();
}
void _setMaximized(bool maximize) {
stateGlobal.setMaximized(maximize);
_saveFrameDebounce.call(_saveFrame);
setState(() {});
}
@override
void onWindowFocus() {
stateGlobal.isFocused.value = true;
}
@override
void onWindowBlur() {
stateGlobal.isFocused.value = false;
}
@override
void onWindowMinimize() {
stateGlobal.setMinimized(true);
stateGlobal.setMaximized(false);
super.onWindowMinimize();
}
@override
void onWindowMaximize() {
stateGlobal.setMinimized(false);
_setMaximized(true);
super.onWindowMaximize();
}
@override
void onWindowUnmaximize() {
stateGlobal.setMinimized(false);
_setMaximized(false);
super.onWindowUnmaximize();
}
_saveFrame() async {
if (widget.tabType == DesktopTabType.main) {
await saveWindowPosition(WindowType.Main);
} else if (kWindowType != null && kWindowId != null) {
await saveWindowPosition(kWindowType!, windowId: kWindowId);
}
}
@override
void onWindowMoved() {
_saveFrameDebounce.call(_saveFrame);
super.onWindowMoved();
}
@override
void onWindowResized() {
_saveFrameDebounce.call(_saveFrame);
super.onWindowMoved();
}
@override
void onWindowClose() async {
mainWindowClose() async => await windowManager.hide();
notMainWindowClose(WindowController controller) async {
if (widget.tabController.length != 0) {
debugPrint("close not empty multiwindow from taskbar");
if (isWindows) {
await controller.show();
await controller.focus();
final res = await widget.onClose?.call() ?? true;
if (!res) return;
}
widget.tabController.clear();
}
await controller.hide();
await rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
}
macOSWindowClose(
Future<bool> Function() checkFullscreen,
Future<void> Function() closeFunc,
) async {
_macOSCheckRestoreCounter = 0;
_macOSCheckRestoreTimer =
Timer.periodic(Duration(milliseconds: 30), (timer) async {
_macOSCheckRestoreCounter++;
if (!await checkFullscreen() || _macOSCheckRestoreCounter >= 30) {
_macOSCheckRestoreTimer?.cancel();
_macOSCheckRestoreTimer = null;
Timer(Duration(milliseconds: 700), () async => await closeFunc());
}
});
}
// hide window on close
if (widget.isMainWindow) {
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
}
// macOS specific workaround, the window is not hiding when in fullscreen.
if (isMacOS && await windowManager.isFullScreen()) {
await windowManager.setFullScreen(false);
await macOSWindowClose(
() async => await windowManager.isFullScreen(),
mainWindowClose,
);
} else {
await mainWindowClose();
}
} else {
// it's safe to hide the subwindow
final controller = WindowController.fromWindowId(kWindowId!);
if (isMacOS) {
// onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
// use ??= to make sure the value is set on first call.
if (await widget.onClose?.call() ?? true) {
if (await controller.isFullScreen()) {
await controller.setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
await macOSWindowClose(
() async => await controller.isFullScreen(),
() async => await notMainWindowClose(controller),
);
} else {
await notMainWindowClose(controller);
}
}
} else {
await notMainWindowClose(controller);
}
}
super.onWindowClose();
}
class WindowActionPanelState extends State<WindowActionPanel> {
bool showTabDowndown() {
return widget.tabController.state.value.tabs.length > 1 &&
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
@@ -703,72 +749,69 @@ class WindowActionPanelState extends State<WindowActionPanel>
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Obx(() => Offstage(
offstage:
!(showTabDowndown() && existingInvisibleTab().isNotEmpty),
child: _TabDropDownButton(
controller: widget.tabController,
labelGetter: widget.labelGetter,
tabkeys: existingInvisibleTab()),
)),
Offstage(offstage: widget.tail == null, child: widget.tail),
Offstage(
offstage: kUseCompatibleUiMode,
child: Row(
Obx(() {
if (showTabDowndown() && existingInvisibleTab().isNotEmpty) {
return _TabDropDownButton(
controller: widget.tabController,
labelGetter: widget.labelGetter,
tabkeys: existingInvisibleTab());
} else {
return Offstage();
}
}),
if (widget.tail != null) widget.tail!,
if (!kUseCompatibleUiMode)
Row(
children: [
Offstage(
offstage: !widget.showMinimize || isMacOS,
child: ActionIcon(
message: 'Minimize',
icon: IconFont.min,
onTap: () {
if (widget.isMainWindow) {
windowManager.minimize();
} else {
WindowController.fromWindowId(kWindowId!).minimize();
}
},
isClose: false,
)),
Offstage(
offstage: !widget.showMaximize || isMacOS,
child: Obx(() => ActionIcon(
message: stateGlobal.isMaximized.isTrue
? 'Restore'
: 'Maximize',
icon: stateGlobal.isMaximized.isTrue
? IconFont.restore
: IconFont.max,
onTap: bind.isIncomingOnly() && isInHomePage()
? null
: _toggleMaximize,
isClose: false,
))),
Offstage(
offstage: !widget.showClose || isMacOS,
child: ActionIcon(
message: 'Close',
icon: IconFont.close,
onTap: () async {
final res = await widget.onClose?.call() ?? true;
if (res) {
// hide for all window
// note: the main window can be restored by tray icon
Future.delayed(Duration.zero, () async {
if (widget.isMainWindow) {
await windowManager.close();
} else {
await WindowController.fromWindowId(kWindowId!)
.close();
}
});
}
},
isClose: true,
))
if (widget.showMinimize && !isMacOS)
ActionIcon(
message: 'Minimize',
icon: IconFont.min,
onTap: () {
if (widget.isMainWindow) {
windowManager.minimize();
} else {
WindowController.fromWindowId(kWindowId!).minimize();
}
},
isClose: false,
),
if (widget.showMaximize && !isMacOS)
Obx(() => ActionIcon(
message: stateGlobal.isMaximized.isTrue
? 'Restore'
: 'Maximize',
icon: stateGlobal.isMaximized.isTrue
? IconFont.restore
: IconFont.max,
onTap: bind.isIncomingOnly() && isInHomePage()
? null
: _toggleMaximize,
isClose: false,
)),
if (widget.showClose && !isMacOS)
ActionIcon(
message: 'Close',
icon: IconFont.close,
onTap: () async {
final res = await widget.onClose?.call() ?? true;
if (res) {
// hide for all window
// note: the main window can be restored by tray icon
Future.delayed(Duration.zero, () async {
if (widget.isMainWindow) {
await windowManager.close();
} else {
await WindowController.fromWindowId(kWindowId!)
.close();
}
});
}
},
isClose: true,
)
],
),
),
],
);
}
@@ -1121,7 +1164,10 @@ class _TabState extends State<_Tab> with RestorationMixin {
child: Row(
children: [
SizedBox(
height: _kTabBarHeight,
// _kTabBarHeight also displays normally
height: _showTabBarBottomDivider(widget.tabType)
? _kTabBarHeight - 1
: _kTabBarHeight,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@@ -1172,22 +1218,26 @@ class _CloseButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
width: _kIconSize,
child: Offstage(
offstage: !visible,
child: InkWell(
hoverColor: MyTheme.tabbar(context).closeHoverColor,
customBorder: const CircleBorder(),
onTap: () => onClose(),
child: Icon(
Icons.close,
size: _kIconSize,
color: tabSelected
? MyTheme.tabbar(context).selectedIconColor
: MyTheme.tabbar(context).unSelectedIconColor,
),
),
)).paddingOnly(left: 10);
width: _kIconSize,
child: () {
if (visible) {
return InkWell(
hoverColor: MyTheme.tabbar(context).closeHoverColor,
customBorder: const CircleBorder(),
onTap: () => onClose(),
child: Icon(
Icons.close,
size: _kIconSize,
color: tabSelected
? MyTheme.tabbar(context).selectedIconColor
: MyTheme.tabbar(context).unSelectedIconColor,
),
);
} else {
return Offstage();
}
}())
.paddingOnly(left: 10);
}
}
@@ -1216,13 +1266,7 @@ class ActionIcon extends StatefulWidget {
}
class _ActionIconState extends State<ActionIcon> {
var hover = false.obs;
@override
void initState() {
super.initState();
hover.value = false;
}
final hover = false.obs;
@override
Widget build(BuildContext context) {
@@ -1341,27 +1385,30 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
child: InkWell(child: Text(label)),
),
Obx(
() => Offstage(
offstage: !(tabInfo?.onTabCloseButton != null &&
menuHover.value),
child: InkWell(
onTap: () {
tabInfo?.onTabCloseButton?.call();
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
onHover: (event) =>
setState(() => btnHover.value = true),
onExit: (event) =>
setState(() => btnHover.value = false),
child: Icon(Icons.close,
color:
btnHover.value ? Colors.red : null))),
),
)
() {
if (tabInfo?.onTabCloseButton != null &&
menuHover.value) {
return InkWell(
onTap: () {
tabInfo?.onTabCloseButton?.call();
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
onHover: (event) =>
setState(() => btnHover.value = true),
onExit: (event) =>
setState(() => btnHover.value = false),
child: Icon(Icons.close,
color:
btnHover.value ? Colors.red : null)));
} else {
return Offstage();
}
},
),
],
),
),
@@ -1373,6 +1420,10 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
}
}
bool _showTabBarBottomDivider(DesktopTabType tabType) {
return tabType == DesktopTabType.main || tabType == DesktopTabType.install;
}
class TabbarTheme extends ThemeExtension<TabbarTheme> {
final Color? selectedTabIconColor;
final Color? unSelectedTabIconColor;

View File

@@ -260,7 +260,7 @@ showCmWindow({bool isStartup = false}) async {
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker();
bind.mainHideDock();
await Future.wait([
windowManager.show(),
windowManager.focus(),
@@ -288,14 +288,14 @@ hideCmWindow({bool isStartup = false}) async {
size: kConnectionManagerWindowSizeClosedChat);
windowManager.setOpacity(0);
await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker();
bind.mainHideDock();
await windowManager.minimize();
await windowManager.hide();
_isCmReadyToShow = true;
} else if (_isCmReadyToShow) {
if (await windowManager.getOpacity() != 0) {
await windowManager.setOpacity(0);
bind.mainHideDocker();
bind.mainHideDock();
await windowManager.minimize();
await windowManager.hide();
}

View File

@@ -50,19 +50,26 @@ class _ConnectionPageState extends State<ConnectionPage> {
bool isPeersLoaded = false;
StreamSubscription? _uniLinksSubscription;
_ConnectionPageState() {
if (!isWeb) _uniLinksSubscription = listenUniLinks();
_idController.addListener(() {
_idEmpty.value = _idController.text.isEmpty;
});
Get.put<IDTextEditingController>(_idController);
}
@override
void initState() {
super.initState();
if (!isWeb) _uniLinksSubscription = listenUniLinks();
if (_idController.text.isEmpty) {
() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final lastRemoteId = await bind.mainGetLastRemoteId();
if (lastRemoteId != _idController.id) {
setState(() {
_idController.id = lastRemoteId;
});
}
}();
});
}
if (isAndroid) {
if (!bind.isCustomClient()) {
@@ -72,11 +79,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
});
}
}
_idController.addListener(() {
_idEmpty.value = _idController.text.isEmpty;
});
Get.put<IDTextEditingController>(_idController);
}
@override
@@ -395,7 +397,7 @@ class _WebMenuState extends State<WebMenu> {
[
PopupMenuItem(
value: "about",
child: Text('${translate('About')} RustDesk'),
child: Text(translate('About RustDesk')),
)
];
},

View File

@@ -204,36 +204,54 @@ class _FileManagerPageState extends State<FileManagerPage> {
setState(() {});
} else if (v == "folder") {
final name = TextEditingController();
gFFI.dialogManager
.show((setState, close, context) => CustomAlertDialog(
title: Text(translate("Create Folder")),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
decoration: InputDecoration(
labelText: translate(
"Please enter the folder name"),
),
controller: name,
),
],
String? errorText;
gFFI.dialogManager.show((setState, close, context) {
name.addListener(() {
if (errorText != null) {
setState(() {
errorText = null;
});
}
});
return CustomAlertDialog(
title: Text(translate("Create Folder")),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
decoration: InputDecoration(
labelText:
translate("Please enter the folder name"),
errorText: errorText,
),
actions: [
dialogButton("Cancel",
onPressed: () => close(false),
isOutline: true),
dialogButton("OK", onPressed: () {
if (name.value.text.isNotEmpty) {
currentFileController.createDir(
PathUtil.join(
currentDir.path,
name.value.text,
currentOptions.isWindows));
close();
}
})
]));
controller: name,
),
],
),
actions: [
dialogButton("Cancel",
onPressed: () => close(false), isOutline: true),
dialogButton("OK", onPressed: () {
if (name.value.text.isNotEmpty) {
if (!PathUtil.validName(
name.value.text,
currentFileController
.options.value.isWindows)) {
setState(() {
errorText =
translate("Invalid folder name");
});
return;
}
currentFileController.createDir(PathUtil.join(
currentDir.path,
name.value.text,
currentOptions.isWindows));
close();
}
})
]);
});
} else if (v == "hidden") {
currentFileController.toggleShowHidden();
}
@@ -497,7 +515,15 @@ class _FileManagerViewState extends State<FileManagerView> {
child: Text(translate("Properties")),
value: "properties",
enabled: false,
)
),
if (!entries[index].isDrive &&
versionCmp(gFFI.ffiModel.pi.version,
"1.3.0") >=
0)
PopupMenuItem(
child: Text(translate("Rename")),
value: "rename",
)
];
},
onSelected: (v) {
@@ -509,6 +535,9 @@ class _FileManagerViewState extends State<FileManagerView> {
_selectedItems.clear();
widget.selectMode.toggle(isLocal);
setState(() {});
} else if (v == "rename") {
controller.renameAction(
entries[index], isLocal);
}
}),
onTap: () {

View File

@@ -34,7 +34,7 @@ class RemotePage extends StatefulWidget {
final bool? isSharedPassword;
@override
State<RemotePage> createState() => _RemotePageState();
State<RemotePage> createState() => _RemotePageState(id);
}
class _RemotePageState extends State<RemotePage> {
@@ -55,6 +55,15 @@ class _RemotePageState extends State<RemotePage> {
InputModel get inputModel => gFFI.inputModel;
SessionID get sessionId => gFFI.sessionId;
final TextEditingController _textController =
TextEditingController(text: initText);
_RemotePageState(String id) {
initSharedStates(id);
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
gFFI.dialogManager.loadMobileActionsOverlayVisible();
}
@override
void initState() {
super.initState();
@@ -77,12 +86,9 @@ class _RemotePageState extends State<RemotePage> {
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
keyboardSubscription =
keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
initSharedStates(widget.id);
gFFI.chatModel
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
_blockableOverlayState.applyFfi(gFFI);
gFFI.dialogManager.loadMobileActionsOverlayVisible();
}
@override
@@ -145,37 +151,59 @@ class _RemotePageState extends State<RemotePage> {
setState(() {});
}
// handle mobile virtual keyboard
void handleSoftKeyboardInput(String newValue) {
void _handleIOSSoftKeyboardInput(String newValue) {
var oldValue = _value;
_value = newValue;
if (isIOS) {
var i = newValue.length - 1;
for (; i >= 0 && newValue[i] != '\1'; --i) {}
var j = oldValue.length - 1;
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
if (i < j) j = i;
newValue = newValue.substring(j + 1);
oldValue = oldValue.substring(j + 1);
var common = 0;
for (;
common < oldValue.length &&
common < newValue.length &&
newValue[common] == oldValue[common];
++common) {}
for (i = 0; i < oldValue.length - common; ++i) {
inputModel.inputKey('VK_BACK');
}
if (newValue.length > common) {
var s = newValue.substring(common);
if (s.length > 1) {
bind.sessionInputString(sessionId: sessionId, value: s);
} else {
inputChar(s);
}
}
return;
var i = newValue.length - 1;
for (; i >= 0 && newValue[i] != '\1'; --i) {}
var j = oldValue.length - 1;
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
if (i < j) j = i;
var subNewValue = newValue.substring(j + 1);
var subOldValue = oldValue.substring(j + 1);
// get common prefix of subNewValue and subOldValue
var common = 0;
for (;
common < subOldValue.length &&
common < subNewValue.length &&
subNewValue[common] == subOldValue[common];
++common) {}
// get newStr from subNewValue
var newStr = "";
if (subNewValue.length > common) {
newStr = subNewValue.substring(common);
}
// Set the value to the old value and early return if is still composing. (1 && 2)
// 1. The composing range is valid
// 2. The new string is shorter than the composing range.
if (_textController.value.isComposingRangeValid) {
final composingLength = _textController.value.composing.end -
_textController.value.composing.start;
if (composingLength > newStr.length) {
_value = oldValue;
return;
}
}
// Delete the different part in the old value.
for (i = 0; i < subOldValue.length - common; ++i) {
inputModel.inputKey('VK_BACK');
}
// Input the new string.
if (newStr.length > 1) {
bind.sessionInputString(sessionId: sessionId, value: newStr);
} else {
inputChar(newStr);
}
}
void _handleNonIOSSoftKeyboardInput(String newValue) {
var oldValue = _value;
_value = newValue;
if (oldValue.isNotEmpty &&
newValue.isNotEmpty &&
oldValue[0] == '\1' &&
@@ -214,6 +242,15 @@ class _RemotePageState extends State<RemotePage> {
}
}
// handle mobile virtual keyboard
void handleSoftKeyboardInput(String newValue) {
if (isIOS) {
_handleIOSSoftKeyboardInput(newValue);
} else {
_handleNonIOSSoftKeyboardInput(newValue);
}
}
void inputChar(String char) {
if (char == '\n') {
char = 'VK_RETURN';
@@ -227,6 +264,7 @@ class _RemotePageState extends State<RemotePage> {
gFFI.invokeMethod("enable_soft_keyboard", true);
// destroy first, so that our _value trick can work
_value = initText;
_textController.text = _value;
setState(() => _showEdit = false);
_timer?.cancel();
_timer = Timer(kMobileDelaySoftKeyboard, () {
@@ -242,12 +280,10 @@ class _RemotePageState extends State<RemotePage> {
});
}
bool get keyboard => gFFI.ffiModel.permissions['keyboard'] != false;
Widget _bottomWidget() => _showGestureHelp
? getGestureHelp()
: (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
? getBottomAppBar(keyboard)
? getBottomAppBar()
: Offstage());
@override
@@ -314,7 +350,7 @@ class _RemotePageState extends State<RemotePage> {
return Container(
color: kColorCanvas,
child: isWebDesktop
? getBodyForDesktopWithListener(keyboard)
? getBodyForDesktopWithListener()
: SafeArea(
child:
OrientationBuilder(builder: (ctx, orientation) {
@@ -346,9 +382,9 @@ class _RemotePageState extends State<RemotePage> {
}
Widget getRawPointerAndKeyBody(Widget child) {
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
final ffiModel = Provider.of<FfiModel>(context);
return RawPointerMouseRegion(
cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer,
cursor: ffiModel.keyboard ? SystemMouseCursors.none : MouseCursor.defer,
inputModel: inputModel,
// Disable RawKeyFocusScope before the connecting is established.
// The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
@@ -361,7 +397,8 @@ class _RemotePageState extends State<RemotePage> {
);
}
Widget getBottomAppBar(bool keyboard) {
Widget getBottomAppBar() {
final ffiModel = Provider.of<FfiModel>(context);
return BottomAppBar(
elevation: 10,
color: MyTheme.accent,
@@ -387,7 +424,7 @@ class _RemotePageState extends State<RemotePage> {
},
)
] +
(isWebDesktop
(isWebDesktop || ffiModel.viewOnly || !ffiModel.keyboard
? []
: gFFI.ffiModel.isPeerAndroid
? [
@@ -491,7 +528,7 @@ class _RemotePageState extends State<RemotePage> {
autofocus: true,
focusNode: _mobileFocusNode,
maxLines: null,
initialValue: _value,
controller: _textController,
// trick way to make backspace work always
keyboardType: TextInputType.multiline,
onChanged: handleSoftKeyboardInput,
@@ -499,48 +536,84 @@ class _RemotePageState extends State<RemotePage> {
),
];
if (showCursorPaint) {
paints.add(CursorPaint());
paints.add(CursorPaint(widget.id));
}
return paints;
}()));
}
Widget getBodyForDesktopWithListener(bool keyboard) {
Widget getBodyForDesktopWithListener() {
final ffiModel = Provider.of<FfiModel>(context);
var paints = <Widget>[ImagePaint()];
if (showCursorPaint) {
final cursor = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: 'show-remote-cursor');
if (keyboard || cursor) {
paints.add(CursorPaint());
if (ffiModel.keyboard || cursor) {
paints.add(CursorPaint(widget.id));
}
}
return Container(
color: MyTheme.canvasColor, child: Stack(children: paints));
}
List<TTextMenu> _getMobileActionMenus() {
if (gFFI.ffiModel.pi.platform != kPeerPlatformAndroid ||
!gFFI.ffiModel.keyboard) {
return [];
}
final enabled = versionCmp(gFFI.ffiModel.pi.version, '1.2.7') >= 0;
if (!enabled) return [];
return [
TTextMenu(
child: Text(translate('Back')),
onPressed: () => gFFI.inputModel.onMobileBack(),
),
TTextMenu(
child: Text(translate('Home')),
onPressed: () => gFFI.inputModel.onMobileHome(),
),
TTextMenu(
child: Text(translate('Apps')),
onPressed: () => gFFI.inputModel.onMobileApps(),
),
TTextMenu(
child: Text(translate('Volume up')),
onPressed: () => gFFI.inputModel.onMobileVolumeUp(),
),
TTextMenu(
child: Text(translate('Volume down')),
onPressed: () => gFFI.inputModel.onMobileVolumeDown(),
),
TTextMenu(
child: Text(translate('Power')),
onPressed: () => gFFI.inputModel.onMobilePower(),
),
];
}
void showActions(String id) async {
final size = MediaQuery.of(context).size;
final x = 120.0;
final y = size.height;
final mobileActionMenus = _getMobileActionMenus();
final menus = toolbarControls(context, id, gFFI);
getChild(TTextMenu menu) {
if (menu.trailingIcon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
menu.child,
menu.trailingIcon!,
]);
} else {
return menu.child;
}
}
final more = menus
.asMap()
.entries
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
.toList();
final List<PopupMenuEntry<int>> more = [
...mobileActionMenus
.asMap()
.entries
.map((e) =>
PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
.toList(),
if (mobileActionMenus.isNotEmpty) PopupMenuDivider(),
...menus
.asMap()
.entries
.map((e) => PopupMenuItem<int>(
child: e.value.getChild(),
value: e.key + mobileActionMenus.length))
.toList(),
];
() async {
var index = await showMenu(
context: context,
@@ -548,8 +621,12 @@ class _RemotePageState extends State<RemotePage> {
items: more,
elevation: 8,
);
if (index != null && index < menus.length) {
menus[index].onPressed.call();
if (index != null) {
if (index < mobileActionMenus.length) {
mobileActionMenus[index].onPressed.call();
} else if (index < mobileActionMenus.length + more.length) {
menus[index - mobileActionMenus.length].onPressed.call();
}
}
}();
}
@@ -569,9 +646,11 @@ class _RemotePageState extends State<RemotePage> {
child: Text(translate(label), style: labelStyle),
trailingIcon: Transform.scale(
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
child: IconButton(
onPressed: onPressed,
icon: icon,
child: IgnorePointer(
child: IconButton(
onPressed: null,
icon: icon,
),
),
),
onPressed: onPressed,
@@ -602,23 +681,11 @@ class _RemotePageState extends State<RemotePage> {
),
onPressVoiceCall),
];
getChild(TTextMenu menu) {
if (menu.trailingIcon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
menu.child,
menu.trailingIcon!,
]);
} else {
return menu.child;
}
}
final menuItems = menus
.asMap()
.entries
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
.map((e) => PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
.toList();
Future.delayed(Duration.zero, () async {
final size = MediaQuery.of(context).size;
@@ -712,11 +779,6 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
onPressed: onPressed);
}
@override
void initState() {
super.initState();
}
_updateRect() {
RenderObject? renderObject = _key.currentContext?.findRenderObject();
if (renderObject == null) {
@@ -885,26 +947,52 @@ class ImagePaint extends StatelessWidget {
}
class CursorPaint extends StatelessWidget {
late final String id;
CursorPaint(this.id);
@override
Widget build(BuildContext context) {
final m = Provider.of<CursorModel>(context);
final c = Provider.of<CanvasModel>(context);
final ffiModel = Provider.of<FfiModel>(context);
final adjust = gFFI.cursorModel.adjustForKeyboard();
var s = c.scale;
final s = c.scale;
double hotx = m.hotx;
double hoty = m.hoty;
if (m.image == null) {
var image = m.image;
if (image == null) {
if (preDefaultCursor.image != null) {
image = preDefaultCursor.image;
hotx = preDefaultCursor.image!.width / 2;
hoty = preDefaultCursor.image!.height / 2;
}
}
if (preForbiddenCursor.image != null &&
!ffiModel.viewOnly &&
!ffiModel.keyboard &&
!ShowRemoteCursorState.find(id).value) {
image = preForbiddenCursor.image;
hotx = preForbiddenCursor.image!.width / 2;
hoty = preForbiddenCursor.image!.height / 2;
}
if (image == null) {
return Offstage();
}
final minSize = 12.0;
double mins =
minSize / (image.width > image.height ? image.width : image.height);
double factor = 1.0;
if (s < mins) {
factor = s / mins;
}
final s2 = s < mins ? mins : s;
return CustomPaint(
painter: ImagePainter(
image: m.image ?? preDefaultCursor.image,
x: m.x * s - hotx + c.x,
y: m.y * s - hoty + c.y - adjust,
scale: 1),
image: image,
x: (m.x - hotx) * factor + c.x / s2,
y: (m.y - hoty) * factor + (c.y - adjust) / s2,
scale: s2),
);
}
}
@@ -982,22 +1070,40 @@ void showOptions(
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
final radios = [
for (var e in viewStyleRadios)
Obx(() => getRadio<String>(e.child, e.value, viewStyle.value, (v) {
e.onChanged?.call(v);
if (v != null) viewStyle.value = v;
})),
Obx(() => getRadio<String>(
e.child,
e.value,
viewStyle.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) viewStyle.value = v;
}
: null)),
const Divider(color: MyTheme.border),
for (var e in imageQualityRadios)
Obx(() => getRadio<String>(e.child, e.value, imageQuality.value, (v) {
e.onChanged?.call(v);
if (v != null) imageQuality.value = v;
})),
Obx(() => getRadio<String>(
e.child,
e.value,
imageQuality.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) imageQuality.value = v;
}
: null)),
const Divider(color: MyTheme.border),
for (var e in codecRadios)
Obx(() => getRadio<String>(e.child, e.value, codec.value, (v) {
e.onChanged?.call(v);
if (v != null) codec.value = v;
})),
Obx(() => getRadio<String>(
e.child,
e.value,
codec.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) codec.value = v;
}
: null)),
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
];
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
@@ -1008,10 +1114,12 @@ void showOptions(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
value: rxCursorToggleValues[e.key].value,
onChanged: (v) {
e.value.onChanged?.call(v);
if (v != null) rxCursorToggleValues[e.key].value = v;
},
onChanged: e.value.onChanged != null
? (v) {
e.value.onChanged?.call(v);
if (v != null) rxCursorToggleValues[e.key].value = v;
}
: null,
title: e.value.child)))
.toList();
@@ -1023,10 +1131,12 @@ void showOptions(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
value: rxToggleValues[e.key].value,
onChanged: (v) {
e.value.onChanged?.call(v);
if (v != null) rxToggleValues[e.key].value = v;
},
onChanged: e.value.onChanged != null
? (v) {
e.value.onChanged?.call(v);
if (v != null) rxToggleValues[e.key].value = v;
}
: null,
title: e.value.child)))
.toList();
final toggles = [
@@ -1046,14 +1156,110 @@ void showOptions(
);
}
var popupDialogMenus = List<Widget>.empty(growable: true);
final resolution = getResolutionMenu(gFFI, id);
if (resolution != null) {
popupDialogMenus.add(ListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
title: resolution.child,
onTap: () {
close();
resolution.onPressed();
},
));
}
final virtualDisplayMenu = getVirtualDisplayMenu(gFFI, id);
if (virtualDisplayMenu != null) {
popupDialogMenus.add(ListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
title: virtualDisplayMenu.child,
onTap: () {
close();
virtualDisplayMenu.onPressed();
},
));
}
if (popupDialogMenus.isNotEmpty) {
popupDialogMenus.add(const Divider(color: MyTheme.border));
}
return CustomAlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: displays + radios + toggles + [privacyModeWidget]),
children: displays +
radios +
popupDialogMenus +
toggles +
[privacyModeWidget]),
);
}, clickMaskDismiss: true, backDismiss: true);
}
TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
if (!showVirtualDisplayMenu(ffi)) {
return null;
}
return TTextMenu(
child: Text(translate("Virtual display")),
onPressed: () {
ffi.dialogManager.show((setState, close, context) {
final children = getVirtualDisplayMenuChildren(ffi, id, close);
return CustomAlertDialog(
title: Text(translate('Virtual display')),
content: Column(
mainAxisSize: MainAxisSize.min,
children: children,
),
);
}, clickMaskDismiss: true, backDismiss: true);
},
);
}
TTextMenu? getResolutionMenu(FFI ffi, String id) {
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final resolutions = pi.resolutions;
final display = pi.tryGetDisplayIfNotAllDisplay(display: pi.currentDisplay);
final visible =
ffiModel.keyboard && (resolutions.length > 1) && display != null;
if (!visible) return null;
return TTextMenu(
child: Text(translate("Resolution")),
onPressed: () {
ffi.dialogManager.show((setState, close, context) {
final children = resolutions
.map((e) => getRadio<String>(
Text('${e.width}x${e.height}'),
'${e.width}x${e.height}',
'${display.width}x${display.height}',
(value) {
close();
bind.sessionChangeResolution(
sessionId: ffi.sessionId,
display: pi.currentDisplay,
width: e.width,
height: e.height,
);
},
))
.toList();
return CustomAlertDialog(
title: Text(translate('Resolution')),
content: Column(
mainAxisSize: MainAxisSize.min,
children: children,
),
);
}, clickMaskDismiss: true, backDismiss: true);
},
);
}
void sendPrompt(bool isMac, String key) {
final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl;
if (isMac) {

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:get/get.dart';
@@ -22,7 +23,22 @@ class ServerPage extends StatefulWidget implements PageShape {
final icon = const Icon(Icons.mobile_screen_share);
@override
final appBarActions = [
final appBarActions = (!bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
? [_DropDownAction()]
: [];
ServerPage({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ServerPageState();
}
class _DropDownAction extends StatelessWidget {
_DropDownAction();
// should only have one action
final actions = [
PopupMenuButton<String>(
tooltip: "",
icon: const Icon(Icons.more_vert),
@@ -101,18 +117,27 @@ class ServerPage extends StatefulWidget implements PageShape {
),
];
},
onSelected: (value) {
onSelected: (value) async {
if (value == "changeID") {
changeIdDialog();
} else if (value == "setPermanentPassword") {
setPermanentPasswordDialog(gFFI.dialogManager);
setPasswordDialog();
} else if (value == "setTemporaryPasswordLength") {
setTemporaryPasswordLengthDialog(gFFI.dialogManager);
} else if (value == kUsePermanentPassword ||
value == kUseTemporaryPassword ||
value == kUseBothPasswords) {
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
gFFI.serverModel.updatePasswordModel();
callback() {
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
gFFI.serverModel.updatePasswordModel();
}
if (value == kUsePermanentPassword &&
(await bind.mainGetPermanentPassword()).isEmpty) {
setPasswordDialog(notEmptyCallback: callback);
} else {
callback();
}
} else if (value.startsWith("AcceptSessionsVia")) {
value = value.substring("AcceptSessionsVia".length);
if (value == "Password") {
@@ -126,10 +151,10 @@ class ServerPage extends StatefulWidget implements PageShape {
})
];
ServerPage({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ServerPageState();
Widget build(BuildContext context) {
return actions[0];
}
}
class _ServerPageState extends State<ServerPage> {
@@ -162,7 +187,7 @@ class _ServerPageState extends State<ServerPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
buildPresetPasswordWarning(),
buildPresetPasswordWarningMobile(),
gFFI.serverModel.isStart
? ServerInfo()
: ServiceNotRunningNotification(),

View File

@@ -1,8 +1,10 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:settings_ui/settings_ui.dart';
@@ -83,18 +85,17 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
var _fingerprint = "";
var _buildDate = "";
var _autoDisconnectTimeout = "";
var _hideServer = false;
var _hideProxy = false;
var _hideNetwork = false;
var _enableTrustedDevices = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_SettingsState() {
_enableAbr = option2bool(
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
_onlyWhiteList = (bind.mainGetOptionSync(key: kOptionWhitelist)) !=
defaultOptionWhitelist;
_onlyWhiteList = whitelistNotEmpty();
_enableDirectIPAccess = option2bool(
kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
_enableRecordSession = option2bool(kOptionEnableRecordSession,
@@ -109,8 +110,20 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
_autoDisconnectTimeout =
bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
_hideServer =
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
_hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
_hideNetwork =
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y';
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
}
() async {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) async {
var update = false;
if (_hasIgnoreBattery) {
@@ -169,7 +182,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
if (update) {
setState(() {});
}
}();
});
}
@override
@@ -233,18 +246,76 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
],
));
final List<AbstractSettingsTile> enhancementsTiles = [];
final List<AbstractSettingsTile> shareScreenTiles = [
final enable2fa = bind.mainHasValid2FaSync();
final List<AbstractSettingsTile> tfaTiles = [
SettingsTile.switchTile(
title: Text(translate('enable-2fa-title')),
initialValue: bind.mainHasValid2FaSync(),
onToggle: (_) async {
initialValue: enable2fa,
onToggle: (v) async {
update() async {
setState(() {});
}
change2fa(callback: update);
if (v == false) {
CommonConfirmDialog(
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
change2fa(callback: update);
});
} else {
change2fa(callback: update);
}
},
),
if (enable2fa)
SettingsTile.switchTile(
title: Text(translate('Telegram bot')),
initialValue: bind.mainHasValidBotSync(),
onToggle: (v) async {
update() async {
setState(() {});
}
if (v == false) {
CommonConfirmDialog(
gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
changeBot(callback: update);
});
} else {
changeBot(callback: update);
}
},
),
if (enable2fa)
SettingsTile.switchTile(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate('Enable trusted devices')),
Text('* ${translate('enable-trusted-devices-tip')}',
style: Theme.of(context).textTheme.bodySmall),
],
),
initialValue: _enableTrustedDevices,
onToggle: isOptionFixed(kOptionEnableTrustedDevices)
? null
: (v) async {
mainSetBoolOption(kOptionEnableTrustedDevices, v);
setState(() {
_enableTrustedDevices = v;
});
},
),
if (enable2fa && _enableTrustedDevices)
SettingsTile(
title: Text(translate('Manage trusted devices')),
trailing: Icon(Icons.arrow_forward_ios),
onPressed: (context) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return _ManageTrustedDevices();
}));
})
];
final List<AbstractSettingsTile> shareScreenTiles = [
SettingsTile.switchTile(
title: Text(translate('Deny LAN discovery')),
initialValue: _denyLANDiscovery,
@@ -273,9 +344,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
initialValue: _onlyWhiteList,
onToggle: (_) async {
update() async {
final onlyWhiteList =
(await bind.mainGetOption(key: kOptionWhitelist)) !=
defaultOptionWhitelist;
final onlyWhiteList = whitelistNotEmpty();
if (onlyWhiteList != _onlyWhiteList) {
setState(() {
_onlyWhiteList = onlyWhiteList;
@@ -530,6 +599,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
));
final disabledSettings = bind.isDisableSettings();
final hideSecuritySettings =
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) == 'Y';
final settings = SettingsList(
sections: [
customClientSection,
@@ -553,13 +624,20 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
],
),
SettingsSection(title: Text(translate("Settings")), tiles: [
if (!disabledSettings)
if (!disabledSettings && !_hideNetwork && !_hideServer)
SettingsTile(
title: Text(translate('ID/Relay Server')),
leading: Icon(Icons.cloud),
onPressed: (context) {
showServerSettings(gFFI.dialogManager);
}),
if (!isIOS && !_hideNetwork && !_hideProxy)
SettingsTile(
title: Text(translate('Socks5/Http(s) Proxy')),
leading: Icon(Icons.network_ping),
onPressed: (context) {
changeSocks5Proxy();
}),
SettingsTile(
title: Text(translate('Language')),
leading: Icon(Icons.translate),
@@ -625,13 +703,24 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
),
],
),
if (isAndroid && !disabledSettings && !outgoingOnly)
if (isAndroid &&
!disabledSettings &&
!outgoingOnly &&
!hideSecuritySettings)
SettingsSection(title: Text('2FA'), tiles: tfaTiles),
if (isAndroid &&
!disabledSettings &&
!outgoingOnly &&
!hideSecuritySettings)
SettingsSection(
title: Text(translate("Share Screen")),
tiles: shareScreenTiles,
),
if (!bind.isIncomingOnly()) defaultDisplaySection(),
if (isAndroid && !disabledSettings && !outgoingOnly)
if (isAndroid &&
!disabledSettings &&
!outgoingOnly &&
!hideSecuritySettings)
SettingsSection(
title: Text(translate("Enhancements")),
tiles: enhancementsTiles,
@@ -786,7 +875,7 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
void showAbout(OverlayDialogManager dialogManager) {
dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text('${translate('About')} RustDesk'),
title: Text(translate('About RustDesk')),
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
Text('Version: $version'),
InkWell(
@@ -940,6 +1029,51 @@ class __DisplayPageState extends State<_DisplayPage> {
}
}
class _ManageTrustedDevices extends StatefulWidget {
const _ManageTrustedDevices();
@override
State<_ManageTrustedDevices> createState() => __ManageTrustedDevicesState();
}
class __ManageTrustedDevicesState extends State<_ManageTrustedDevices> {
RxList<TrustedDevice> trustedDevices = RxList.empty(growable: true);
RxList<Uint8List> selectedDevices = RxList.empty();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(translate('Manage trusted devices')),
centerTitle: true,
actions: [
Obx(() => IconButton(
icon: Icon(Icons.delete, color: Colors.white),
onPressed: selectedDevices.isEmpty
? null
: () {
confrimDeleteTrustedDevicesDialog(
trustedDevices, selectedDevices);
}))
],
),
body: FutureBuilder(
future: TrustedDevice.get(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
final devices = snapshot.data as List<TrustedDevice>;
trustedDevices = devices.obs;
return trustedDevicesTable(trustedDevices, selectedDevices);
}),
);
}
}
class _RadioEntry {
final String label;
final String value;

View File

@@ -41,18 +41,16 @@ class GestureHelp extends StatefulWidget {
final OnTouchModeChange onTouchModeChange;
@override
State<StatefulWidget> createState() => _GestureHelpState();
State<StatefulWidget> createState() => _GestureHelpState(touchMode);
}
class _GestureHelpState extends State<GestureHelp> {
var _selectedIndex;
var _touchMode;
late int _selectedIndex;
late bool _touchMode;
@override
void initState() {
_touchMode = widget.touchMode;
_GestureHelpState(bool touchMode) {
_touchMode = touchMode;
_selectedIndex = _touchMode ? 1 : 0;
super.initState();
}
@override

View File

@@ -17,17 +17,17 @@ import '../common.dart';
final syncAbOption = 'sync-ab-with-recent-sessions';
bool shouldSyncAb() {
return bind.mainGetLocalOption(key: syncAbOption).isNotEmpty;
return bind.mainGetLocalOption(key: syncAbOption) == 'Y';
}
final sortAbTagsOption = 'sync-ab-tags';
bool shouldSortTags() {
return bind.mainGetLocalOption(key: sortAbTagsOption).isNotEmpty;
return bind.mainGetLocalOption(key: sortAbTagsOption) == 'Y';
}
final filterAbTagOption = 'filter-ab-by-intersection';
bool filterAbTagByIntersection() {
return bind.mainGetLocalOption(key: filterAbTagOption).isNotEmpty;
return bind.mainGetLocalOption(key: filterAbTagOption) == 'Y';
}
const _personalAddressBookName = "My address book";
@@ -111,9 +111,10 @@ class AbModel {
Future<void> _pullAb(
{required ForcePullAb? force, required bool quiet}) async {
if (bind.isDisableAb()) return;
debugPrint("pullAb, force: $force, quiet: $quiet");
if (!gFFI.userModel.isLogin) return;
if (gFFI.userModel.networkError.isNotEmpty) return;
if (force == null && listInitialized && current.initialized) return;
debugPrint("pullAb, force: $force, quiet: $quiet");
if (!listInitialized || force == ForcePullAb.listAndCurrent) {
try {
// Read personal guid every time to avoid upgrading the server without closing the main window
@@ -815,8 +816,6 @@ abstract class BaseAb {
}
class LegacyAb extends BaseAb {
final sortTags = shouldSortTags().obs;
final filterByIntersection = filterAbTagByIntersection().obs;
bool get emtpy => peers.isEmpty && tags.isEmpty;
// licensedDevices is obtained from personal ab, shared ab restrict it in server
var licensedDevices = 0;
@@ -1209,8 +1208,6 @@ class LegacyAb extends BaseAb {
class Ab extends BaseAb {
AbProfile profile;
late final bool personal;
final sortTags = shouldSortTags().obs;
final filterByIntersection = filterAbTagByIntersection().obs;
bool get emtpy => peers.isEmpty && tags.isEmpty;
Ab(this.profile, this.personal);

View File

@@ -33,6 +33,8 @@ class CmFileModel {
_onFileRemove(evt['remove']);
} else if (evt['create_dir'] != null) {
_onDirCreate(evt['create_dir']);
} else if (evt['rename'] != null) {
_onRename(evt['rename']);
}
}
@@ -59,8 +61,6 @@ class CmFileModel {
_dealOneJob(dynamic l, bool calcSpeed) {
final data = TransferJobSerdeData.fromJson(l);
Client? client =
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
var jobTable = _jobTables[data.connId];
if (jobTable == null) {
debugPrint("jobTable should not be null");
@@ -70,12 +70,7 @@ class CmFileModel {
if (job == null) {
job = CmFileLog();
jobTable.add(job);
final currentSelectedTab =
gFFI.serverModel.tabController.state.value.selectedTabInfo;
if (!(gFFI.chatModel.isShowCMSidePage &&
currentSelectedTab.key == data.connId.toString())) {
client?.unreadChatMessageCount.value += 1;
}
_addUnread(data.connId);
}
job.id = data.id;
job.action =
@@ -167,8 +162,6 @@ class CmFileModel {
try {
dynamic d = jsonDecode(log);
FileActionLog data = FileActionLog.fromJson(d);
Client? client =
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
var jobTable = _jobTables[data.connId];
if (jobTable == null) {
debugPrint("jobTable should not be null");
@@ -179,17 +172,45 @@ class CmFileModel {
..fileName = data.path
..action = CmFileAction.createDir
..state = JobState.done);
final currentSelectedTab =
gFFI.serverModel.tabController.state.value.selectedTabInfo;
if (!(gFFI.chatModel.isShowCMSidePage &&
currentSelectedTab.key == data.connId.toString())) {
client?.unreadChatMessageCount.value += 1;
}
_addUnread(data.connId);
jobTable.refresh();
} catch (e) {
debugPrint('$e');
}
}
_onRename(dynamic log) {
try {
dynamic d = jsonDecode(log);
FileRenamenLog data = FileRenamenLog.fromJson(d);
var jobTable = _jobTables[data.connId];
if (jobTable == null) {
debugPrint("jobTable should not be null");
return;
}
final fileName = '${data.path} -> ${data.newName}';
jobTable.add(CmFileLog()
..id = 0
..fileName = fileName
..action = CmFileAction.rename
..state = JobState.done);
_addUnread(data.connId);
jobTable.refresh();
} catch (e) {
debugPrint('$e');
}
}
_addUnread(int connId) {
Client? client =
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == connId);
final currentSelectedTab =
gFFI.serverModel.tabController.state.value.selectedTabInfo;
if (!(gFFI.chatModel.isShowCMSidePage &&
currentSelectedTab.key == connId.toString())) {
client?.unreadChatMessageCount.value += 1;
}
}
}
enum CmFileAction {
@@ -198,6 +219,7 @@ enum CmFileAction {
localToRemote,
remove,
createDir,
rename,
}
class CmFileLog {
@@ -285,3 +307,22 @@ class FileActionLog {
dir: d['dir'] ?? false,
);
}
class FileRenamenLog {
int connId = 0;
String path = '';
String newName = '';
FileRenamenLog({
required this.connId,
required this.path,
required this.newName,
});
FileRenamenLog.fromJson(dynamic d)
: this(
connId: d['connId'] ?? 0,
path: d['path'] ?? '',
newName: d['newName'] ?? '',
);
}

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/utils/event_loop.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
@@ -642,6 +643,77 @@ class FileController {
path: path,
isRemote: !isLocal);
}
Future<void> renameAction(Entry item, bool isLocal) async {
final textEditingController = TextEditingController(text: item.name);
String? errorText;
dialogManager?.show((setState, close, context) {
textEditingController.addListener(() {
if (errorText != null) {
setState(() {
errorText = null;
});
}
});
submit() async {
final newName = textEditingController.text;
if (newName.isEmpty || newName == item.name) {
close();
return;
}
if (directory.value.entries.any((e) => e.name == newName)) {
setState(() {
errorText = translate("Already exists");
});
return;
}
if (!PathUtil.validName(newName, options.value.isWindows)) {
setState(() {
if (item.isDirectory) {
errorText = translate("Invalid folder name");
} else {
errorText = translate("Invalid file name");
}
});
return;
}
await bind.sessionRenameFile(
sessionId: sessionId,
actId: JobController.jobID.next(),
path: item.path,
newName: newName,
isRemote: !isLocal);
close();
}
return CustomAlertDialog(
content: Column(
children: [
DialogTextField(
title: '${translate('Rename')} ${item.name}',
controller: textEditingController,
errorText: errorText,
),
],
),
actions: [
dialogButton(
"Cancel",
icon: Icon(Icons.close_rounded),
onPressed: close,
isOutline: true,
),
dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed: submit,
),
],
onSubmit: submit,
onCancel: close,
);
});
}
}
class JobController {
@@ -1083,6 +1155,13 @@ class PathUtil {
final pathUtil = isWindows ? windowsContext : posixContext;
return pathUtil.dirname(path);
}
static bool validName(String name, bool isWindows) {
final unixFileNamePattern = RegExp(r'^[^/\0]+$');
final windowsFileNamePattern = RegExp(r'^[^<>:"/\\|?*]+$');
final reg = isWindows ? windowsFileNamePattern : unixFileNamePattern;
return reg.hasMatch(name);
}
}
class DirectoryOptions {

View File

@@ -28,6 +28,7 @@ class GroupModel {
Future<void> pull({force = true, quiet = false}) async {
if (bind.isDisableGroupPanel()) return;
if (!gFFI.userModel.isLogin || groupLoading.value) return;
if (gFFI.userModel.networkError.isNotEmpty) return;
if (!force && initialized) return;
if (!quiet) {
groupLoading.value = true;

View File

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

View File

@@ -192,10 +192,10 @@ class FfiModel with ChangeNotifier {
_permissions[k] = v == 'true';
});
// Only inited at remote page
if (desktopType == DesktopType.remote) {
if (parent.target?.connType == ConnType.defaultConn) {
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
}
debugPrint('$_permissions');
debugPrint('updatePermission: $_permissions');
notifyListeners();
}
@@ -384,7 +384,7 @@ class FfiModel with ChangeNotifier {
} else if (name == 'use_texture_render') {
_handleUseTextureRender(evt, sessionId, peerId);
} else {
debugPrint('Unknown event name: $name');
debugPrint('Event is not handled in the fixed branch: $name');
}
};
}
@@ -438,20 +438,6 @@ class FfiModel with ChangeNotifier {
_handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) {
final running = evt['running'] == 'true';
parent.target?.elevationModel.onPortableServiceRunning(running);
if (running) {
if (pi.primaryDisplay != kInvalidDisplayIndex) {
if (pi.currentDisplay != pi.primaryDisplay) {
// Notify to switch display
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
'elevated_switch_display_msg', '', parent.target!.dialogManager);
bind.sessionSwitchDisplay(
isDesktop: isDesktop,
sessionId: sessionId,
value: Int32List.fromList([pi.primaryDisplay]),
);
}
}
}
}
handleAliasChanged(Map<String, dynamic> evt) {
@@ -737,6 +723,8 @@ class FfiModel with ChangeNotifier {
/// Handle the peer info event based on [evt].
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
parent.target?.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
// This call is to ensuer the keyboard mode is updated depending on the peer version.
parent.target?.inputModel.updateKeyboardMode();
@@ -1015,14 +1003,15 @@ class FfiModel with ChangeNotifier {
// Notify to switch display
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
'display_is_plugged_out_msg', '', parent.target!.dialogManager);
final newDisplay = pi.primaryDisplay == kInvalidDisplayIndex
? 0
: pi.primaryDisplay;
final displays = newDisplay;
final isPeerPrimaryDisplayValid =
pi.primaryDisplay == kInvalidDisplayIndex ||
pi.primaryDisplay >= pi.displays.length;
final newDisplay =
isPeerPrimaryDisplayValid ? 0 : pi.primaryDisplay;
bind.sessionSwitchDisplay(
isDesktop: isDesktop,
sessionId: sessionId,
value: Int32List.fromList([displays]),
value: Int32List.fromList([newDisplay]),
);
if (_pi.isSupportMultiUiSession) {
@@ -1187,26 +1176,28 @@ class ImageModel with ChangeNotifier {
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
onRgba(int display, Uint8List rgba) {
clearImage() => _image = null;
onRgba(int display, Uint8List rgba) async {
try {
await decodeAndUpdate(display, rgba);
} catch (e) {
debugPrint('onRgba error: $e');
}
platformFFI.nextRgba(sessionId, display);
}
decodeAndUpdate(int display, Uint8List rgba) async {
final pid = parent.target?.id;
final rect = parent.target?.ffiModel.pi.getDisplayRect(display);
img.decodeImageFromPixels(
rgba,
rect?.width.toInt() ?? 0,
rect?.height.toInt() ?? 0,
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
onPixelsCopied: () {
// Unlock the rgba memory from rust codes.
platformFFI.nextRgba(sessionId, display);
}).then((image) {
if (parent.target?.id != pid) return;
try {
// my throw exception, because the listener maybe already dispose
update(image);
} catch (e) {
debugPrint('update image: $e');
}
});
final image = await img.decodeImageFromPixels(
rgba,
rect?.width.toInt() ?? 0,
rect?.height.toInt() ?? 0,
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
);
if (parent.target?.id != pid) return;
await update(image);
}
update(ui.Image? image) async {
@@ -1740,7 +1731,7 @@ class PredefinedCursor {
_image2 = img2.decodePng(base64Decode(png));
if (_image2 != null) {
// The png type of forbidden cursor image is `PngColorType.indexed`.
if (isWindows && id == kPreForbiddenCursorId) {
if (id == kPreForbiddenCursorId) {
_image2 = _image2!.convert(format: img2.Format.uint8, numChannels: 4);
}
@@ -2561,32 +2552,30 @@ class FFI {
}
} else if (message is EventToUI_Rgba) {
final display = message.field0;
if (imageModel.useTextureRender || ffiModel.pi.forceTextureRender) {
//debugPrint("EventToUI_Rgba display:$display");
textureModel.setTextureType(display: display, gpuTexture: false);
// 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();
await imageModel.onRgba(display, rgba);
} 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);
}
platformFFI.nextRgba(sessionId, display);
}
} else if (message is EventToUI_Texture) {
final display = message.field0;
debugPrint("EventToUI_Texture display:$display");
if (hasGpuTextureRender) {
textureModel.setTextureType(display: display, gpuTexture: true);
onEvent2UIRgba();
final gpuTexture = message.field1;
debugPrint(
"EventToUI_Texture display:$display, gpuTexture:$gpuTexture");
if (gpuTexture && !hasGpuTextureRender) {
debugPrint('the gpuTexture is not supported.');
return;
}
textureModel.setTextureType(display: display, gpuTexture: gpuTexture);
onEvent2UIRgba();
}
}();
});
@@ -2622,8 +2611,9 @@ class FFI {
remember: remember);
}
void send2FA(SessionID sessionId, String code) {
bind.sessionSend2Fa(sessionId: sessionId, code: code);
void send2FA(SessionID sessionId, String code, bool trustThisDevice) {
bind.sessionSend2Fa(
sessionId: sessionId, code: code, trustThisDevice: trustThisDevice);
}
/// Close the remote session.
@@ -2640,7 +2630,7 @@ class FFI {
canvasModel.scale,
ffiModel.pi.currentDisplay);
}
imageModel.update(null);
await imageModel.update(null);
cursorModel.clear();
ffiModel.clear();
canvasModel.clear();

View File

@@ -117,9 +117,13 @@ class PlatformFFI {
? DynamicLibrary.open('librustdesk.so')
: isWindows
? DynamicLibrary.open('librustdesk.dll')
: isMacOS
? DynamicLibrary.open("liblibrustdesk.dylib")
: DynamicLibrary.process();
:
// Use executable itself as the dynamic library for MacOS.
// Multiple dylib instances will cause some global instances to be invalid.
// eg. `lazy_static` objects in rust side, will be created more than once, which is not expected.
//
// isMacOS? DynamicLibrary.open("liblibrustdesk.dylib") :
DynamicLibrary.process();
debugPrint('initializing FFI $_appType');
try {
_session_get_rgba = dylib.lookupFunction<F3Dart, F3>("session_get_rgba");

View File

@@ -177,6 +177,11 @@ class ServerModel with ChangeNotifier {
await timerCallback();
});
}
// Initial keyboard status is off on mobile
if (isMobile) {
bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
}
}
/// 1. check android permission
@@ -191,7 +196,7 @@ class ServerModel with ChangeNotifier {
bind.mainSetOption(key: kOptionEnableAudio, value: "N");
} else {
final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
_audioOk = audioOption.isEmpty;
_audioOk = audioOption != 'N';
}
// file
@@ -201,7 +206,7 @@ class ServerModel with ChangeNotifier {
} else {
final fileOption =
await bind.mainGetOption(key: kOptionEnableFileTransfer);
_fileOk = fileOption.isEmpty;
_fileOk = fileOption != 'N';
}
notifyListeners();

View File

@@ -14,7 +14,7 @@ class StateGlobal {
bool _isMinimized = false;
final RxBool isMaximized = false.obs;
final RxBool _showTabBar = true.obs;
final RxDouble _resizeEdgeSize = RxDouble(windowEdgeSize);
final RxDouble _resizeEdgeSize = RxDouble(windowResizeEdgeSize);
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
final RxBool showRemoteToolBar = false.obs;
final svcStatus = SvcStatus.notReady.obs;
@@ -93,7 +93,7 @@ class StateGlobal {
? kFullScreenEdgeSize
: isMaximized.isTrue
? kMaximizeEdgeSize
: windowEdgeSize;
: windowResizeEdgeSize;
String getInputSource({bool force = false}) {
if (force || _inputSource.isEmpty) {

View File

@@ -17,13 +17,23 @@ bool refreshingUser = false;
class UserModel {
final RxString userName = ''.obs;
final RxBool isAdmin = false.obs;
final RxString networkError = ''.obs;
bool get isLogin => userName.isNotEmpty;
WeakReference<FFI> parent;
UserModel(this.parent);
UserModel(this.parent) {
userName.listen((p0) {
// When user name becomes empty, show login button
// When user name becomes non-empty:
// For _updateLocalUserInfo, network error will be set later
// For login success, should clear network error
networkError.value = '';
});
}
void refreshCurrentUser() async {
if (bind.isDisableAccount()) return;
networkError.value = '';
final token = bind.mainGetLocalOption(key: 'access_token');
if (token == '') {
await updateOtherModels();
@@ -38,12 +48,18 @@ class UserModel {
if (refreshingUser) return;
try {
refreshingUser = true;
final response = await http.post(Uri.parse('$url/api/currentUser'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token'
},
body: json.encode(body));
final http.Response response;
try {
response = await http.post(Uri.parse('$url/api/currentUser'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token'
},
body: json.encode(body));
} catch (e) {
networkError.value = e.toString();
rethrow;
}
refreshingUser = false;
final status = response.statusCode;
if (status == 401 || status == 400) {

View File

@@ -13,14 +13,12 @@ Future<ui.Image?> decodeImageFromPixels(
int? rowBytes,
int? targetWidth,
int? targetHeight,
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;
}
}
@@ -28,7 +26,6 @@ Future<ui.Image?> decodeImageFromPixels(
assert(allowUpscaling || targetHeight <= height);
if (!(allowUpscaling || targetHeight <= height)) {
print("not allow upscaling but targetHeight > height");
onPixelsCopied?.call();
return null;
}
}
@@ -36,9 +33,7 @@ Future<ui.Image?> decodeImageFromPixels(
final ui.ImmutableBuffer buffer;
try {
buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
onPixelsCopied?.call();
} catch (e) {
onPixelsCopied?.call();
return null;
}

View File

@@ -142,7 +142,10 @@ class RustdeskImpl {
}
Future<void> sessionSend2Fa(
{required UuidValue sessionId, required String code, dynamic hint}) {
{required UuidValue sessionId,
required String code,
required bool trustThisDevice,
dynamic hint}) {
return Future(() => js.context.callMethod('setByName', ['send_2fa', code]));
}
@@ -1412,7 +1415,7 @@ class RustdeskImpl {
return false;
}
bool mainHideDocker({dynamic hint}) {
bool mainHideDock({dynamic hint}) {
throw UnimplementedError();
}
@@ -1614,5 +1617,38 @@ class RustdeskImpl {
throw UnimplementedError();
}
bool mainHasValidBotSync({dynamic hint}) {
throw UnimplementedError();
}
Future<String> mainVerifyBot({required String token, dynamic hint}) {
throw UnimplementedError();
}
String mainGetUnlockPin({dynamic hint}) {
throw UnimplementedError();
}
String mainSetUnlockPin({required String pin, dynamic hint}) {
throw UnimplementedError();
}
bool sessionGetEnableTrustedDevices(
{required UuidValue sessionId, dynamic hint}) {
throw UnimplementedError();
}
Future<String> mainGetTrustedDevices({dynamic hint}) {
throw UnimplementedError();
}
Future<void> mainRemoveTrustedDevices({required String json, dynamic hint}) {
throw UnimplementedError();
}
Future<void> mainClearTrustedDevices({dynamic hint}) {
throw UnimplementedError();
}
void dispose() {}
}

View File

@@ -45,5 +45,7 @@
<string>Record the sound from microphone for the purpose of the remote desktop.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>LSUIElement</key>
<string>1</string>
</dict>
</plist>

2
flutter/ndk_x86.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
cargo ndk --platform 21 --target i686-linux-android build --release --features flutter

View File

@@ -335,7 +335,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: "60773827434eefe6d01eefa814dca9a032b970b3"
resolved-ref: "80b063b9d4e015f62e17f42a5aa0b3d20a365926"
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
source: git
version: "0.1.0"
@@ -377,7 +377,7 @@ packages:
path: "."
ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
url: "https://github.com/21pages/dynamic_layouts.git"
url: "https://github.com/rustdesk-org/dynamic_layouts.git"
source: git
version: "0.0.1+1"
external_path:
@@ -511,7 +511,7 @@ packages:
path: "."
ref: "38951317afe79d953ab25733667bd96e172a80d3"
resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3"
url: "https://github.com/21pages/flutter_gpu_texture_renderer"
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
source: git
version: "0.0.1"
flutter_improved_scrolling:

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.6+44
version: 1.3.0+46
environment:
sdk: '^3.1.0'
@@ -79,7 +79,7 @@ dependencies:
git:
url: https://github.com/rustdesk-org/flutter_improved_scrolling
uni_links: ^0.5.1
uni_links_desktop: ^0.1.7
uni_links_desktop: ^0.1.6 # use 0.1.6 to make flutter 3.13 works
path: ^1.8.1
auto_size_text: ^3.0.0
bot_toast: ^4.0.3
@@ -92,14 +92,14 @@ dependencies:
dropdown_button2: ^2.0.0
flutter_gpu_texture_renderer:
git:
url: https://github.com/21pages/flutter_gpu_texture_renderer
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
ref: 38951317afe79d953ab25733667bd96e172a80d3
uuid: ^3.0.7
auto_size_text_field: ^2.2.1
flex_color_picker: ^3.3.0
dynamic_layouts:
git:
url: https://github.com/21pages/dynamic_layouts.git
url: https://github.com/rustdesk-org/dynamic_layouts.git
ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9
pull_down_button: ^0.9.3
device_info_plus: ^9.1.0

View File

@@ -5,6 +5,9 @@
#include "resource.h"
#include <cstdlib> // for getenv and _putenv
#include <cstring> // for strcmp
namespace {
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
@@ -143,6 +146,25 @@ bool Win32Window::CreateAndShow(const std::wstring& title,
return OnCreate();
}
static void trySetWindowForeground(HWND window) {
char* value = nullptr;
size_t size = 0;
// Use _dupenv_s to safely get the environment variable
_dupenv_s(&value, &size, "SET_FOREGROUND_WINDOW");
if (value != nullptr) {
// Correctly compare the value with "1"
if (strcmp(value, "1") == 0) {
// Clear the environment variable
_putenv("SET_FOREGROUND_WINDOW=");
// Set the window to foreground
SetForegroundWindow(window);
}
// Free the duplicated string
free(value);
}
}
// static
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
UINT const message,
@@ -156,6 +178,7 @@ LRESULT CALLBACK Win32Window::WndProc(HWND const window,
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
EnableFullDpiSupportIfAvailable(window);
that->window_handle_ = window;
trySetWindowForeground(window);
} else if (Win32Window* that = GetThisFromHandle(window)) {
return that->MessageHandler(window, message, wparam, lparam);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 481 KiB

View File

@@ -795,7 +795,11 @@ impl FuseNode {
conn_id: desc.conn_id,
stream_id: rand::random(),
index: inode as usize - 2,
name: desc.name.to_str().unwrap().to_owned(),
name: desc
.name
.to_str()
.map(|s| s.to_string())
.unwrap_or_default(),
parent: None,
attributes: InodeAttributes::from_description(inode, desc),
children: Vec::new(),
@@ -1140,7 +1144,7 @@ mod fuse_test {
}
fn build_single_file(prefix: &str) {
let raw_name = "衬衫的价格为 9 镑 15 便士.txt";
let raw_name = "simple_test_file.txt";
let f_name = if prefix == "" {
raw_name.to_string()
} else {

View File

@@ -1,3 +1,4 @@
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::{CliprdrError, CliprdrServiceContext};
#[cfg(target_os = "windows")]
@@ -52,9 +53,9 @@ pub fn create_cliprdr_context(
log::warn!("umount {:?} may fail: {:?}", mnt_path, e);
}
let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse().unwrap())?;
let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse()?)?;
log::debug!("start cliprdr FUSE");
unix_ctx.run().expect("failed to start cliprdr FUSE");
unix_ctx.run()?;
Ok(Box::new(unix_ctx) as Box<_>)
}
@@ -63,8 +64,10 @@ pub fn create_cliprdr_context(
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
struct DummyCliprdrContext {}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl CliprdrServiceContext for DummyCliprdrContext {
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
Ok(())

View File

@@ -113,7 +113,7 @@ impl LocalFile {
let win32_time = self
.last_write_time
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.unwrap_or_default()
.as_nanos() as u64
/ 100
+ LDAP_EPOCH_DELTA;
@@ -188,7 +188,7 @@ impl LocalFile {
pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> {
self.load_handle()?;
let handle = self.handle.as_mut().unwrap();
let handle = self.handle.as_mut()?;
if offset != self.offset.load(Ordering::Relaxed) {
handle
@@ -238,9 +238,9 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
})?;
if mt.is_dir() {
let dir = std::fs::read_dir(path).unwrap();
let dir = std::fs::read_dir(path)?;
for entry in dir {
let entry = entry.unwrap();
let entry = entry?;
let path = entry.path();
constr_file_lst(&path, file_list, visited)?;
}

View File

@@ -383,13 +383,11 @@ impl ClipboardContext {
let file_contents_id = fmt_lst
.iter()
.find(|(_, name)| name == FILECONTENTS_FORMAT_NAME)
.map(|(id, _)| *id)
.unwrap();
.map(|(id, _)| *id)?;
let file_descriptor_id = fmt_lst
.iter()
.find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME)
.map(|(id, _)| *id)
.unwrap();
.map(|(id, _)| *id)?;
add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id);
add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id);

View File

@@ -7,9 +7,9 @@ use crate::CliprdrError;
// url encode and decode is needed
const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/');
pub(super) fn encode_path_to_uri(path: &PathBuf) -> String {
let encoded = percent_encoding::percent_encode(path.to_str().unwrap().as_bytes(), &ENCODE_SET)
.to_string();
pub(super) fn encode_path_to_uri(path: &PathBuf) -> io::Result<String> {
let encoded =
percent_encoding::percent_encode(path.to_str()?.as_bytes(), &ENCODE_SET).to_string();
format!("file://{}", encoded)
}
@@ -54,7 +54,7 @@ mod uri_test {
#[test]
fn test_conversion() {
let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png");
let uri = super::encode_path_to_uri(&path);
let uri = super::encode_path_to_uri(&path).unwrap();
assert_eq!(
uri,
"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png"

View File

@@ -89,7 +89,13 @@ impl SysClipboard for X11Clipboard {
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
*self.former_file_list.lock() = paths.to_vec();
let uri_list: Vec<String> = paths.iter().map(encode_path_to_uri).collect();
let uri_list: Vec<String> = {
let mut v = Vec::new();
for path in paths {
v.push(encode_path_to_uri(path)?);
}
v
};
let uri_list = uri_list.join("\n");
let text_uri_list_data = uri_list.as_bytes().to_vec();
let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat();

View File

@@ -5,16 +5,16 @@
#![allow(non_snake_case)]
#![allow(deref_nullptr)]
use std::{
boxed::Box,
ffi::{CStr, CString},
result::Result,
};
use crate::{
allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType,
ERR_CODE_INVALID_PARAMETER, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
};
use hbb_common::log;
use std::{
boxed::Box,
ffi::{CStr, CString},
result::Result,
};
// only used error code will be recorded here
/// success
@@ -779,7 +779,7 @@ pub fn server_format_list(
} else {
let n = match CString::new(format.1) {
Ok(n) => n,
Err(_) => CString::new("").unwrap(),
Err(_) => CString::new("").unwrap_or_default(),
};
CLIPRDR_FORMAT {
formatId: format.0 as UINT32,

View File

@@ -22,8 +22,8 @@ appveyor = { repository = "pythoneer/enigo-85xiy" }
serde = { version = "1.0", optional = true }
serde_derive = { version = "1.0", optional = true }
log = "0.4"
rdev = { git = "https://github.com/fufesou/rdev" }
tfc = { git = "https://github.com/fufesou/The-Fat-Controller" }
rdev = { git = "https://github.com/rustdesk-org/rdev" }
tfc = { git = "https://github.com/rustdesk-org/The-Fat-Controller", branch = "history/rebase_upstream_20240722" }
hbb_common = { path = "../hbb_common" }
[features]

View File

@@ -154,8 +154,8 @@ impl MouseControllable for Enigo {
}
},
match button {
MouseButton::Back => XBUTTON1 as u32 * WHEEL_DELTA as u32,
MouseButton::Forward => XBUTTON2 as u32 * WHEEL_DELTA as u32,
MouseButton::Back => XBUTTON1 as u32,
MouseButton::Forward => XBUTTON2 as u32,
_ => 0,
},
0,

View File

@@ -37,8 +37,8 @@ libc = "0.2"
dlopen = "0.1"
toml = "0.7"
uuid = { version = "1.3", features = ["v4"] }
# crash, versions >= 0.29.1 are affected by #GuillaumeGomez/sysinfo/1052
sysinfo = { git = "https://github.com/rustdesk-org/sysinfo" }
# new sysinfo issue: https://github.com/rustdesk/rustdesk/pull/6330#issuecomment-2270871442
sysinfo = { git = "https://github.com/rustdesk-org/sysinfo", branch = "rlim_max" }
thiserror = "1.0"
httparse = "1.5"
base64 = "0.22"
@@ -46,7 +46,7 @@ url = "2.2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1"
machine-uid = { git = "https://github.com/21pages/machine-uid" }
machine-uid = { git = "https://github.com/rustdesk-org/machine-uid" }
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false }
rustls-platform-verifier = "0.3.1"
@@ -58,7 +58,7 @@ tokio-native-tls ="0.3"
protobuf-codegen = { version = "3.4" }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser", "synchapi", "pdh", "memoryapi"] }
winapi = { version = "0.3", features = ["winuser", "synchapi", "pdh", "memoryapi", "sysinfoapi"] }
[target.'cfg(target_os = "macos")'.dependencies]
osascript = "0.3"

View File

@@ -81,10 +81,13 @@ message LoginRequest {
uint64 session_id = 10;
string version = 11;
OSLogin os_login = 12;
string my_platform = 13;
bytes hwid = 14;
}
message Auth2FA {
string code = 1;
bytes hwid = 2;
}
message ChatMessage { string text = 1; }
@@ -136,6 +139,7 @@ message LoginResponse {
string error = 1;
PeerInfo peer_info = 2;
}
bool enable_trusted_devices = 3;
}
message TouchScaleUpdate {
@@ -271,6 +275,10 @@ enum ControlKey {
RShift = 73;
RControl = 74;
RAlt = 75;
VolumeMute = 76; // mainly used on mobile devices as controlled side
VolumeUp = 77;
VolumeDown = 78;
Power = 79; // mainly used on mobile devices as controlled side
CtrlAltDel = 100;
LockScreen = 101;
}
@@ -311,11 +319,25 @@ message Hash {
string challenge = 2;
}
enum ClipboardFormat {
Text = 0;
Rtf = 1;
Html = 2;
ImageRgba = 21;
ImagePng = 22;
ImageSvg = 23;
}
message Clipboard {
bool compress = 1;
bytes content = 2;
int32 width = 3;
int32 height = 4;
ClipboardFormat format = 5;
}
message MultiClipboards { repeated Clipboard clipboards = 1; }
enum FileType {
Dir = 0;
DirLink = 2;
@@ -349,6 +371,12 @@ message ReadAllFiles {
bool include_hidden = 3;
}
message FileRename {
int32 id = 1;
string path = 2;
string new_name = 3;
}
message FileAction {
oneof union {
ReadDir read_dir = 1;
@@ -360,6 +388,7 @@ message FileAction {
ReadAllFiles all_files = 7;
FileTransferCancel cancel = 8;
FileTransferSendConfirmRequest send_confirm = 9;
FileRename rename = 10;
}
}
@@ -810,5 +839,6 @@ message Message {
PeerInfo peer_info = 25;
PointerDeviceEvent pointer_device_event = 26;
Auth2FA auth_2fa = 27;
MultiClipboards multi_clipboards = 28;
}
}

View File

@@ -21,13 +21,13 @@ message PunchHoleRequest {
string licence_key = 3;
ConnType conn_type = 4;
string token = 5;
string version = 6;
}
message PunchHole {
bytes socket_addr = 1;
string relay_server = 2;
NatType nat_type = 3;
string request_region = 4;
}
message TestNatRequest {
@@ -52,7 +52,6 @@ message PunchHoleSent {
string relay_server = 3;
NatType nat_type = 4;
string version = 5;
string request_region = 6;
}
message RegisterPk {
@@ -92,6 +91,7 @@ message PunchHoleResponse {
bool is_local = 6;
}
string other_failure = 7;
int32 feedback = 8;
}
message ConfigUpdate {
@@ -108,7 +108,6 @@ message RequestRelay {
string licence_key = 6;
ConnType conn_type = 7;
string token = 8;
string request_region = 9;
}
message RelayResponse {
@@ -121,7 +120,7 @@ message RelayResponse {
}
string refuse_reason = 6;
string version = 7;
string request_region = 8;
int32 feedback = 9;
}
message SoftwareUpdate { string url = 1; }
@@ -133,7 +132,6 @@ message SoftwareUpdate { string url = 1; }
message FetchLocalAddr {
bytes socket_addr = 1;
string relay_server = 2;
string request_region = 3;
}
message LocalAddr {
@@ -142,7 +140,6 @@ message LocalAddr {
string relay_server = 3;
string id = 4;
string version = 5;
string request_region = 6;
}
message PeerDiscovery {
@@ -168,6 +165,10 @@ message KeyExchange {
repeated bytes keys = 1;
}
message HealthCheck {
string token = 1;
}
message RendezvousMessage {
oneof union {
RegisterPeer register_peer = 6;
@@ -190,5 +191,6 @@ message RendezvousMessage {
OnlineRequest online_request = 23;
OnlineResponse online_response = 24;
KeyExchange key_exchange = 25;
HealthCheck hc = 26;
}
}

View File

@@ -1,5 +1,5 @@
use std::{cell::RefCell, io};
use zstd::bulk::{Compressor, Decompressor};
use zstd::bulk::Compressor;
// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(),
// which is currently 22. Levels >= 20
@@ -7,7 +7,6 @@ use zstd::bulk::{Compressor, Decompressor};
// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT
thread_local! {
static COMPRESSOR: RefCell<io::Result<Compressor<'static>>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL));
static DECOMPRESSOR: RefCell<io::Result<Decompressor<'static>>> = RefCell::new(Decompressor::new());
}
pub fn compress(data: &[u8]) -> Vec<u8> {
@@ -31,27 +30,5 @@ pub fn compress(data: &[u8]) -> Vec<u8> {
}
pub fn decompress(data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
DECOMPRESSOR.with(|d| {
if let Ok(mut d) = d.try_borrow_mut() {
match &mut *d {
Ok(d) => {
const MAX: usize = 1024 * 1024 * 64;
const MIN: usize = 1024 * 1024;
let mut n = 30 * data.len();
n = n.clamp(MIN, MAX);
match d.decompress(data, n) {
Ok(res) => out = res,
Err(err) => {
crate::log::debug!("Failed to decompress: {}", err);
}
}
}
Err(err) => {
crate::log::debug!("Failed to get decompressor: {}", err);
}
}
}
});
out
zstd::decode_all(data).unwrap_or_default()
}

View File

@@ -10,6 +10,7 @@ use std::{
};
use anyhow::Result;
use bytes::Bytes;
use rand::Rng;
use regex::Regex;
use serde as de;
@@ -52,6 +53,7 @@ lazy_static::lazy_static! {
static ref CONFIG: RwLock<Config> = RwLock::new(Config::load());
static ref CONFIG2: RwLock<Config2> = RwLock::new(Config2::load());
static ref LOCAL_CONFIG: RwLock<LocalConfig> = RwLock::new(LocalConfig::load());
static ref TRUSTED_DEVICES: RwLock<(Vec<TrustedDevice>, bool)> = Default::default();
static ref ONLINE: Mutex<HashMap<String, i64>> = Default::default();
pub static ref PROD_RENDEZVOUS_SERVER: RwLock<String> = RwLock::new(match option_env!("RENDEZVOUS_SERVER") {
Some(key) if !key.is_empty() => key,
@@ -69,6 +71,7 @@ lazy_static::lazy_static! {
pub static ref DEFAULT_LOCAL_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
pub static ref OVERWRITE_LOCAL_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
pub static ref HARD_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
pub static ref BUILTIN_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
}
lazy_static::lazy_static! {
@@ -207,6 +210,10 @@ pub struct Config2 {
nat_type: i32,
#[serde(default, deserialize_with = "deserialize_i32")]
serial: i32,
#[serde(default, deserialize_with = "deserialize_string")]
unlock_pin: String,
#[serde(default, deserialize_with = "deserialize_string")]
trusted_devices: String,
#[serde(default)]
socks: Option<Socks5Server>,
@@ -426,14 +433,20 @@ fn patch(path: PathBuf) -> PathBuf {
impl Config2 {
fn load() -> Config2 {
let mut config = Config::load_::<Config2>("2");
let mut store = false;
if let Some(mut socks) = config.socks {
let (password, _, store) =
let (password, _, store2) =
decrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION);
socks.password = password;
config.socks = Some(socks);
if store {
config.store();
}
store |= store2;
}
let (unlock_pin, _, store2) =
decrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION);
config.unlock_pin = unlock_pin;
store |= store2;
if store {
config.store();
}
config
}
@@ -449,6 +462,8 @@ impl Config2 {
encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
config.socks = Some(socks);
}
config.unlock_pin =
encrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
Config::store_(&config, "2");
}
@@ -487,7 +502,19 @@ pub fn load_path<T: serde::Serialize + serde::de::DeserializeOwned + Default + s
#[inline]
pub fn store_path<T: serde::Serialize>(path: PathBuf, cfg: T) -> crate::ResultType<()> {
Ok(confy::store_path(path, cfg)?)
#[cfg(not(windows))]
{
use std::os::unix::fs::PermissionsExt;
Ok(confy::store_path_perms(
path,
cfg,
fs::Permissions::from_mode(0o600),
)?)
}
#[cfg(windows)]
{
Ok(confy::store_path(path, cfg)?)
}
}
impl Config {
@@ -975,6 +1002,7 @@ impl Config {
}
config.password = password.into();
config.store();
Self::clear_trusted_devices();
}
pub fn get_permanent_password() -> String {
@@ -1068,6 +1096,77 @@ impl Config {
NetworkType::Direct
}
pub fn get_unlock_pin() -> String {
CONFIG2.read().unwrap().unlock_pin.clone()
}
pub fn set_unlock_pin(pin: &str) {
let mut config = CONFIG2.write().unwrap();
if pin == config.unlock_pin {
return;
}
config.unlock_pin = pin.to_string();
config.store();
}
pub fn get_trusted_devices_json() -> String {
serde_json::to_string(&Self::get_trusted_devices()).unwrap_or_default()
}
pub fn get_trusted_devices() -> Vec<TrustedDevice> {
let (devices, synced) = TRUSTED_DEVICES.read().unwrap().clone();
if synced {
return devices;
}
let devices = CONFIG2.read().unwrap().trusted_devices.clone();
let (devices, succ, store) = decrypt_str_or_original(&devices, PASSWORD_ENC_VERSION);
if succ {
let mut devices: Vec<TrustedDevice> =
serde_json::from_str(&devices).unwrap_or_default();
let len = devices.len();
devices.retain(|d| !d.outdate());
if store || devices.len() != len {
Self::set_trusted_devices(devices.clone());
}
*TRUSTED_DEVICES.write().unwrap() = (devices.clone(), true);
devices
} else {
Default::default()
}
}
fn set_trusted_devices(mut trusted_devices: Vec<TrustedDevice>) {
trusted_devices.retain(|d| !d.outdate());
let devices = serde_json::to_string(&trusted_devices).unwrap_or_default();
let max_len = 1024 * 1024;
if devices.bytes().len() > max_len {
log::error!("Trusted devices too large: {}", devices.bytes().len());
return;
}
let devices = encrypt_str_or_original(&devices, PASSWORD_ENC_VERSION, max_len);
let mut config = CONFIG2.write().unwrap();
config.trusted_devices = devices;
config.store();
*TRUSTED_DEVICES.write().unwrap() = (trusted_devices, true);
}
pub fn add_trusted_device(device: TrustedDevice) {
let mut devices = Self::get_trusted_devices();
devices.retain(|d| d.hwid != device.hwid);
devices.push(device);
Self::set_trusted_devices(devices);
}
pub fn remove_trusted_devices(hwids: &Vec<Bytes>) {
let mut devices = Self::get_trusted_devices();
devices.retain(|d| !hwids.contains(&d.hwid));
Self::set_trusted_devices(devices);
}
pub fn clear_trusted_devices() {
Self::set_trusted_devices(Default::default());
}
pub fn get() -> Config {
return CONFIG.read().unwrap().clone();
}
@@ -1898,6 +1997,22 @@ impl Group {
}
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct TrustedDevice {
pub hwid: Bytes,
pub time: i64,
pub id: String,
pub name: String,
pub platform: String,
}
impl TrustedDevice {
pub fn outdate(&self) -> bool {
const DAYS_90: i64 = 90 * 24 * 60 * 60 * 1000;
self.time + DAYS_90 < crate::get_time()
}
}
deserialize_default!(deserialize_string, String);
deserialize_default!(deserialize_bool, bool);
deserialize_default!(deserialize_i32, i32);
@@ -2087,6 +2202,22 @@ pub mod keys {
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";
pub const OPTION_ENABLE_TRUSTED_DEVICES: &str = "enable-trusted-devices";
// buildin options
pub const OPTION_DISPLAY_NAME: &str = "display-name";
pub const OPTION_DISABLE_UDP: &str = "disable-udp";
pub const OPTION_PRESET_USERNAME: &str = "preset-user-name";
pub const OPTION_PRESET_STRATEGY_NAME: &str = "preset-strategy-name";
pub const OPTION_REMOVE_PRESET_PASSWORD_WARNING: &str = "remove-preset-password-warning";
pub const OPTION_HIDE_SECURITY_SETTINGS: &str = "hide-security-settings";
pub const OPTION_HIDE_NETWORK_SETTINGS: &str = "hide-network-settings";
pub const OPTION_HIDE_SERVER_SETTINGS: &str = "hide-server-settings";
pub const OPTION_HIDE_PROXY_SETTINGS: &str = "hide-proxy-settings";
pub const OPTION_HIDE_USERNAME_ON_CARD: &str = "hide-username-on-card";
pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards";
pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password";
pub const OPTION_HIDE_TRAY: &str = "hide-tray";
// flutter local options
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
@@ -2096,6 +2227,7 @@ pub mod keys {
pub const OPTION_FLUTTER_PEER_TAB_VISIBLE: &str = "peer-tab-visible";
pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type";
pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name";
pub const OPTION_ALLOW_REMOTE_CM_MODIFICATION: &str = "allow-remote-cm-modification";
// android floating window options
pub const OPTION_DISABLE_FLOATING_WINDOW: &str = "disable-floating-window";
@@ -2173,6 +2305,7 @@ pub mod keys {
OPTION_KEEP_SCREEN_ON,
OPTION_DISABLE_GROUP_PANEL,
OPTION_PRE_ELEVATE_SERVICE,
OPTION_ALLOW_REMOTE_CM_MODIFICATION,
];
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS
pub const KEYS_SETTINGS: &[&str] = &[
@@ -2211,6 +2344,24 @@ pub mod keys {
OPTION_PRESET_ADDRESS_BOOK_TAG,
OPTION_ENABLE_DIRECTX_CAPTURE,
OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE,
OPTION_ENABLE_TRUSTED_DEVICES,
];
// BUILDIN_SETTINGS
pub const KEYS_BUILDIN_SETTINGS: &[&str] = &[
OPTION_DISPLAY_NAME,
OPTION_DISABLE_UDP,
OPTION_PRESET_USERNAME,
OPTION_PRESET_STRATEGY_NAME,
OPTION_REMOVE_PRESET_PASSWORD_WARNING,
OPTION_HIDE_SECURITY_SETTINGS,
OPTION_HIDE_NETWORK_SETTINGS,
OPTION_HIDE_SERVER_SETTINGS,
OPTION_HIDE_PROXY_SETTINGS,
OPTION_HIDE_USERNAME_ON_CARD,
OPTION_HIDE_HELP_CARDS,
OPTION_DEFAULT_CONNECT_PASSWORD,
OPTION_HIDE_TRAY,
];
}
@@ -2471,4 +2622,26 @@ mod tests {
assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_field_str");
}
}
#[test]
fn test_store_load() {
let peerconfig_id = "123456789";
let cfg: PeerConfig = Default::default();
cfg.store(&peerconfig_id);
assert_eq!(PeerConfig::load(&peerconfig_id), cfg);
#[cfg(not(windows))]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
// ignore file type information by masking with 0o777 (see https://stackoverflow.com/a/50045872)
fs::metadata(PeerConfig::path(&peerconfig_id))
.expect("reading metadata failed")
.permissions()
.mode()
& 0o777,
0o600
);
}
}
}

View File

@@ -838,6 +838,21 @@ pub fn create_dir(dir: &str) -> ResultType<()> {
Ok(())
}
#[inline]
pub fn rename_file(path: &str, new_name: &str) -> ResultType<()> {
let path = std::path::Path::new(&path);
if path.exists() {
let dir = path
.parent()
.ok_or(anyhow!("Parent directoy of {path:?} not exists"))?;
let new_path = dir.join(&new_name);
std::fs::rename(&path, &new_path)?;
Ok(())
} else {
bail!("{path:?} not exists");
}
}
#[inline]
pub fn transform_windows_path(entries: &mut Vec<FileEntry>) {
for entry in entries {

View File

@@ -85,40 +85,19 @@ pub fn get_display_server_of_session(session: &str) -> String {
run_loginctl(Some(vec!["show-session", "-p", "Type", session]))
// Check session type of the session
{
let display_server = String::from_utf8_lossy(&output.stdout)
String::from_utf8_lossy(&output.stdout)
.replace("Type=", "")
.trim_end()
.into();
if display_server == "tty" {
// If the type is tty...
if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "TTY", session]))
// Get the tty number
{
let tty: String = String::from_utf8_lossy(&output.stdout)
.replace("TTY=", "")
.trim_end()
.into();
if let Ok(xorg_results) = run_cmds(&format!("ps -e | grep \"{tty}.\\\\+Xorg\""))
// And check if Xorg is running on that tty
{
if xorg_results.trim_end() != "" {
// If it is, manually return "x11", otherwise return tty
return "x11".to_owned();
}
}
}
}
display_server
.into()
} else {
"".to_owned()
};
if display_server.is_empty() || display_server == "tty" {
// loginctl has not given the expected output. try something else.
if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") {
display_server = sestype;
if !sestype.is_empty() {
return sestype.to_lowercase();
}
}
}
if display_server == "" {
display_server = "x11".to_owned();
}
display_server.to_lowercase()

View File

@@ -1,6 +1,5 @@
use std::{
collections::VecDeque,
os::windows::raw::HANDLE,
sync::{Arc, Mutex},
time::Instant,
};
@@ -17,7 +16,7 @@ use winapi::{
sysinfoapi::VerSetConditionMask,
winbase::{VerifyVersionInfoW, INFINITE, WAIT_OBJECT_0},
winnt::{
OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION,
HANDLE, OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION,
VER_MINORVERSION, VER_SERVICEPACKMAJOR, VER_SERVICEPACKMINOR,
},
},

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk-portable-packer"
version = "1.2.6"
version = "1.3.0"
edition = "2021"
description = "RustDesk Remote Desktop"
@@ -14,6 +14,9 @@ dirs = "5.0"
md5 = "0.7"
winapi = { version = "0.3", features = ["winbase"] }
[target.'cfg(target_os = "windows")'.dependencies]
native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]}
[package.metadata.winres]
LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved."
ProductName = "RustDesk"

View File

@@ -8,6 +8,8 @@ use std::{
use bin_reader::BinaryReader;
pub mod bin_reader;
#[cfg(windows)]
mod ui;
#[cfg(windows)]
const APP_METADATA: &[u8] = include_bytes!("../app_metadata.toml");
@@ -17,6 +19,8 @@ const APP_METADATA_CONFIG: &str = "meta.toml";
const META_LINE_PREFIX_TIMESTAMP: &str = "timestamp = ";
const APP_PREFIX: &str = "rustdesk";
const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
#[cfg(windows)]
const SET_FOREGROUND_WINDOW_ENV_KEY: &str = "SET_FOREGROUND_WINDOW";
fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool {
let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else {
@@ -55,7 +59,13 @@ fn write_meta(dir: &PathBuf, ts: u64) {
}
}
fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<PathBuf> {
fn setup(
reader: BinaryReader,
dir: Option<PathBuf>,
clear: bool,
_args: &Vec<String>,
_ui: &mut bool,
) -> Option<PathBuf> {
let dir = if let Some(dir) = dir {
dir
} else {
@@ -70,6 +80,11 @@ fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<Path
let mut ts = 0;
if clear || !is_timestamp_matches(&dir, &mut ts) {
#[cfg(windows)]
if _args.is_empty() {
*_ui = true;
ui::setup();
}
std::fs::remove_dir_all(&dir).ok();
}
for file in reader.files.iter() {
@@ -83,7 +98,7 @@ fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<Path
Some(dir.join(&reader.exe))
}
fn execute(path: PathBuf, args: Vec<String>) {
fn execute(path: PathBuf, args: Vec<String>, _ui: bool) {
println!("executing {}", path.display());
// setup env
let exe = std::env::current_exe().unwrap_or_default();
@@ -95,13 +110,28 @@ fn execute(path: PathBuf, args: Vec<String>) {
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
if _ui {
cmd.env(SET_FOREGROUND_WINDOW_ENV_KEY, "1");
}
}
cmd.env(APPNAME_RUNTIME_ENV_KEY, exe_name)
let _child = cmd
.env(APPNAME_RUNTIME_ENV_KEY, exe_name)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.ok();
.spawn();
#[cfg(windows)]
if _ui {
match _child {
Ok(child) => unsafe {
winapi::um::winuser::AllowSetForegroundWindow(child.id() as u32);
},
Err(e) => {
eprintln!("{:?}", e);
}
}
}
}
fn main() {
@@ -119,18 +149,21 @@ fn main() {
let click_setup = args.is_empty() && arg_exe.to_lowercase().ends_with("install.exe");
let quick_support = args.is_empty() && arg_exe.to_lowercase().ends_with("qs.exe");
let mut ui = false;
let reader = BinaryReader::default();
if let Some(exe) = setup(
reader,
None,
click_setup || args.contains(&"--silent-install".to_owned()),
&args,
&mut ui,
) {
if click_setup {
args = vec!["--install".to_owned()];
} else if quick_support {
args = vec!["--quick_support".to_owned()];
}
execute(exe, args);
execute(exe, args, ui);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

232
libs/portable/src/ui.rs Normal file
View File

@@ -0,0 +1,232 @@
use native_windows_gui as nwg;
use nwg::NativeUi;
use std::cell::RefCell;
const GIF_DATA: &[u8] = include_bytes!("./res/spin.gif");
const LABEL_DATA: &[u8] = include_bytes!("./res/label.png");
const GIF_SIZE: i32 = 32;
const BG_COLOR: [u8; 3] = [90, 90, 120];
const BORDER_COLOR: [u8; 3] = [40, 40, 40];
const GIF_DELAY: u64 = 30;
#[derive(Default)]
pub struct BasicApp {
window: nwg::Window,
border_image: nwg::ImageFrame,
bg_image: nwg::ImageFrame,
gif_image: nwg::ImageFrame,
label_image: nwg::ImageFrame,
border_layout: nwg::GridLayout,
bg_layout: nwg::GridLayout,
inner_layout: nwg::GridLayout,
timer: nwg::AnimationTimer,
decoder: nwg::ImageDecoder,
gif_index: RefCell<usize>,
gif_images: RefCell<Vec<nwg::Bitmap>>,
}
impl BasicApp {
fn exit(&self) {
self.timer.stop();
nwg::stop_thread_dispatch();
}
fn load_gif(&self) -> Result<(), nwg::NwgError> {
let image_source = self.decoder.from_stream(GIF_DATA)?;
for frame_index in 0..image_source.frame_count() {
let image_data = image_source.frame(frame_index)?;
let image_data = self
.decoder
.resize_image(&image_data, [GIF_SIZE as u32, GIF_SIZE as u32])?;
let bmp = image_data.as_bitmap()?;
self.gif_images.borrow_mut().push(bmp);
}
Ok(())
}
fn update_gif(&self) -> Result<(), nwg::NwgError> {
let images = self.gif_images.borrow();
if images.len() == 0 {
return Err(nwg::NwgError::ImageDecoderError(
-1,
"no gif images".to_string(),
));
}
let image_index = *self.gif_index.borrow() % images.len();
self.gif_image.set_bitmap(Some(&images[image_index]));
*self.gif_index.borrow_mut() = (image_index + 1) % images.len();
Ok(())
}
fn start_timer(&self) {
self.timer.start();
}
}
mod basic_app_ui {
use super::*;
use native_windows_gui::{self as nwg, Bitmap};
use nwg::{Event, GridLayoutItem};
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
pub struct BasicAppUi {
inner: Rc<BasicApp>,
default_handler: RefCell<Vec<nwg::EventHandler>>,
}
impl nwg::NativeUi<BasicAppUi> for BasicApp {
fn build_ui(mut data: BasicApp) -> Result<BasicAppUi, nwg::NwgError> {
data.decoder = nwg::ImageDecoder::new()?;
let col_cnt: i32 = 7;
let row_cnt: i32 = 3;
let border_width: i32 = 1;
let window_size = (
GIF_SIZE * col_cnt + 2 * border_width,
GIF_SIZE * row_cnt + 2 * border_width,
);
// Controls
nwg::Window::builder()
.flags(nwg::WindowFlags::POPUP | nwg::WindowFlags::VISIBLE)
.size(window_size)
.center(true)
.build(&mut data.window)?;
nwg::ImageFrame::builder()
.parent(&data.window)
.size(window_size)
.background_color(Some(BORDER_COLOR))
.build(&mut data.border_image)?;
nwg::ImageFrame::builder()
.parent(&data.border_image)
.size((row_cnt * GIF_SIZE, col_cnt * GIF_SIZE))
.background_color(Some(BG_COLOR))
.build(&mut data.bg_image)?;
nwg::ImageFrame::builder()
.parent(&data.bg_image)
.size((GIF_SIZE, GIF_SIZE))
.background_color(Some(BG_COLOR))
.build(&mut data.gif_image)?;
nwg::ImageFrame::builder()
.parent(&data.bg_image)
.background_color(Some(BG_COLOR))
.bitmap(Some(&Bitmap::from_bin(LABEL_DATA)?))
.build(&mut data.label_image)?;
nwg::AnimationTimer::builder()
.parent(&data.window)
.interval(std::time::Duration::from_millis(GIF_DELAY))
.build(&mut data.timer)?;
// Wrap-up
let ui = BasicAppUi {
inner: Rc::new(data),
default_handler: Default::default(),
};
// Layouts
nwg::GridLayout::builder()
.parent(&ui.window)
.spacing(0)
.margin([0, 0, 0, 0])
.max_column(Some(1))
.max_row(Some(1))
.child_item(GridLayoutItem::new(&ui.border_image, 0, 0, 1, 1))
.build(&ui.border_layout)?;
nwg::GridLayout::builder()
.parent(&ui.border_image)
.spacing(0)
.margin([
border_width as _,
border_width as _,
border_width as _,
border_width as _,
])
.max_column(Some(1))
.max_row(Some(1))
.child_item(GridLayoutItem::new(&ui.bg_image, 0, 0, 1, 1))
.build(&ui.bg_layout)?;
nwg::GridLayout::builder()
.parent(&ui.bg_image)
.spacing(0)
.margin([0, 0, 0, 0])
.max_column(Some(col_cnt as _))
.max_row(Some(row_cnt as _))
.child_item(GridLayoutItem::new(&ui.gif_image, 2, 1, 1, 1))
.child_item(GridLayoutItem::new(&ui.label_image, 3, 1, 3, 1))
.build(&ui.inner_layout)?;
// Events
let evt_ui = Rc::downgrade(&ui.inner);
let handle_events = move |evt, _evt_data, _handle| {
if let Some(evt_ui) = evt_ui.upgrade().as_mut() {
match evt {
Event::OnWindowClose => {
evt_ui.exit();
}
Event::OnTimerTick => {
if let Err(e) = evt_ui.update_gif() {
eprintln!("{:?}", e);
}
}
_ => {}
}
}
};
ui.default_handler
.borrow_mut()
.push(nwg::full_bind_event_handler(
&ui.window.handle,
handle_events,
));
return Ok(ui);
}
}
impl Drop for BasicAppUi {
/// To make sure that everything is freed without issues, the default handler must be unbound.
fn drop(&mut self) {
let mut handlers = self.default_handler.borrow_mut();
for handler in handlers.drain(0..) {
nwg::unbind_event_handler(&handler);
}
}
}
impl Deref for BasicAppUi {
type Target = BasicApp;
fn deref(&self) -> &BasicApp {
&self.inner
}
}
}
fn ui() -> Result<(), nwg::NwgError> {
nwg::init()?;
let app = BasicApp::build_ui(Default::default())?;
app.load_gif()?;
app.start_timer();
nwg::dispatch_thread_events();
Ok(())
}
pub fn setup() {
std::thread::spawn(move || {
if let Err(e) = ui() {
eprintln!("{:?}", e);
}
});
}

View File

@@ -21,7 +21,7 @@ cfg-if = "1.0"
num_cpus = "1.15"
lazy_static = "1.4"
hbb_common = { path = "../hbb_common" }
webm = { git = "https://github.com/21pages/rust-webm" }
webm = { git = "https://github.com/rustdesk-org/rust-webm" }
serde = {version="1.0", features=["derive"]}
[dependencies.winapi]
@@ -59,7 +59,7 @@ gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
gstreamer-video = { version = "0.16", optional = true }
[dependencies.hwcodec]
git = "https://github.com/21pages/hwcodec"
git = "https://github.com/rustdesk-org/hwcodec"
optional = true

View File

@@ -188,6 +188,52 @@ fn gen_vcpkg_package(package: &str, ffi_header: &str, generated: &str, regex: &s
generate_bindings(&ffi_header, &includes, &ffi_rs, &exact_file, regex);
}
// If you have problems installing ffmpeg, you can download $VCPKG_ROOT/installed from ci
// Linux require link in hwcodec
/*
fn ffmpeg() {
// ffmpeg
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let static_libs = vec!["avcodec", "avutil", "avformat"];
static_libs.iter().for_each(|lib| {
find_package(lib);
});
if target_os == "windows" {
println!("cargo:rustc-link-lib=static=libmfx");
}
// os
let dyn_libs: Vec<&str> = if target_os == "windows" {
["User32", "bcrypt", "ole32", "advapi32"].to_vec()
} else if target_os == "linux" {
let mut v = ["va", "va-drm", "va-x11", "vdpau", "X11", "stdc++"].to_vec();
if target_arch == "x86_64" {
v.push("z");
}
v
} else if target_os == "macos" || target_os == "ios" {
["c++", "m"].to_vec()
} else if target_os == "android" {
["z", "m", "android", "atomic"].to_vec()
} else {
panic!("unsupported os");
};
dyn_libs
.iter()
.map(|lib| println!("cargo:rustc-link-lib={}", lib))
.count();
if target_os == "macos" || target_os == "ios" {
println!("cargo:rustc-link-lib=framework=CoreFoundation");
println!("cargo:rustc-link-lib=framework=CoreVideo");
println!("cargo:rustc-link-lib=framework=CoreMedia");
println!("cargo:rustc-link-lib=framework=VideoToolbox");
println!("cargo:rustc-link-lib=framework=AVFoundation");
}
}
*/
fn main() {
// note: all link symbol names in x86 (32-bit) are prefixed wth "_".
// run "rustup show" to show current default toolchain, if it is stable-x86-pc-windows-msvc,
@@ -204,6 +250,7 @@ fn main() {
gen_vcpkg_package("libvpx", "vpx_ffi.h", "vpx_ffi.rs", "^[vV].*");
gen_vcpkg_package("aom", "aom_ffi.h", "aom_ffi.rs", "^(aom|AOM|OBU|AV1).*");
gen_vcpkg_package("libyuv", "yuv_ffi.h", "yuv_ffi.rs", ".*");
// ffmpeg();
// there is problem with cfg(target_os) in build.rs, so use our workaround
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();

View File

@@ -349,6 +349,10 @@ fn init_ndk_context() -> JniResult<()> {
jvm.get_java_vm_pointer() as _,
ctx.as_obj().as_raw() as _,
);
#[cfg(feature = "hwcodec")]
hwcodec::android::ffmpeg_set_java_vm(
jvm.get_java_vm_pointer() as _,
);
}
*lock = true;
return Ok(());

View File

@@ -21,7 +21,7 @@ use crate::{
use hbb_common::{
anyhow::anyhow,
bail,
config::{keys::OPTION_ENABLE_HWCODEC, option2bool, Config, PeerConfig},
config::{option2bool, Config, PeerConfig},
lazy_static, log,
message_proto::{
supported_decoding::PreferCodec, video_frame, Chroma, CodecAbility, EncodedVideoFrames,
@@ -836,7 +836,9 @@ 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") {
use hbb_common::config::keys::OPTION_ENABLE_HWCODEC;
if !cfg!(target_os = "ios") {
return option2bool(
OPTION_ENABLE_HWCODEC,
&Config::get_option(OPTION_ENABLE_HWCODEC),
@@ -846,6 +848,8 @@ pub fn enable_hwcodec_option() -> bool {
}
#[cfg(feature = "vram")]
pub fn enable_vram_option(encode: bool) -> bool {
use hbb_common::config::keys::OPTION_ENABLE_HWCODEC;
if cfg!(windows) {
let enable = option2bool(
OPTION_ENABLE_HWCODEC,

View File

@@ -32,13 +32,16 @@ pub fn convert_to_yuv(
dst_fmt.h
);
}
if src_pixfmt == crate::Pixfmt::BGRA || src_pixfmt == crate::Pixfmt::RGBA {
if src_pixfmt == crate::Pixfmt::BGRA
|| src_pixfmt == crate::Pixfmt::RGBA
|| src_pixfmt == crate::Pixfmt::RGB565LE
{
// stride is calculated, not real, so we need to check it
if src_stride[0] < src_width * 4 {
if src_stride[0] < src_width * src_pixfmt.bytes_per_pixel() {
bail!(
"src_stride[0] < src_width * 4: {} < {}",
"src_stride too small: {} < {}",
src_stride[0],
src_width * 4
src_width * src_pixfmt.bytes_per_pixel()
);
}
if src.len() < src_stride[0] * src_height {
@@ -51,19 +54,26 @@ pub fn convert_to_yuv(
}
}
let align = |x: usize| (x + 63) / 64 * 64;
let unsupported = format!(
"unsupported pixfmt conversion: {src_pixfmt:?} -> {:?}",
dst_fmt.pixfmt
);
match (src_pixfmt, dst_fmt.pixfmt) {
(crate::Pixfmt::BGRA, crate::Pixfmt::I420) | (crate::Pixfmt::RGBA, crate::Pixfmt::I420) => {
(crate::Pixfmt::BGRA, crate::Pixfmt::I420)
| (crate::Pixfmt::RGBA, crate::Pixfmt::I420)
| (crate::Pixfmt::RGB565LE, crate::Pixfmt::I420) => {
let dst_stride_y = dst_fmt.stride[0];
let dst_stride_uv = dst_fmt.stride[1];
dst.resize(dst_fmt.h * dst_stride_y * 2, 0); // waste some memory to ensure memory safety
let dst_y = dst.as_mut_ptr();
let dst_u = dst[dst_fmt.u..].as_mut_ptr();
let dst_v = dst[dst_fmt.v..].as_mut_ptr();
let f = if src_pixfmt == crate::Pixfmt::BGRA {
ARGBToI420
} else {
ABGRToI420
let f = match src_pixfmt {
crate::Pixfmt::BGRA => ARGBToI420,
crate::Pixfmt::RGBA => ABGRToI420,
crate::Pixfmt::RGB565LE => RGB565ToI420,
_ => bail!(unsupported),
};
call_yuv!(f(
src.as_ptr(),
@@ -78,7 +88,9 @@ pub fn convert_to_yuv(
src_height as _,
));
}
(crate::Pixfmt::BGRA, crate::Pixfmt::NV12) | (crate::Pixfmt::RGBA, crate::Pixfmt::NV12) => {
(crate::Pixfmt::BGRA, crate::Pixfmt::NV12)
| (crate::Pixfmt::RGBA, crate::Pixfmt::NV12)
| (crate::Pixfmt::RGB565LE, crate::Pixfmt::NV12) => {
let dst_stride_y = dst_fmt.stride[0];
let dst_stride_uv = dst_fmt.stride[1];
dst.resize(
@@ -87,14 +99,33 @@ pub fn convert_to_yuv(
);
let dst_y = dst.as_mut_ptr();
let dst_uv = dst[dst_fmt.u..].as_mut_ptr();
let f = if src_pixfmt == crate::Pixfmt::BGRA {
ARGBToNV12
} else {
ABGRToNV12
let (input, input_stride) = match src_pixfmt {
crate::Pixfmt::BGRA => (src.as_ptr(), src_stride[0]),
crate::Pixfmt::RGBA => (src.as_ptr(), src_stride[0]),
crate::Pixfmt::RGB565LE => {
let mid_stride = src_width * 4;
mid_data.resize(mid_stride * src_height, 0);
call_yuv!(RGB565ToARGB(
src.as_ptr(),
src_stride[0] as _,
mid_data.as_mut_ptr(),
mid_stride as _,
src_width as _,
src_height as _,
));
(mid_data.as_ptr(), mid_stride)
}
_ => bail!(unsupported),
};
let f = match src_pixfmt {
crate::Pixfmt::BGRA => ARGBToNV12,
crate::Pixfmt::RGBA => ABGRToNV12,
crate::Pixfmt::RGB565LE => ARGBToNV12,
_ => bail!(unsupported),
};
call_yuv!(f(
src.as_ptr(),
src_stride[0] as _,
input,
input_stride as _,
dst_y,
dst_stride_y as _,
dst_uv,
@@ -103,7 +134,9 @@ pub fn convert_to_yuv(
src_height as _,
));
}
(crate::Pixfmt::BGRA, crate::Pixfmt::I444) | (crate::Pixfmt::RGBA, crate::Pixfmt::I444) => {
(crate::Pixfmt::BGRA, crate::Pixfmt::I444)
| (crate::Pixfmt::RGBA, crate::Pixfmt::I444)
| (crate::Pixfmt::RGB565LE, crate::Pixfmt::I444) => {
let dst_stride_y = dst_fmt.stride[0];
let dst_stride_u = dst_fmt.stride[1];
let dst_stride_v = dst_fmt.stride[2];
@@ -115,23 +148,39 @@ pub fn convert_to_yuv(
let dst_y = dst.as_mut_ptr();
let dst_u = dst[dst_fmt.u..].as_mut_ptr();
let dst_v = dst[dst_fmt.v..].as_mut_ptr();
let src = if src_pixfmt == crate::Pixfmt::BGRA {
src
} else {
mid_data.resize(src.len(), 0);
call_yuv!(ABGRToARGB(
src.as_ptr(),
src_stride[0] as _,
mid_data.as_mut_ptr(),
src_stride[0] as _,
src_width as _,
src_height as _,
));
mid_data
let (input, input_stride) = match src_pixfmt {
crate::Pixfmt::BGRA => (src.as_ptr(), src_stride[0]),
crate::Pixfmt::RGBA => {
mid_data.resize(src.len(), 0);
call_yuv!(ABGRToARGB(
src.as_ptr(),
src_stride[0] as _,
mid_data.as_mut_ptr(),
src_stride[0] as _,
src_width as _,
src_height as _,
));
(mid_data.as_ptr(), src_stride[0])
}
crate::Pixfmt::RGB565LE => {
let mid_stride = src_width * 4;
mid_data.resize(mid_stride * src_height, 0);
call_yuv!(RGB565ToARGB(
src.as_ptr(),
src_stride[0] as _,
mid_data.as_mut_ptr(),
mid_stride as _,
src_width as _,
src_height as _,
));
(mid_data.as_ptr(), mid_stride)
}
_ => bail!(unsupported),
};
call_yuv!(ARGBToI444(
src.as_ptr(),
src_stride[0] as _,
input,
input_stride as _,
dst_y,
dst_stride_y as _,
dst_u,
@@ -143,10 +192,7 @@ pub fn convert_to_yuv(
));
}
_ => {
bail!(
"convert not support, {src_pixfmt:?} -> {:?}",
dst_fmt.pixfmt
);
bail!(unsupported);
}
}
Ok(())

View File

@@ -192,19 +192,21 @@ impl EncoderApi for HwRamEncoder {
}
fn support_abr(&self) -> bool {
["qsv", "vaapi", "mediacodec"]
["qsv", "vaapi", "mediacodec", "videotoolbox"]
.iter()
.all(|&x| !self.config.name.contains(x))
}
fn support_changing_quality(&self) -> bool {
["vaapi", "mediacodec"]
["vaapi", "mediacodec", "videotoolbox"]
.iter()
.all(|&x| !self.config.name.contains(x))
}
fn latency_free(&self) -> bool {
!self.config.name.contains("mediacodec")
["mediacodec", "videotoolbox"]
.iter()
.all(|&x| !self.config.name.contains(x))
}
fn is_hardware(&self) -> bool {
@@ -501,12 +503,12 @@ pub struct HwCodecConfig {
// portable: ui start check process, check process send to ui
// sciter and unilink: get from ipc server
impl HwCodecConfig {
#[cfg(any(target_os = "windows", target_os = "linux"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn set(config: String) {
let config = serde_json::from_str(&config).unwrap_or_default();
log::info!("set hwcodec config");
log::debug!("{config:?}");
#[cfg(windows)]
#[cfg(any(windows, target_os = "macos"))]
hbb_common::config::common_store(&config, "_hwcodec");
*CONFIG.lock().unwrap() = Some(config);
*CONFIG_SET_BY_IPC.lock().unwrap() = true;
@@ -578,7 +580,7 @@ impl HwCodecConfig {
..Default::default()
}
}
#[cfg(windows)]
#[cfg(any(windows, target_os = "macos"))]
{
let config = CONFIG.lock().unwrap().clone();
match config {
@@ -606,13 +608,13 @@ impl HwCodecConfig {
{
CONFIG.lock().unwrap().clone().unwrap_or_default()
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[cfg(target_os = "ios")]
{
HwCodecConfig::default()
}
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn get_set_value() -> Option<HwCodecConfig> {
let set = CONFIG_SET_BY_IPC.lock().unwrap().clone();
if set {
@@ -622,7 +624,7 @@ impl HwCodecConfig {
}
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn already_set() -> bool {
CONFIG_SET_BY_IPC.lock().unwrap().clone()
}
@@ -690,7 +692,7 @@ pub fn check_available_hwcodec() -> String {
serde_json::to_string(&c).unwrap_or_default()
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn start_check_process() {
if !enable_hwcodec_option() || HwCodecConfig::already_set() {
return;

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