Compare commits

...

245 Commits

Author SHA1 Message Date
fufesou
65f7541ec7 Fix. Compare cert blob before deleting it. (#7643)
* Fix. Compare cert blob before deleting it.

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

* Rename function name

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-07 14:31:09 +08:00
rustdesk
22da14e3f7 fix https://github.com/rustdesk/rustdesk/discussions/7628, https://github.com/rustdesk/rustdesk/discussions/7490 2024-04-07 11:31:51 +08:00
fufesou
fddad09167 Refact. Remove unused memset (#7633)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-06 23:19:50 +08:00
fufesou
f36f065508 Refact. Delete check reg fingerprint (#7631)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-06 22:58:42 +08:00
21pages
0c294eefae reordered peer tab (#7604)
* reordered peer tab

Signed-off-by: 21pages <pages21@163.com>

* opt peer tab visible menu, avoid checkbox value splash

Signed-off-by: 21pages <pages21@163.com>

---------

Signed-off-by: 21pages <pages21@163.com>
2024-04-06 17:53:03 +08:00
fufesou
5a0333ddaf Feat. msi (#7610)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-06 16:30:33 +08:00
Integral
e0b9a9a82f Update cn.rs (#7614) 2024-04-06 16:29:55 +08:00
fufesou
ab07eb6f4a Fix. Remove strange cert dir created by 1.2.3 (#7620)
* Fix. Remove strange cert dir created by 1.2.3

1. Remove `install_cert()`.
2. https://github.com/rustdesk/rustdesk/discussions/6444#discussioncomment-9017532

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

* comments

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-06 16:22:59 +08:00
Yevhen Popok
bddd800769 Update Ukrainian translation (#7611)
* Update Ukrainian translation

* Update Ukrainian translation

* Update Ukrainian translation
2024-04-06 16:21:11 +08:00
fufesou
5533ebf86a Fix. Remote toolbar, keyboard mode (#7626)
Move modeOnly to the scope it is needed.
Get modeOnly everytime of dropdown menu.

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-06 11:37:31 +08:00
fufesou
2397fdc495 Fix. Remote toolbar, menu refresh (#7605)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-05 09:28:35 +08:00
fufesou
129548764e fix build (#7597)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-03 22:20:56 +08:00
fufesou
ed257e39d0 Feat. msi (#7590)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-03 14:27:35 +08:00
rustdesk
33eaf2e05c bump to flutter 3.19.5 for wrong cpu instruction used in flutter 3.16 -> 3.19.4, which cause not running on some CPU 2024-04-03 14:25:09 +08:00
21pages
41d99d5108 fix forget password toast error when personal ab doesn't contain this id (#7589)
Signed-off-by: 21pages <pages21@163.com>
2024-04-02 23:18:52 +08:00
RustDesk
edefa5f786 Revert "Refact. CI, print sha256sum (#7580)" (#7587)
This reverts commit 2b20714f50.
2024-04-02 22:20:07 +08:00
21pages
d7b47b49d2 opt password sync, opt ab widgets (#7582)
* Opt sync conctrl with password source, add some comments
* For sync from recent, legacy ab remove forceRelay, rdpPort, rdpUsername,
  because it's not used, personal ab add sync hash
* Opt style of add Id dialog

Signed-off-by: 21pages <pages21@163.com>
2024-04-02 22:08:47 +08:00
fufesou
74af7ef8b2 Fix. Custom client, connection status (#7586)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-02 21:59:17 +08:00
redistay
1cfc9b6139 chore: fix some typos (#7585)
Signed-off-by: redistay <wujunjing@outlook.com>
2024-04-02 21:41:54 +08:00
fufesou
db6bf547a9 Feat. msi (#7584)
* Feat. msi

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

* remove ununsed file

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-02 18:10:09 +08:00
fufesou
2b20714f50 Refact. CI, print sha256sum (#7580)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-02 15:06:37 +08:00
FastAct
cfa72ad7d1 Update nl.rs (#7581) 2024-04-02 14:47:40 +08:00
fufesou
2e11a8b458 Fix. Linux, run_cmds, trim new line (#7579)
* Fix. Linux, run_cmds, trim new line

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

* add tests

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-02 14:47:13 +08:00
XLion
58fe95d6fd Uppercase API (#7577) 2024-04-02 11:44:27 +08:00
21pages
de65c8b2cf fix resolution restoration (#7572)
* Fix resolution restore. Screen resolution changes may be slow and cause errors in judgment. Remove the consideration of resolution changes caused by other processes.
* Keep the design unchange: when all connections end, revert to the resolution when there were no connections.
* Resolution menu use `scaledRect` for retina
* I can't reproduce the restoration failure of retina with 3rd software, but it maybe the same reason of slow resolution change.

Signed-off-by: 21pages <pages21@163.com>
2024-04-01 17:20:19 +08:00
rustdesk
57510980ed move pkill tray to execstop of the service for linux 2024-04-01 17:07:10 +08:00
hzhou
26a7f9dd46 kill tray in postrm of linux 2024-04-01 16:36:34 +08:00
hzhou
7fb2464d84 fix sysinfo update when id change, and add debug info for pk change 2024-04-01 16:21:05 +08:00
fufesou
9e7137ce09 Fix. Custom resolution removed after 2rd conn (#7571)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-01 13:27:52 +08:00
mehdi-song
80747fc306 Update fa.rs (#7566) 2024-03-31 21:37:22 +08:00
RustDesk
0d75f71d16 Update build-macos-arm64.yml 2024-03-30 18:27:23 +08:00
RustDesk
46c2288f50 Update build-macos-arm64.yml 2024-03-30 18:09:55 +08:00
RustDesk
d76c9ad1db Update build-macos-arm64.yml 2024-03-30 17:39:44 +08:00
RustDesk
3faa18c020 Update build-macos-arm64.yml 2024-03-30 17:19:03 +08:00
RustDesk
d699496a52 Update build-macos-arm64.yml 2024-03-30 17:13:23 +08:00
RustDesk
19bf5f1a23 Update build-macos-arm64.yml 2024-03-30 17:07:34 +08:00
RustDesk
080b56a0cb Update build-macos-arm64.yml 2024-03-30 16:55:28 +08:00
fufesou
82d9b033f2 Fix. Custom client, get app name (#7562)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-30 13:39:34 +08:00
FastAct
bf6e465970 Update nl.rs (#7554) 2024-03-29 19:43:06 +08:00
fufesou
3ef9824d8e Flutter web, custom cursor (#7545)
* Flutter web, custom cursor

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

* trivial changes

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

* Flutter web, custom cursor, use date after 'updateGetKey()'

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

* trivial changes

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-29 10:52:32 +08:00
21pages
4b0e88ce46 fix not saving cache after changing current address book (#7544)
and get address book mode from cache

Signed-off-by: 21pages <pages21@163.com>
2024-03-28 20:50:53 +08:00
21pages
0c32a889a9 fix ab cache not work, set current ab name after load cache (#7542)
Signed-off-by: 21pages <pages21@163.com>
2024-03-28 19:37:25 +08:00
rustdesk
6186b41e3f modify comment 2024-03-28 11:52:43 +08:00
fufesou
6e44a91d0b Refact. Flutter web desktop (#7539)
* Refact. Flutter web desktop

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

* Flutter web, prevent default context menu

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-28 11:38:11 +08:00
fufesou
810b980e6b Try fix, click tray icon, run main window (#7540)
* Try fix, click tray icon, run main window

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

* Remove start uni link on Windows, use 'run_me' instead

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-28 11:14:03 +08:00
21pages
22356940d9 ab show loading LinearProgressIndicator (#7538)
* Display the LinearProgressIndicator at the top of ab if the current ab is both empty and in a loading state.
* Add pull error like the legacy ab.
* When forcefully reading the 'ab' list, retrieve the personal ab guid to judge the current mode  to prevent server upgrades while the main window remains open

Signed-off-by: 21pages <pages21@163.com>
2024-03-27 21:28:21 +08:00
21pages
cc30f7aa02 handle boot args if initialLink is empty (#7537)
Signed-off-by: 21pages <pages21@163.com>
2024-03-27 15:51:30 +08:00
RustDesk
6e689400b6 Revert "macos support uni command "rustdesk rustdesk://xxxx" without mainwind…" (#7536)
This reverts commit 50fbf8833b.
2024-03-27 14:24:14 +08:00
21pages
50fbf8833b macos support uni command "rustdesk rustdesk://xxxx" without mainwindow (#7534)
Signed-off-by: 21pages <pages21@163.com>
2024-03-27 13:42:50 +08:00
fufesou
d7137990b9 Refact. Flutter web, peer cards (#7525)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-27 11:26:29 +08:00
flusheDData
8ecc107c06 New terms added (#7526)
* Update es.rs

New terms added

* Update es.rs

mistype correction

* Update es.rs

New term added

* Update es.rs

New terms added

* Update es.rs

New terms added

* Update es.rs

New terms added
2024-03-27 11:26:11 +08:00
rustdesk
1fa3837bb0 revert back to async_trait since having to use 1.75 because of win7
support
2024-03-27 03:36:33 +08:00
rustdesk
5aec5b084a refactor --service/--server start time check (my stupid to restart
--server)
2024-03-27 03:02:36 +08:00
rustdesk
b77add97db change to nightly-2023-10-13, the day of 1.2.3 2024-03-27 02:22:38 +08:00
rustdesk
b306d808fb change to nightly-2023-12-28, the day of 1.75.0 2024-03-27 02:13:25 +08:00
rustdesk
1729c3f76d have to use nightly for win32 2024-03-27 02:05:54 +08:00
rustdesk
2eb591391e fix ci 2024-03-27 01:48:34 +08:00
rustdesk
075194fea8 x86 rust toolchain 2024-03-27 01:45:17 +08:00
rustdesk
28b376d2d2 stick to rust 1.75 for win7/8, https://github.com/rustdesk/rustdesk/discussions/7503 2024-03-27 01:33:01 +08:00
crazeteam
4f3de4cf54 chore: fix some typos (#7518)
Signed-off-by: crazeteam <lilujing@outlook.com>
2024-03-27 01:08:56 +08:00
rustdesk
85f387bfb9 improve https://github.com/rustdesk/rustdesk/discussions/5631#discussioncomment-8916273 2024-03-27 00:38:13 +08:00
rustdesk
81aec8402b Temp solution for https://github.com/rustdesk/rustdesk/discussions/5631 2024-03-26 22:31:58 +08:00
rustdesk
d407dfed0a remove main.cc since not found the way to code sign it since g++ no ENABLE_HARDENED_RUNTIME 2024-03-26 21:27:02 +08:00
rustdesk
dcd4d0886e Close -> "Toggle tags" 2024-03-26 16:57:29 +08:00
rustdesk
9ad240951e fix uni link when mac service started, by use
applicationShouldOpenUntitledFile delegate
2024-03-26 14:11:02 +08:00
fufesou
e86d4657da Refact. Flutter web, key input (#7511)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-26 10:45:06 +08:00
rustdesk
d36e33a7cf rustdesk -> RustDesk in agent/daemon process since in App, the exe is
RustDesk rather than lowercase
2024-03-25 21:18:45 +08:00
21pages
376bcefc14 fix peer tab Tooltip persist via move it up (#7510)
Signed-off-by: 21pages <pages21@163.com>
2024-03-25 20:58:55 +08:00
21pages
c0c110fe7e opt pull ab (#7509)
* get ab settings only on new mode
* avoid pull dummpy current ab if list not initialized

Signed-off-by: 21pages <pages21@163.com>
2024-03-25 20:38:26 +08:00
rustdesk
3dc6e576df check isDisableAccount and isDisableAb before pull ab / group 2024-03-25 20:14:34 +08:00
21pages
49f717fcf8 opt pull ab (#7508)
1. Test legacy/new mode only upon logging out.
2. Avoid pulling all data unnecessarily:
* On startup: Retrieve list, current, and personal data.
* On refresh: Retrieve list and current data.
* On changing AB/switching tabs: Attempt to pull current data if not initialized.
3. Cache only personal and current AB.
4. Synchronize current AB from recent.
5. Remove AB loading CircularProgressIndicator.

Signed-off-by: 21pages <pages21@163.com>
2024-03-25 19:59:21 +08:00
rustdesk
00152e0db4 upload unsigned dmg since tar.gz does not work app failure 2024-03-25 15:43:18 +08:00
rustdesk
1964203848 fix unsigned mac 2024-03-25 15:04:34 +08:00
rustdesk
495444abd0 fix ci 2024-03-25 12:28:27 +08:00
rustdesk
09ec0b6325 fix https://github.com/rustdesk/rustdesk/discussions/7501 2024-03-25 12:15:51 +08:00
rustdesk
cad0f15a83 fix ci 2024-03-25 12:06:49 +08:00
rustdesk
b087dcd328 upload unsigned macos app 2024-03-25 11:21:01 +08:00
fufesou
8dff263a0c Refact. Flutter web, mid commit (#7502)
* Refact. Flutter web, mid commit

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

* Refact. Flutter web, mid commit

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-25 10:47:53 +08:00
XLion
d546b23bd8 Update translation (#7500) 2024-03-25 10:45:06 +08:00
solokot
bb9400745e Update ru.rs (#7496) 2024-03-25 10:43:01 +08:00
rustdesk
30f84e78e1 fix custom tray of mac 2024-03-24 22:17:40 +08:00
rustdesk
30af020ba8 fix config::is_incoming_only 2024-03-24 20:28:27 +08:00
rustdesk
ef27cd6c5c custom.txt and tray for mac 2024-03-24 20:09:37 +08:00
rustdesk
770289cd67 replace Assets.xcassets/AppIcon.appiconset with AppIcon.icns for custom
client
2024-03-24 15:23:24 +08:00
rustdesk
25b5c96648 remove default create-dmg in build.py 2024-03-24 14:04:05 +08:00
fufesou
85cafda168 Refact. Flutter web, mid commit (#7494)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-24 11:23:06 +08:00
21pages
96e1b5b0f6 remove deserializing config option using default config (#7491)
Signed-off-by: 21pages <pages21@163.com>
Co-authored-by: squarer3 <squalerblack@gmail.com>
2024-03-24 11:20:12 +08:00
fufesou
dd90e84f6a Fix. Peer tab, match name and icon (#7489)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-23 22:23:30 +08:00
rustdesk
05969c2f99 Hostname -> Server 2024-03-23 17:10:52 +08:00
fufesou
05ef1c3924 Feat. ci web (#7486)
* Feat. ci web

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

* Update README.md

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-03-23 14:28:25 +08:00
fufesou
8018920a64 Fix. Platform is MacOS and is Linux (#7485)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-23 10:27:17 +08:00
21pages
dcb7e389eb opt sytle of ab name (#7484)
Signed-off-by: 21pages <pages21@163.com>
2024-03-23 10:10:22 +08:00
fufesou
a15cd62fd4 Refact. Flutter web, mid commit (#7482)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-23 10:08:55 +08:00
rustdesk
1c3a2e475d safer handle_intranet 2024-03-23 00:56:18 +08:00
FastAct
83c45d772f Update nl.rs (#7475) 2024-03-22 23:36:59 +08:00
21pages
96c8d4de6a fix restore ab afer pullAb (#7480)
* Opt ab name drop downdrop, more width

Signed-off-by: 21pages <pages21@163.com>
2024-03-22 23:32:59 +08:00
fufesou
9558974080 Refact. Build flutter web (#7472)
* Refact. Build flutter web

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

* Refact. Flutter web, wrap Platform.xx

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-22 13:16:37 +08:00
21pages
285e298a8b fix ab tag flashing when refreshing (#7471)
* opt isFull, fix web console menu

Signed-off-by: 21pages <pages21@163.com>

* fix ab tag flashing when refreshing

Signed-off-by: 21pages <pages21@163.com>

---------

Signed-off-by: 21pages <pages21@163.com>
2024-03-22 11:02:22 +08:00
jxdv
57b17b1798 update cs.rs (#7467) 2024-03-22 09:53:37 +08:00
jxdv
7e13642098 update sk.rs (#7466)
* update sk.rs

* update pw warn
2024-03-22 09:53:21 +08:00
Mr-Update
26b5f53b12 Update de.rs (#7461) 2024-03-22 09:52:33 +08:00
bovirus
896b321cdc Italian language update (#7462) 2024-03-22 09:52:02 +08:00
Kleofass
4115a5936c Update lv.rs (#7460) 2024-03-22 09:51:46 +08:00
FastAct
0e0cf7df09 Update nl.rs (#7459) 2024-03-21 16:59:02 +08:00
21pages
0c232be0c4 opt web console menu text (#7452)
Signed-off-by: 21pages <pages21@163.com>
2024-03-21 14:44:13 +08:00
21pages
67bc26ed57 remove address book operation code, as it duplicates the the functionality of web console (#7451)
Signed-off-by: 21pages <pages21@163.com>
2024-03-21 14:01:18 +08:00
rustdesk
f421a14659 fix lang 2024-03-21 11:18:48 +08:00
RustDesk
81b74cf46c Update README.md 2024-03-21 11:17:09 +08:00
solokot
b1a3ce323b Update ru.rs (#7442) 2024-03-21 10:12:47 +08:00
Kleofass
1f58826444 Update lv.rs (#7440) 2024-03-21 10:12:33 +08:00
Nadahar
2ea853b1e6 Fixed CPU performance montor on non-English Windows (#7446)
Signed-off-by: Nadahar <Nadahar@users.noreply.github.com>
2024-03-21 10:12:17 +08:00
Daniel Marschall
a2e1647faa Update de.rs (#7447)
* Update de.rs

* Update de.rs
2024-03-21 10:08:43 +08:00
21pages
4e060203ad ab share rules, use user/group name rather than level (#7443)
Signed-off-by: 21pages <pages21@163.com>
2024-03-21 07:23:08 +08:00
RustDesk
0540e2cf6c Revert "missing quotes around MACOS_CODESIGN_IDENTIRY (#7416)" (#7438)
This reverts commit 794d3ffcac.
2024-03-20 21:20:39 +08:00
rustdesk
21e4679b6c improve tfa 2024-03-20 20:00:29 +08:00
RustDesk
8b9728b40b Revert "Fix how 2FA codes are verified (#7429)" (#7436)
This reverts commit e0739709e7.
2024-03-20 19:54:47 +08:00
p0358
e0739709e7 Fix how 2FA codes are verified (#7429)
Signed-off-by: p0358 <p0358@users.noreply.github.com>
2024-03-20 19:50:55 +08:00
21pages
41da6d552f shared address book (#7229)
Signed-off-by: 21pages <pages21@163.com>
2024-03-20 15:05:54 +08:00
solokot
ecb70b43df Update ru.rs (#7433) 2024-03-20 11:28:10 +08:00
FastAct
b2290f5b4c Update nl.rs (#7426) 2024-03-19 18:53:35 +08:00
Lee Jong Mun
c6f415e9df kor translation update (#7424) 2024-03-19 18:53:08 +08:00
XLion
3f7d622512 Small fix for langs (#7418) 2024-03-19 18:52:36 +08:00
Ronan LE MEILLAT
794d3ffcac missing quotes around MACOS_CODESIGN_IDENTIRY (#7416) 2024-03-19 18:51:33 +08:00
RustDesk
ee2f87ef32 Update build-macos-arm64.yml 2024-03-17 11:28:12 +08:00
rustdesk
87da2a165d fix ci 2024-03-17 11:20:25 +08:00
rustdesk
130deb8e22 add build arm64 macos 2024-03-17 11:17:23 +08:00
rustdesk
b65d65cb12 fix ci 2024-03-17 00:56:59 +08:00
Kleofass
3caa5776c8 Update lv.rs (#7408) 2024-03-16 22:38:08 +08:00
rustdesk
6cbbb6498f init_log once 2024-03-16 22:20:50 +08:00
rustdesk
fb12ee7f2a fix loadLogo 2024-03-16 21:55:21 +08:00
bovirus
48842b6f2a Update Italian language (#7407) 2024-03-16 17:41:26 +08:00
fufesou
fc90526258 Fix. Custom client, maxmize of incoming only (#7406)
* Fix. Custom client, maxmize of incoming only

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

* format

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-16 17:33:58 +08:00
fufesou
efd1f24ab2 Fix. Custom client, ui bugs (#7405)
* Fix. Custom client, ui bugs

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

* trivial

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-16 15:37:23 +08:00
rustdesk
0808672b6e modify warn 2024-03-16 11:59:12 +08:00
rustdesk
fe0de47805 preset password warning 2024-03-16 11:52:48 +08:00
jxdv
ccf5b0ef53 update cs.rs (#7404) 2024-03-16 10:13:12 +08:00
jxdv
218f4872b8 update sk.rs (#7403) 2024-03-16 10:12:12 +08:00
rustdesk
077569dd5d better option save 2024-03-16 01:51:16 +08:00
rustdesk
a3f79065c5 better error handle in job.py 2024-03-16 00:30:30 +08:00
rustdesk
fbfcf862cb add divider of statusbar back and add quit button for qs 2024-03-16 00:24:18 +08:00
RustDesk
7307b84dad Update flutter-build.yml 2024-03-15 23:36:03 +08:00
Daniel Marschall
6b15a9b6d2 Made censoring in screenshot nicer (Fixes #7398) (#7399) 2024-03-15 22:20:43 +08:00
fufesou
e3420fd563 Refact. Linux, x11, user --server (#7400)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-15 22:18:14 +08:00
RustDesk
31120f51c9 Update flutter-build.yml 2024-03-15 18:07:17 +08:00
fufesou
3d279ec127 fix build (#7397)
* fix build

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

* fmt

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-15 17:46:36 +08:00
fufesou
48cd76b938 Fix. Linux run commands, getent passwd (#7396)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-15 17:26:33 +08:00
fufesou
8e5fbc8480 Fix. Use rounded (seconds) timestamp for test (#7394)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-15 14:24:22 +08:00
bovirus
cdea968c24 Add files via upload (#7393)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-03-15 10:19:46 +08:00
DeDuplicate
a6aabed6a1 add hebrew translation (#7391)
* add hebrew translation

* Update lang.rs

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-03-15 10:18:08 +08:00
Mr-Update
cde235896f Update de.rs (#7390)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-03-15 10:16:24 +08:00
jxdv
ca48aaf5ee update cs.rs (#7388)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-03-15 10:14:44 +08:00
jxdv
8a705a6a9a update sk.rs (#7387)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-03-15 10:14:03 +08:00
rustdesk
b3adbc1110 move outgoing_only_desk_tip to end 2024-03-15 10:12:58 +08:00
rustdesk
2bbc13ccf6 adjust logo and statusbar 2024-03-15 09:04:49 +08:00
fufesou
411384f0e7 Fix. MacOS, incoming only, restore pos (#7392)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-15 08:42:41 +08:00
rustdesk
a9b0bc0409 fix some minors of custom client 2024-03-15 00:26:53 +08:00
shuangcui
228582296c chore: remove repetitive word (#7386)
Signed-off-by: shuangcui <fliter@qq.com>
2024-03-14 21:36:55 +08:00
XLion
d67108cd20 Update translation (#7385)
Add translation for tw and add `to` to the sentence to make it more clear.
2024-03-14 21:33:08 +08:00
rustdesk
175f185cb1 fix https://github.com/rustdesk/rustdesk/discussions/7381 2024-03-14 20:38:57 +08:00
rustdesk
56f27ec356 fix ci 2024-03-14 11:45:16 +08:00
fufesou
5253d67e04 Fix/custom client styles (#7373)
* Fix. qs styles

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

* custom client, options

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

* Move logo.svg to icon.svg

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

* Refact. Custom client, connection status ui.

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

* Custom client ui. Disable settings, hide "Change password"

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

* Custom client, logo align center

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

* Custom client, refact, outgoing ui

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

* Custom client, outgoing, settings icon

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

* Custom client, powered by RustDesk

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

* Custom client, remove unused SizeBox

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

* Update config.rs

* Update flutter_ffi.rs

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-03-14 11:36:14 +08:00
rustdesk
a314f59db9 fix https://github.com/rustdesk/rustdesk/issues/7377 2024-03-14 11:03:47 +08:00
rustdesk
da4e268e63 fix job 2024-03-14 06:59:56 +08:00
XLion
c186378d43 Correct copyright year (#7375)
* Correct copyright year

* Correct name
2024-03-13 21:12:04 +08:00
rustdesk
482bb5f64f fix CI 2024-03-13 09:41:28 +08:00
n-connect
3c1492e9bc Update hu.rs - fixing ID Server and others (#7366)
After a failed client setup in Network settings - due to wrong translation
2024-03-12 23:19:23 +08:00
rustdesk
be301b825f load tray icon from asset 2024-03-12 22:13:28 +08:00
rustdesk
fc875f7ad6 more on custom client 2024-03-12 21:47:29 +08:00
rustdesk
f644053811 fix hostname for mac 2024-03-11 01:36:43 +08:00
rustdesk
c0e5e78d2b no sign for build ci 2024-03-10 15:49:07 +08:00
rustdesk
d103872c61 avoid upload debug file 2024-03-10 15:39:49 +08:00
rustdesk
d47104f3f3 new sign 2024-03-10 15:06:49 +08:00
rustdesk
d4288f89d3 fix ci 2024-03-10 14:03:28 +08:00
rustdesk
7060257051 Remove some arc, and add some custom client 2024-03-10 12:48:00 +08:00
Dimitris Apostolou
866ec097c0 fix: fix crate vulnerabilities (#7344) 2024-03-09 17:52:18 +08:00
21pages
dcbe2805e6 improve waiting for image when only one image received (#7340)
* fix padding of mobile server page PopupMenuItem

Signed-off-by: 21pages <pages21@163.com>

* improve waiting for image when only one image received

* For flutter texture late creation: create texture between session add and session start, it works not in principle but in test.
* For late PeerInfo handling
	a. rgba texture render: allow zero size in on_rgba
	b. gpu texture render and rgba buffer render doesn't check size currently
* Fix wrong valid flag of first frame in rgba texture render

Other issues:
* decodeImageFromPixels may fail on first image
* Correct width/height when resolution changes

Signed-off-by: 21pages <pages21@163.com>

---------

Signed-off-by: 21pages <pages21@163.com>
2024-03-08 17:24:02 +08:00
rustdesk
262814391a fix ci 2024-03-08 00:47:21 +08:00
rustdesk
b32cf403e6 overwrite config 2024-03-08 00:22:52 +08:00
rustdesk
921ee34779 unify winres 2024-03-07 17:00:32 +08:00
rustdesk
75a871c437 format 2024-03-07 16:37:51 +08:00
rustdesk
b5a47efe8e remove share rdp from strategy, useless and leading to confusion 2024-03-07 16:33:46 +08:00
Andrzej Rudnik
8ff24d876c Update pl.rs (#7329) 2024-03-07 14:09:09 +08:00
21pages
dd44bb2a0c flutter windows main.cpp get app name from rust (#7316)
Signed-off-by: 21pages <pages21@163.com>
2024-03-06 17:57:05 +08:00
rustdesk
f3686b23c7 fix tw 2024-03-06 17:38:33 +08:00
XLion
fccdc8ca1f Fix lang detection for zh-TW (#7326)
It is ``TW``, not ``tw``
2024-03-06 17:35:40 +08:00
rustdesk
fc149666d9 remove icon.rc and add compression level 2024-03-05 23:22:15 +08:00
jxdv
4292838e80 update sk.rs (#7317)
* fix typo

* update translations
2024-03-05 11:59:38 +08:00
fufesou
befac94473 Fix. macos, workaround fn (#7319)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-03-05 11:31:41 +08:00
rustdesk
c0128d86f4 fix ci 2024-03-04 22:55:25 +08:00
rustdesk
794636e208 use winres in portable 2024-03-04 22:19:18 +08:00
rustdesk
eff6ac254f improve generate.py 2024-03-04 21:16:14 +08:00
rustdesk
fa575be289 fix ci 2024-03-04 19:53:17 +08:00
rustdesk
060ad68570 add rustdesk.tar.gz to ci 2024-03-04 18:12:34 +08:00
21pages
1620394736 fix windows ci, downgrade flutter-action (#7312)
Signed-off-by: 21pages <pages21@163.com>
2024-03-04 09:17:56 +08:00
RustDesk
1e6e91e4bd Update clear-cache.yml 2024-03-03 22:29:31 +08:00
rustdesk
b0cbe2ae70 upgrade clear-acache 2024-03-03 22:18:49 +08:00
rustdesk
1391202985 clear-cache action 2024-03-03 22:17:00 +08:00
mehdi-song
d9ad24f563 Update fa.rs (#7309)
;-)
For all those who live in the world
2024-03-03 19:38:03 +08:00
21pages
61279752e2 upgrade uni link desktop (#7307)
* fix macos uni not work since flutter 3.16, upgrade uni_links_desktop

Signed-off-by: 21pages <pages21@163.com>

* Connect when clicking enter (drop-down is not considered)

Signed-off-by: 21pages <pages21@163.com>

---------

Signed-off-by: 21pages <pages21@163.com>
2024-03-03 17:31:21 +08:00
rustdesk
1794b65182 remove devcontainer from readme, since it is not maintained and not good
for use
2024-03-01 15:50:25 +08:00
rustdesk
6bb5629bb4 update readme 2024-03-01 15:44:19 +08:00
rustdesk
0f3db29e75 fix https://github.com/rustdesk/rustdesk/discussions/7045 2024-02-29 11:27:21 +08:00
Yevhen Popok
c6cdf77a68 Update Ukrainian UI translation (#7288) 2024-02-29 09:57:48 +08:00
XLion
48682faf71 Update tw.rs (#7287) 2024-02-28 22:52:13 +08:00
21pages
49d396538c port forward, wait until TestDelay is replied (#7282)
* Revert "fix rdp not work, caused by test_delay_timer (#7274)"

This reverts commit 7fca0ba588.

* port forward, wait until TestDelay is replied

Signed-off-by: 21pages <pages21@163.com>

---------

Signed-off-by: 21pages <pages21@163.com>
2024-02-28 12:18:13 +08:00
flusheDData
30802820b0 new terms (#7275)
* Update es.rs

New terms added

* Update es.rs

mistype correction

* Update es.rs

New term added

* Update es.rs

New terms added

* Update es.rs

New terms added
2024-02-28 09:12:16 +08:00
21pages
7fca0ba588 fix rdp not work, caused by test_delay_timer (#7274)
* fix rdp not work, caused by test_delay_timer

Signed-off-by: 21pages <pages21@163.com>

* Update connection.rs

---------

Signed-off-by: 21pages <pages21@163.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-02-28 00:11:02 +08:00
21pages
50d080d098 enable retina scale factor (#7269)
* enable retina scale factor

* enabled only when there are only one video service running
* scale mouse event
* scale cursor position
* scale remote menu display button
* adjust resolution

Signed-off-by: 21pages <pages21@163.com>

* Update server.rs

---------

Signed-off-by: 21pages <pages21@163.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-02-27 22:28:23 +08:00
fufesou
96792bec78 Refact. Qs, do not check qs on startup. (#7272)
Process will not exit if early return on the flutter side.

core_main.rs has checked qs already.

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-02-27 17:29:12 +08:00
fufesou
e6953c8883 Feat. Quick support, ui (#7267)
* Feat. QS ui

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

* Remove 'Quick support'

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

* add help card

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

* use addPostFrameCallback to get child size

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

* Fix. qs, set home window size

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

* Qs, set setResizable for settings page

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

* Qs, help cards margin bottom

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

* Qs, online status, padding

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

* Qs, online status, padding

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

* Qs, online status, use margin instead of padding

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

* Qs, fix, start cm window

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-02-27 17:02:10 +08:00
21pages
1d9b1781d4 update runas to 1.2.0 (#7271)
Signed-off-by: 21pages <pages21@163.com>
2024-02-27 15:42:35 +08:00
rustdesk
6720bb9f6b fix ci 2024-02-26 19:01:42 +08:00
rustdesk
2217e5303d fix ci 2024-02-26 18:12:28 +08:00
rustdesk
c8392a714c license.rs -> custom_server.rs to remove misunderstanding 2024-02-26 18:05:54 +08:00
rustdesk
749a1d2da2 support logo.png 2024-02-26 17:49:12 +08:00
21pages
2920e33d71 fix is_physical_console_session (#7266)
Signed-off-by: 21pages <pages21@163.com>
2024-02-26 13:34:26 +08:00
21pages
1bc11a207a fix rdp session ctrl+alt+delete (#7258)
Signed-off-by: 21pages <pages21@163.com>
2024-02-26 13:22:21 +08:00
fufesou
ddbd4e79a2 Refact. ci, third party, upload artifact (#7265)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-02-26 10:25:45 +08:00
fufesou
49c107bb5f Refact/ci remove third party topmost window (#7263)
* Refact. Remove third-party prebuilt dep, topmost window

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

* fix ci

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

* Update third-party-RustDeskTempTopMostWindow.yml

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-02-26 02:24:00 +08:00
fufesou
1b99d28c9b Refact. build.py, skip portable packing (#7264)
ci and build.py do the packing.

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-02-26 02:20:37 +08:00
21pages
7b8e983412 fix calling getWindowName before bind init (#7259)
Signed-off-by: 21pages <pages21@163.com>
2024-02-26 00:02:42 +08:00
rustdesk
f1373234dd remove useless is_rdp_service_open 2024-02-25 20:10:36 +08:00
21pages
b511a52e09 remove is_rdp_service_open judgement for option share rdp (#7257)
Signed-off-by: 21pages <pages21@163.com>
2024-02-25 20:09:30 +08:00
rustdesk
4c8c2aa323 translate rustdesk 2024-02-25 15:06:55 +08:00
rustdesk
a28c9f8f36 remove more "rustdesk" built-in 2024-02-25 13:29:06 +08:00
fufesou
9ff1dfe019 Test. More comment (#7251)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-02-24 23:03:56 +08:00
fufesou
3ae52dacfc test RustDeskInterval with interval_at (#7249)
* test RustDeskInterval with interval_at

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

* Test. RustDeskInterval, to tokio interval

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

* Add comment

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-02-24 22:44:46 +08:00
rustdesk
aa6f879504 fix config string in exe file name 2024-02-24 20:36:18 +08:00
fufesou
1dde5c5689 Refact. get_version_number support '- patch version' (#7197)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-02-24 20:11:27 +08:00
FastAct
913b422d74 Update nl.rs (#7248) 2024-02-24 17:08:54 +08:00
fufesou
2459bcd206 Fix. Multi sub windows, sync peer options (#7247)
* Fix. Multi window, sync peer options

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

* Remove unused `use`

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-02-23 22:49:53 +08:00
wangweijie
cce1ce0ee0 Add loongarch64 support (#7244)
Signed-off-by: wangweijie <wangweijie@loongson.cn>
2024-02-23 20:08:35 +08:00
21pages
8518513aff fix haredware codec memory leak (#7240)
Signed-off-by: 21pages <pages21@163.com>
2024-02-23 13:33:01 +08:00
jxdv
ce5e28c543 update cs.rs (#7234) 2024-02-23 10:15:53 +08:00
jxdv
9b7a8203f6 update sk.rs (#7233) 2024-02-23 10:14:49 +08:00
solokot
fd8f2422ea Update ru.rs (#7232) 2024-02-23 00:51:10 +08:00
fufesou
09325fadaf Fix. Text clipboard is not working correctly (#7226)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-02-22 13:44:44 +08:00
Mr-Update
d7d7be9366 Update de.rs (#7225) 2024-02-22 11:44:16 +08:00
fufesou
25afdda2b2 Fix/arboard clipboard context timeout (#7217)
* Fix. Set custom timeout for arboard clipboard

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

* fix build

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

* Do not block tokio runtime

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

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-02-21 22:05:27 +08:00
bovirus
c690d5e940 Italian language update (#7211) 2024-02-21 11:30:06 +08:00
Kleofass
243a8b60b1 Update lv.rs (#7210) 2024-02-21 11:29:54 +08:00
21pages
7b317619ac fix windows ci wrong sign directory since update to flutter 3.16.9 (#7209)
Signed-off-by: 21pages <pages21@163.com>
2024-02-20 21:05:19 +08:00
grummbeer
c987bed965 Automate year of copyright (#7206) 2024-02-20 18:00:07 +08:00
NicKoehler
ada4bafb94 Update italian readme (#7205)
* updated it readme

* fix link

* fix other links
2024-02-20 17:28:49 +08:00
fufesou
27f5d0fd54 Fix. Remove unused idd feature in ci. (#7198)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-02-20 03:19:38 +08:00
251 changed files with 12674 additions and 3490 deletions

61
.github/workflows/build-macos-arm64.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Flutter Nightly MacOS Arm64 Build
on:
#schedule:
# schedule build every night
# - cron: "0/6 * * * *"
workflow_dispatch:
env:
CARGO_NDK_VERSION: "3.1.2"
LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.16.9"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
FLUTTER_ELINUX_COMMIT_ID: "c02bd16e1630f5bd690b85c5c2456ac1920e25af"
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2023.10.19
# for multiarch gcc compatibility
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
VERSION: "1.2.4"
NDK_VERSION: "r26b"
#signing keys env variable checks
ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}'
MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}'
# To make a custom build with your own servers set the below secret values
RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}'
RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}'
API_SERVER: '${{ secrets.API_SERVER }}'
UPLOAD_ARTIFACT: true
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs:
build-for-macOS-arm64:
name: build-for-macOS-arm64
runs-on: [self-hosted, macOS, ARM64]
steps:
#- name: Import the codesign cert
# if: env.MACOS_P12_BASE64 != null
# uses: apple-actions/import-codesign-certs@v1
# continue-on-error: true
# 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: Run
shell: bash
run: |
cd /opt/build
#./update_mac_template.sh
#security default-keychain -s rustdesk.keychain
#security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
./agent.sh

37
.github/workflows/clear-cache.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Clear cache
on:
workflow_dispatch:
permissions:
actions: write
jobs:
clear-cache:
runs-on: ubuntu-latest
steps:
- name: Clear cache
uses: actions/github-script@v7
with:
script: |
console.log("About to clear")
const caches = await github.rest.actions.getActionsCacheList({
owner: context.repo.owner,
repo: context.repo.repo,
})
for (const cache of caches.data.actions_caches) {
console.log(cache)
github.rest.actions.deleteActionsCacheById({
owner: context.repo.owner,
repo: context.repo.repo,
cache_id: cache.id,
})
}
console.log("Clear completed")
- name: Purge cache # Above seems not clear thouroughly, so add this to double clear
uses: MyAlbum/purge-cache@v2
with:
accessed: true # Purge caches by their last accessed time (default)
created: false # Purge caches by their created time (default)
max-age: 1 # in seconds

View File

@@ -11,9 +11,10 @@ on:
default: "nightly"
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.16.9"
FLUTTER_VERSION: "3.19.5"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
@@ -26,17 +27,30 @@ env:
VERSION: "1.2.4"
NDK_VERSION: "r26b"
#signing keys env variable checks
ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}'
MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}'
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
# To make a custom build with your own servers set the below secret values
RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}'
RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}'
API_SERVER: '${{ secrets.API_SERVER }}'
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
API_SERVER: "${{ secrets.API_SERVER }}"
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs:
build-RustDeskTempTopMostWindow:
uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml
with:
upload-artifact: ${{ inputs.upload-artifact }}
target: windows-2019
configuration: Release
platform: x64
target_version: Windows10
strategy:
fail-fast: false
build-for-windows-flutter:
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
needs: [build-RustDeskTempTopMostWindow]
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
@@ -63,26 +77,18 @@ jobs:
version: ${{ env.LLVM_VERSION }}
- name: Install flutter
uses: subosito/flutter-action@v2
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Replace engine with rustdesk custom flutter engine
run: |
flutter doctor -v
flutter precache --windows
Invoke-WebRequest -Uri https://github.com/fufesou/flutter-engine/releases/download/bugfix-subwindow-crash-3.16.9-apply-pull-47787/windows-x64-release.zip -OutFile windows-x64-flutter-release.zip
Expand-Archive windows-x64-flutter-release.zip -DestinationPath .
mv -Force windows-x64-release/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: ''
components: "rustfmt"
- uses: Swatinem/rust-cache@v2
with:
@@ -107,7 +113,7 @@ jobs:
shell: bash
- name: Build rustdesk
run: python3 .\build.py --portable --hwcodec --flutter --gpucodec --feature IddDriver
run: python3 .\build.py --portable --hwcodec --flutter --gpucodec --skip-portable-pack
- name: find Runner.res
# Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res
@@ -125,37 +131,42 @@ jobs:
ls -l ./libs/portable/Runner.res;
fi
- name: Sign rustdesk files
uses: GermanBluefox/code-sign-action@v7
if: false # env.UPLOAD_ARTIFACT == 'true'
- name: Download RustDeskTempTopMostWindow artifacts
uses: actions/download-artifact@master
if: ${{ inputs.upload-artifact }}
with:
certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}'
password: '${{ secrets.WINDOWS_PFX_PASSWORD }}'
certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}'
# certificatename: '${{ secrets.CERTNAME }}'
folder: './flutter/build/windows/runner/Release/'
recursive: true
name: topmostwindow-artifacts
path: "./flutter/build/windows/x64/runner/Release/"
- name: Compress unsigned
shell: bash
run: |
mv ./flutter/build/windows/x64/runner/Release ./rustdesk
tar czf rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-unsigned.tar.gz rustdesk
- name: Sign rustdesk files
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
shell: bash
run: |
pip3 install requests argparse
BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./rustdesk/
- name: Build self-extracted executable
shell: bash
if: env.UPLOAD_ARTIFACT == 'true'
run: |
pushd ./libs/portable
python3 ./generate.py -f ../../flutter/build/windows/x64/runner/Release/ -o . -e ../../flutter/build/windows/x64/runner/Release/rustdesk.exe
pip3 install -r requirements.txt
python3 ./generate.py -f ../../rustdesk/ -o . -e ../../rustdesk/rustdesk.exe
popd
mkdir -p ./SignOutput
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.exe
- name: Sign rustdesk self-extracted file
uses: GermanBluefox/code-sign-action@v7
if: false #env.UPLOAD_ARTIFACT == 'true'
with:
certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}'
password: '${{ secrets.WINDOWS_PFX_PASSWORD }}'
certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}'
# certificatename: '${{ secrets.WINDOWS_PFX_NAME }}'
folder: './SignOutput'
recursive: false
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
shell: bash
run: |
BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput
- name: Publish Release
uses: softprops/action-gh-release@v1
@@ -165,13 +176,14 @@ jobs:
tag_name: ${{ env.TAG_NAME }}
files: |
./SignOutput/rustdesk-*.exe
./rustdesk-*.tar.gz
# The fallback for the flutter version, we use Sciter for 32bit Windows.
build-for-windows-sciter:
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
runs-on: ${{ matrix.job.os }}
# Temporarily disable this action due to additional test is needed.
# if: false
# if: false
strategy:
fail-fast: false
matrix:
@@ -199,9 +211,9 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: nightly-${{ matrix.job.target }}
toolchain: nightly-2023-10-13-${{ matrix.job.target }} # must use nightly here, because of abi_thiscall feature required
targets: ${{ matrix.job.target }}
components: ''
components: "rustfmt"
- uses: Swatinem/rust-cache@v2
with:
@@ -248,15 +260,11 @@ jobs:
fi
- name: Sign rustdesk files
uses: GermanBluefox/code-sign-action@v7
if: false #env.UPLOAD_ARTIFACT == 'true'
with:
certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}'
password: '${{ secrets.WINDOWS_PFX_PASSWORD }}'
certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}'
# certificatename: '${{ secrets.CERTNAME }}'
folder: './Release/'
recursive: true
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
shell: bash
run: |
pip3 install requests argparse
BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./Release/
- name: Build self-extracted executable
shell: bash
@@ -267,17 +275,14 @@ jobs:
popd
mkdir -p ./SignOutput
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.exe
mv ./Release ./rustdesk
tar czf rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.tar.gz rustdesk
- name: Sign rustdesk self-extracted file
uses: GermanBluefox/code-sign-action@v7
if: false #env.UPLOAD_ARTIFACT == 'true'
with:
certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}'
password: '${{ secrets.WINDOWS_PFX_PASSWORD }}'
certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}'
# certificatename: '${{ secrets.WINDOWS_PFX_NAME }}'
folder: './SignOutput'
recursive: false
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
shell: bash
run: |
BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/
- name: Publish Release
uses: softprops/action-gh-release@v1
@@ -287,6 +292,79 @@ jobs:
tag_name: ${{ env.TAG_NAME }}
files: |
./SignOutput/rustdesk-*.exe
./rustdesk-*.tar.gz
build-for-macOS-arm64:
name: build-for-macOS-arm64
runs-on: [self-hosted, macOS, ARM64]
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
- name: Install flutter rust bridge deps
shell: bash
run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
- name: Build rustdesk
run: |
# --hwcodec not supported on macos yet
./build.py --flutter
- name: create unsigned dmg
if: env.UPLOAD_ARTIFACT == 'true'
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 }}-arm64.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
- name: Upload unsigned macOS app
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@master
with:
name: rustdesk-unsigned-macos-arm64
path: rustdesk-${{ env.VERSION }}-arm64.dmg # can not upload the directory directly or tar.gz file, which destroy the link structure, causing the codesign failed
- name: Codesign app and create signed dmg
if: env.MACOS_P12_BASE64 != null && env.UPLOAD_ARTIFACT == 'true'
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"
# 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 ~/.p12/api-key.json --staple rustdesk-${{ env.VERSION }}.dmg
- name: Rename rustdesk
if: env.UPLOAD_ARTIFACT == 'true'
run: |
for name in rustdesk*??.dmg; do
mv "$name" "${name%%.dmg}-aarch64.dmg"
done
- name: Publish DMG package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
rustdesk*-aarch64.dmg
build-for-macOS:
name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}]
@@ -299,7 +377,7 @@ jobs:
target: x86_64-apple-darwin,
os: macos-latest,
extra-build-args: "",
arch: x86_64
arch: x86_64,
}
steps:
- name: Export GitHub Actions cache environment variables
@@ -358,9 +436,9 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: ''
components: "rustfmt"
- uses: Swatinem/rust-cache@v2
with:
@@ -397,6 +475,21 @@ jobs:
# --hwcodec not supported on macos yet
./build.py --flutter ${{ matrix.job.extra-build-args }}
- name: create unsigned dmg
if: env.UPLOAD_ARTIFACT == 'true'
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 }}-x64.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
- name: Upload unsigned macOS app
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@master
with:
name: rustdesk-unsigned-macos-x64
path: rustdesk-${{ env.VERSION }}-x64.dmg # can not upload the directory directly or tar.gz, which destroy the link structure, causing the codesign failed
- name: Codesign app and create signed dmg
if: env.MACOS_P12_BASE64 != null && env.UPLOAD_ARTIFACT == 'true'
run: |
@@ -408,7 +501,7 @@ jobs:
security default-keychain -s rustdesk.keychain
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
# start sign the rustdesk.app and dmg
rm rustdesk-${{ env.VERSION }}.dmg || true
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
@@ -431,6 +524,36 @@ jobs:
files: |
rustdesk*-${{ matrix.job.arch }}.dmg
publish_unsigned_mac:
needs:
- build-for-macOS
- build-for-macOS-arm64
runs-on: ubuntu-latest
if: ${{ inputs.upload-artifact }}
steps:
- name: Download artifacts
uses: actions/download-artifact@master
with:
name: rustdesk-unsigned-macos-x64
path: ./
- name: Download Artifacts
uses: actions/download-artifact@master
with:
name: rustdesk-unsigned-macos-arm64
path: ./
- name: Combine unsigned macos app
run: |
tar czf rustdesk-${{ env.VERSION }}-macos-unsigned.tar.gz *.dmg
- name: Publish unsigned macos app
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: rustdesk-${{ env.VERSION }}-macos-unsigned.tar.gz
generate-bridge-linux:
uses: ./.github/workflows/bridge.yml
@@ -480,9 +603,9 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: ''
components: "rustfmt"
- uses: Swatinem/rust-cache@v2
with:
@@ -538,14 +661,14 @@ jobs:
target: aarch64-linux-android,
os: ubuntu-20.04,
extra-build-features: "",
openssl-arch: android-arm64
openssl-arch: android-arm64,
}
- {
arch: armv7,
target: armv7-linux-androideabi,
os: ubuntu-20.04,
extra-build-features: "",
openssl-arch: android-arm
openssl-arch: android-arm,
}
steps:
- name: Export GitHub Actions cache environment variables
@@ -632,8 +755,8 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
components: ''
toolchain: ${{ env.RUST_VERSION }}
components: "rustfmt"
- uses: Swatinem/rust-cache@v2
with:
@@ -747,21 +870,21 @@ jobs:
target: x86_64-unknown-linux-gnu,
os: ubuntu-20.04,
extra-build-features: "",
enable-headless: true
enable-headless: true,
}
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-20.04,
extra-build-features: "flatpak",
enable-headless: false
enable-headless: false,
}
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-20.04,
extra-build-features: "appimage",
enable-headless: false
enable-headless: false,
}
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
steps:
@@ -796,9 +919,9 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: ''
components: "rustfmt"
- name: Save Rust toolchain version
run: |
@@ -935,7 +1058,7 @@ jobs:
os: ubuntu-20.04, # just for naming package, not running host
use-cross: true,
extra-build-features: "",
enable-headless: true
enable-headless: true,
}
- {
arch: aarch64,
@@ -943,7 +1066,7 @@ jobs:
os: ubuntu-20.04, # just for naming package, not running host
use-cross: true,
extra-build-features: "appimage",
enable-headless: false
enable-headless: false,
}
# - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" }
# - {
@@ -1005,9 +1128,9 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: ''
components: "rustfmt"
- name: Save Rust toolchain version
run: |
@@ -1144,7 +1267,7 @@ jobs:
os: ubuntu-latest,
use-cross: true,
extra-build-features: "",
enable-headless: true
enable-headless: true,
}
# - { arch: armv7, target: armv7-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "appimage" }
# - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
@@ -1197,9 +1320,9 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: ''
components: "rustfmt"
- name: Save Rust toolchain version
run: |
@@ -1816,7 +1939,7 @@ jobs:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
res/rustdesk*.zst
res/rustdesk-${{ env.VERSION }}*.zst
- name: Build appimage package
if: matrix.job.extra-build-features == 'appimage' && env.UPLOAD_ARTIFACT == 'true'
@@ -2001,3 +2124,69 @@ jobs:
tag_name: ${{ env.TAG_NAME }}
files: |
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak
build-rustdesk-web:
name: build-rustdesk-web
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
job:
- { arch: x86_64, target: x86_64-unknown-linux-gnu, os: ubuntu-18.04 }
env:
RELEASE_NAME: web-basic
steps:
- name: Checkout source code
uses: actions/checkout@v3
- name: Prepare env
run: |
sudo apt-get update -y
sudo apt-get install -y wget npm
- name: Install flutter
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
# https://rustdesk.com/docs/en/dev/build/web/
- name: Build web
shell: bash
run: |
pushd flutter/web/js
npm install yarn -g
npm install typescript -g
npm install protoc -g
# Install protoc first, see: https://google.github.io/proto-lens/installing-protoc.html
npm install ts-proto
# Only works with vite <= 2.8, see: https://github.com/vitejs/vite/blob/main/docs/guide/build.md#chunking-strategy
npm install vite@2.8
yarn install && yarn build
popd
pushd flutter/web
wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/web_deps.tar.gz
tar xzf web_deps.tar.gz
popd
pushd flutter
flutter build web --release
cd build
cp ../web/README.md web
# TODO: Remove the following line when the web is almost complete.
echo -e "\n\nThis build is for preview and not full functionality." >> web/README.md
dir_name="rustdesk-${{ env.VERSION }}-${{ env.RELEASE_NAME }}"
mv web "${dir_name}" && tar czf "${dir_name}".tar.gz "${dir_name}"
sha256sum "${dir_name}".tar.gz
popd
- name: Publish web
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
flutter/build/rustdesk-${{ env.VERSION }}-${{ env.RELEASE_NAME }}.tar.gz

View File

@@ -31,7 +31,7 @@ jobs:
version: ${{ env.LLVM_VERSION }}
- name: Install flutter
uses: subosito/flutter-action@v2
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -64,7 +64,7 @@ jobs:
shell: bash
- name: Build rustdesk
run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver
run: python3 .\build.py --portable --hwcodec --flutter
- name: Build self-extracted executable
shell: bash

View File

@@ -0,0 +1,60 @@
name: build RustDeskTempTopMostWindow
on:
workflow_call:
inputs:
upload-artifact:
type: boolean
default: true
target:
description: 'Target'
required: true
type: string
default: 'windows-2019'
configuration:
description: 'Configuration'
required: true
type: string
default: 'Release'
platform:
description: 'Platform'
required: true
type: string
default: 'x64'
target_version:
description: 'TargetVersion'
required: true
type: string
default: 'Windows10'
env:
project_path: WindowInjection/WindowInjection.vcxproj
jobs:
build-RustDeskTempTopMostWindow:
runs-on: ${{ inputs.target }}
strategy:
fail-fast: false
env:
build_output_dir: RustDeskTempTopMostWindow/WindowInjection/${{ inputs.platform }}/${{ inputs.configuration }}
steps:
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@v2
- name: Download the source code
run: |
git clone https://github.com/rustdesk-org/RustDeskTempTopMostWindow RustDeskTempTopMostWindow
# Build. commit 53b548a5398624f7149a382000397993542ad796 is tag v0.3
- name: Build the project
run: |
cd RustDeskTempTopMostWindow && git checkout 53b548a5398624f7149a382000397993542ad796
msbuild ${{ env.project_path }} -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.platform }} /p:TargetVersion=${{ inputs.target_version }}
- name: Archive build artifacts
uses: actions/upload-artifact@master
if: ${{ inputs.upload-artifact }}
with:
name: topmostwindow-artifacts
path: |
./${{ env.build_output_dir }}/WindowInjection.dll

453
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ version = "1.2.4"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"
description = "A remote control software."
description = "RustDesk Remote Desktop"
default-run = "rustdesk"
rust-version = "1.75"
@@ -45,7 +45,8 @@ unix-file-copy-paste = [
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
whoami = "1.4"
async-trait = "0.1"
whoami = "1.5.0"
scrap = { path = "libs/scrap", features = ["wayland"] }
hbb_common = { path = "libs/hbb_common" }
serde_derive = "1.0"
@@ -57,7 +58,6 @@ lazy_static = "1.4"
sha2 = "0.10"
repng = "0.2"
parity-tokio-ipc = { git = "https://github.com/rustdesk-org/parity-tokio-ipc" }
runas = "=1.0" # https://github.com/mitsuhiko/rust-runas/issues/13
magnum-opus = { git = "https://github.com/rustdesk-org/magnum-opus" }
dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
rubato = { version = "0.12", optional = true }
@@ -95,7 +95,7 @@ sys-locale = "0.3"
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
clipboard = { path = "libs/clipboard" }
ctrlc = "3.2"
arboard = { version = "3.2", features = ["wayland-data-control"] }
arboard = { git = "https://github.com/fufesou/arboard", branch = "feat/x11_set_conn_timeout", features = ["wayland-data-control"] }
system_shutdown = "4.0"
qrcode-generator = "4.1"
@@ -107,6 +107,7 @@ virtual_display = { path = "libs/virtual_display", optional = true }
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
shared_memory = "0.12"
tauri-winrt-notification = "0.1.2"
runas = "1.2"
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2"
@@ -162,9 +163,10 @@ members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "lib
exclude = ["vdi/host", "examples/custom_plugin"]
[package.metadata.winres]
LegalCopyright = "Copyright © 2023 Purslane, Inc."
# this FileDescription overrides package.description
FileDescription = "RustDesk"
LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved."
ProductName = "RustDesk"
FileDescription = "RustDesk Remote Desktop"
OriginalFilename = "rustdesk.exe"
[target.'cfg(target_os="windows")'.build-dependencies]
winres = "0.1"

View File

@@ -13,8 +13,6 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
@@ -31,22 +29,6 @@ RustDesk welcomes contribution from everyone. See [CONTRIBUTING.md](docs/CONTRIB
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Free Public Servers
Below are the servers you are using for free, they may change over time. If you are not close to one of these, your network may be slow.
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
| Germany | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## Dev Container
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info.
## Dependencies
Desktop versions use Flutter or Sciter (deprecated) for GUI, this tutorial is for Sciter only, since it is easier and more friendly to start. Check out our [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for building Flutter version.
@@ -180,12 +162,12 @@ Please ensure that you are running these commands from the root of the RustDesk
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client
## Snapshots
## Screenshots
![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

@@ -80,8 +80,10 @@ def parse_rc_features(feature):
return get_all_features()
elif isinstance(feature, list):
if windows:
# download third party is deprecated, we use github ci instead.
# force add PrivacyMode
feature.append('PrivacyMode')
# feature.append('PrivacyMode')
pass
for feat in feature:
if isinstance(feat, str) and feat.upper() == 'ALL':
return get_all_features()
@@ -145,6 +147,12 @@ def make_parser():
action='store_true',
help='Skip cargo build process, only flutter version + Linux supported currently'
)
if windows:
parser.add_argument(
'--skip-portable-pack',
action='store_true',
help='Skip packing, only flutter version + Windows supported'
)
parser.add_argument(
"--package",
type=str
@@ -188,6 +196,9 @@ def generate_build_script_for_docker():
system2("bash /tmp/build.sh")
# Downloading third party resources is deprecated.
# We can use this function in an offline build environment.
# Even in an online environment, we recommend building third-party resources yourself.
def download_extract_features(features, res_dir):
import re
@@ -410,9 +421,11 @@ def build_flutter_dmg(version, features):
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
os.chdir('flutter')
system2('flutter build macos --release')
'''
system2(
"create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg")
'''
os.chdir("..")
@@ -427,7 +440,7 @@ def build_flutter_arch_manjaro(version, features):
system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f')
def build_flutter_windows(version, features):
def build_flutter_windows(version, features, skip_portable_pack):
if not skip_cargo:
system2(f'cargo build --features {features} --lib --release')
if not os.path.exists("target/release/librustdesk.dll"):
@@ -438,6 +451,8 @@ def build_flutter_windows(version, features):
os.chdir('..')
shutil.copy2('target/release/deps/dylib_virtual_display.dll',
flutter_build_dir_2)
if skip_portable_pack:
return
os.chdir('libs/portable')
system2('pip3 install -r requirements.txt')
system2(
@@ -487,7 +502,7 @@ def main():
os.chdir('../../..')
if flutter:
build_flutter_windows(version, features)
build_flutter_windows(version, features, args.skip_portable_pack)
return
system2('cargo build --release --features ' + features)
# system2('upx.exe target/release/rustdesk.exe')

View File

@@ -1,9 +1,11 @@
#[cfg(windows)]
fn build_windows() {
let file = "src/platform/windows.cc";
cc::Build::new().file(file).compile("windows");
let file2 = "src/platform/windows_delete_test_cert.cc";
cc::Build::new().file(file).file(file2).compile("windows");
println!("cargo:rustc-link-lib=WtsApi32");
println!("cargo:rerun-if-changed={}", file);
println!("cargo:rerun-if-changed={}", file2);
}
#[cfg(target_os = "macos")]

View File

@@ -27,14 +27,6 @@
[**BINARY تنزيل**](https://github.com/rustdesk/rustdesk/releases)
## خوادم مفتوحة ومجانية
فيما يلي الخوادم التي تستخدمها مجانًا، وقد تتغير طوال الوقت. إذا لم تكن قريبًا من أحد هؤلاء، فقد تكون شبكتك بطيئة.
| الموقع | المورد | المواصفات |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## التبعيات
لواجهة المستخدم الرسومية [sciter](https://sciter.com/) نسخة سطح المكتب تستخدم

View File

@@ -22,14 +22,6 @@ Projekt RustDesk vítá přiložení ruky k dílu od každého. Jak začít se d
[**STAHOVÁNÍ ZKOMPILOVANÝCH APLIKACÍ**](https://github.com/rustdesk/rustdesk/releases)
## Veřejné, zdarma službu nabízející servery
Níže jsou uvedeny servery zdarma k vašemu použití (údaje se mohou v čase měnit). Pokud se nenacházíte v oblastech světa poblíž nich, spojení může být pomalé.
| umístění | dodavatel | parametry |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## Softwarové součásti, na kterých závisí
Varianta pro počítač používá pro grafické uživatelské rozhraní [sciter](https://sciter.com/) stáhněte si potřebnou knihovnu.

View File

@@ -19,14 +19,6 @@ RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](docs/CONT
[**PROGRAM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
## Gratis offentlige servere
Nedenfor er de servere, du bruger gratis, det kan ændre sig med tiden. Hvis du ikke er tæt på en af disse, kan dit netværk være langsomt.
| Beliggenhed | Udbyder | Specifikation |
| ---------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## Afhængigheder
Desktopversioner bruger [sciter](https://sciter.com/) eller Flutter til GUI, denne vejledning er kun for Sciter.

View File

@@ -29,22 +29,6 @@ RustDesk heißt jegliche Mitarbeit willkommen. Schauen Sie sich [CONTRIBUTING-DE
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Freie öffentliche Server
Nachfolgend sind die Server gelistet, die Sie kostenlos nutzen können. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls Sie nicht in der Nähe einer dieser Server sind, kann es sein, dass Ihre Verbindung langsam sein wird.
| Standort | Anbieter | Spezifikation |
| --------- | ------------- | ------------------ |
| Deutschland | [Hetzner](https://www.hetzner.com/de/) | 2 vCPU / 4 GB RAM |
| Ukraine (Kiew) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## Dev-Container
[![In Dev-Containern öffnen](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Wenn Sie VS Code und Docker bereits installiert haben, können Sie auf das Abzeichen oben klicken, um loszulegen. Wenn Sie darauf klicken, wird VS Code automatisch die Dev-Container-Erweiterung installieren, den Quellcode in ein Container-Volume klonen und einen Dev-Container für die Verwendung aufsetzen.
Weitere Informationen finden Sie in [DEVCONTAINER-DE.md](DEVCONTAINER-DE.md).
## Abhängigkeiten
Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter.

View File

@@ -19,14 +19,6 @@ RustDesk bonvenigas kontribuon de ĉiuj. Vidu [`docs/CONTRIBUTING.md`](CONTRIBUT
[**BINARA ELŜUTO**](https://github.com/rustdesk/rustdesk/releases)
## Senpagaj publikaj serviloj
Malsupre estas la serviloj, kiuj vi uzas senpage, ĝi povas ŝanĝi laŭlonge de la tempo. Se vi ne estas proksima de unu de tiuj, via reto povas esti malrapida.
| Situo | Vendanto | Detaloj |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dependantaĵoj
La labortabla versio uzas [sciter](https://sciter.com/) por la interfaco, bonvolu elŝuti la bibliotekon dinamikan sciter.

View File

@@ -25,15 +25,6 @@ RustDesk agradece la contribución de todo el mundo. Lee [`docs/CONTRIBUTING.md`
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Servidores gratis de uso público
A continuación se muestran los servidores gratuitos, pueden cambiar a medida que pasa el tiempo. Si no estás cerca de uno de ellos, tu conexión puede ser lenta.
| Ubicación | Compañía | Especificación |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dependencias
La versión Desktop usa [Sciter](https://sciter.com/) o Flutter para el GUI, este tutorial es solo para Sciter.

View File

@@ -25,13 +25,6 @@
[دریافت نرم‌افزار](https://github.com/rustdesk/rustdesk/releases)
## سرورهای عمومی رایگان
شما مي‌توانید از سرورهای زیر به رایگان استفاده کنید. این لیست ممکن است به مرور زمان تغییر می‌کند. اگر به این سرورها نزدیک نیستید، ممکن است اتصال شما کند باشد.
| موقعیت | سرویس دهنده | مشخصات |
| --------- | ------------- | ------------------ |
| آلمان | Hetzner | 2 vCPU / 4GB RAM |
## وابستگی ها
نسخه‌های رومیزی از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده می‌کنند. خواهشمندیم کتابخانه‌ی پویای sciter را خودتان دانلود کنید از این منابع دریافت کنید.

View File

@@ -19,14 +19,6 @@ RustDesk toivottaa avustukset tervetulleiksi kaikilta. Katso lisätietoja [`docs
[**BINAARILATAUS**](https://github.com/rustdesk/rustdesk/releases)
## Vapaita julkisia palvelimia
Alla on palvelimia, joita voit käyttää ilmaiseksi, ne saattavat muuttua ajan mittaan. Jos et ole lähellä yhtä näistä, verkkosi voi olla hidas.
| Sijainti | Myyjä | Määrittely |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Riippuvuudet
Desktop-versiot käyttävät [sciter](https://sciter.com/) graafisena käyttöliittymänä, lataa sciter-dynaaminen kirjasto itsellesi.

View File

@@ -19,14 +19,6 @@ RustDesk accueille les contributions de tout le monde. Voir [`docs/CONTRIBUTING.
[**TÉLÉCHARGEMENT BINAIRE**](https://github.com/rustdesk/rustdesk/releases)
## Serveurs publics libres
Ci-dessous se trouvent les serveurs que vous utilisez gratuitement, cela peut changer au fil du temps. Si vous n'êtes pas proche de l'un d'entre eux, votre réseau peut être lent.
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## Dépendances
Les versions de bureau utilisent [sciter](https://sciter.com/) pour l'interface graphique, veuillez télécharger la bibliothèque dynamique sciter vous-même.

View File

@@ -29,22 +29,6 @@
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Δωρεάν δημόσιοι διακομιστές
Παρακάτω είναι οι διακομιστές που χρησιμοποιούνται δωρεάν, ενδέχεται να αλλάξουν με την πάροδο του χρόνου. Εάν δεν είστε κοντά σε ένα από αυτούς, το δίκτυό σας ίσως να είναι αργό.
| Περιοχή | Πάροχος | Προδιαγραφές |
| --------- | ------------- | ------------------ |
| Γερμανία | Hetzner | 2 vCPU / 4GB RAM |
| Ουκρανία (Κίεβο) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dev Container
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Αν έχετε εγκατεστημένα το VS Code και το Docker, μπορείτε να ξεκινήσετε κάνοντας κλικ στην παραπάνω εικόνα. Αυτό θα έχει ως αποτέλεσμα, το VS Code να εγκαταστήσει αυτόματα την επέκταση Dev Containers, εάν χρειάζεται, θα κλωνοποιήσει τον πηγαίο κώδικα σε έναν νέο container και θα εκκινήσει ένα Dev Container για χρήση προγραμματισμού.
Για περισσότερες πληροφορίες μεταβείτε στο [DEVCONTAINER.md](docs/DEVCONTAINER.md).
## Προαπαιτούμενα για build
Στις παραθυρικές εκδόσεις χρησιμοποιείται είτε το [sciter](https://sciter.com/) είτε το Flutter, τα παρακάτω βήματα είναι μόνο για το Sciter.

View File

@@ -27,14 +27,6 @@ A RustDesk szívesen fogad minden contributiont, támogatást mindenkitől. Lás
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Ingyenes publikus szerverek
Ezalatt az üzenet alatt találhatóak azok a publikus szerverek, amelyeket ingyen használhatsz. Ezek a szerverek változhatnak a jövőben, illetve a hálózatuk lehet hogy lassú lehet.
| Hely | Host | Specifikáció |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dependencies
Az asztali verziók [sciter](https://sciter.com/)-t használnak a GUI-hoz, kérlek telepítsd a dynamikus könyvtárat magad.

View File

@@ -31,20 +31,6 @@ RustDesk mengajak semua orang untuk ikut berkontribusi. Lihat [`docs/CONTRIBUTIN
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Server Publik Gratis
Di bawah ini merupakan server gratis yang bisa kamu gunakan, seiring dengan waktu mungkin akan terjadi perubahan spesifikasi pada setiap server yang ada. Jika lokasi kamu berada jauh dengan salah satu server yang tersedia, kemungkinan koneksi akan terasa lambat ketika melakukan proses remote.
| Lokasi | Penyedia | Spesifikasi |
| --------- | ------------- | ------------------ |
| Jerman | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM |
| Ukraina (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dev Container
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Apabila PC kamu sudah terinstal VS Code dan Docker, kamu bisa mengklik badge yang ada diatas untuk memulainya. Dengan mengklik badge tersebut secara otomatis akan menginstal ekstensi pada VS Code, lakukan kloning (clone) source code kedalam container volume, dan aktifkan dev container untuk menggunakannya.
## Dependensi
Pada versi desktop, antarmuka pengguna (GUI) menggunakan [Sciter](https://sciter.com/) atau flutter

View File

@@ -4,8 +4,8 @@
<a href="#passaggi-per-la-compilazione">Compilazione</a> •
<a href="#come-compilare-con-docker">Docker</a> •
<a href="#struttura-dei-file">Struttura</a> •
<a href="#screenshots">Schermate</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-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</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>
<a href="#schermate">Schermate</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-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>]<br>
<b>Abbiamo bisogno del tuo aiuto per tradurre questo file README e la <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI RustDesk</a> nella tua lingua nativa</b>
</p>
@@ -13,28 +13,29 @@ Chatta con noi su: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Ancora un altro software per il controllo remoto del desktop, scritto in Rust.
Funziona immediatamente, nessuna configurazione richiesta. Hai il pieno controllo dei tuoi dati, senza preoccupazioni per la sicurezza.
Puoi usare il nostro server rendezvous/relay, [configurare il tuo server](https://rustdesk.com/server) o [realizzare il tuo server rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
[![Bounties aperti](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
RustDesk accoglie il contributo di tutti.
Per ulteriori informazioni su come iniziare a contribuire, vedi [`docs/CONTRIBUTING-IT.md`](CONTRIBUTING.md).
Ancora un altro software per il controllo remoto del desktop, scritto in Rust. Funziona immediatamente, nessuna configurazione richiesta. Hai il pieno controllo dei tuoi dati, senza preoccupazioni per la sicurezza. Puoi usare il nostro server rendezvous/relay, [configurare il tuo server](https://rustdesk.com/server) o [realizzare il tuo server rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
[**DOWNLOAD PROGRAMMA**](https://github.com/rustdesk/rustdesk/releases)
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
## Server pubblici gratuiti
RustDesk accoglie il contributo di tutti. Per ulteriori informazioni su come iniziare a contribuire, vedi [CONTRIBUTING.md](CONTRIBUTING-IT.md).
Qui sotto trovi i server che possono essere usati gratuitamente, la lista potrebbe cambiare nel tempo.
Se non sei vicino a uno di questi server, la connessione potrebbe essere lenta.
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
| Posizione | Venditore | Specifiche |
| --------- | ------------- | ------------------ |
| Germania | Hetzner | 2 vCPU / 4GB RAM |
| Ucraina (Kyev) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
[**SCARICA PROGRAMMA**](https://github.com/rustdesk/rustdesk/releases)
[**SCARICA NIGHTLY**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Dipendenze
La versione Desktop usa per la GUI [sciter](https://sciter.com/), per favore scarica la libreria dinamica sciter.
Le versioni desktop utilizzano Flutter o Sciter (deprecato) per l'interfaccia utente, questo tutorial è solo per Sciter, poiché è più facile per iniziare. Controlla il nostro [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) per la compilazione della versione Flutter.
Scarica la libreria dinamica Sciter.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
@@ -51,12 +52,22 @@ La versione Desktop usa per la GUI [sciter](https://sciter.com/), per favore sca
- Esegui `cargo run`
## [Build](https://rustdesk.com/docs/en/dev/build/)
## Come compilare in 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)
@@ -125,10 +136,7 @@ Quindi, ogni volta che devi compilare l'applicazione, esegui il seguente comando
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
```
Tieni presente che la prima build potrebbe richiedere più tempo prima che le dipendenze vengano memorizzate nella cache, le build successive saranno più veloci.
Inoltre, se hai bisogno di specificare argomenti diversi per il comando build, puoi farlo alla fine del comando nella posizione `<OPTIONAL-ARGS>`.
Ad esempio, se vuoi creare una versione di rilascio ottimizzata, esegui il comando precedentemente indicato seguito da `--release`.
L'eseguibile generato sarà creato nella cartella destinazione del sistema e può essere eseguito con:
Tieni presente che la prima build potrebbe richiedere più tempo prima che le dipendenze vengano memorizzate nella cache, le build successive saranno più veloci. Inoltre, se hai bisogno di specificare argomenti diversi per il comando build, puoi farlo alla fine del comando nella posizione `<OPTIONAL-ARGS>`. Ad esempio, se vuoi creare una versione di rilascio ottimizzata, esegui il comando precedentemente indicato seguito da `--release`. L'eseguibile generato sarà creato nella cartella destinazione del sistema e può essere eseguito con:
```sh
target/debug/rustdesk
@@ -140,19 +148,21 @@ Oppure, se stai avviando un eseguibile di rilascio:
target/release/rustdesk
```
Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altrimenti l'applicazione potrebbe non essere in grado di trovare le risorse richieste.
Nota inoltre che altri sottocomandi cargo come `install` o `run` non sono attualmente supportati tramite questo metodo poiché installerebbero o eseguirebbero il programma all'interno del container anziché nell'host.
Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altrimenti l'applicazione potrebbe non essere in grado di trovare le risorse richieste. Nota inoltre che altri sottocomandi cargo come `install` o `run` non sono attualmente supportati tramite questo metodo poiché installerebbero o eseguirebbero il programma all'interno del container anziché nell'host.
## Struttura dei file
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec video, config, wrapper tcp/udp, protobuf, funzioni per il trasferimento file, e altre funzioni utili.
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: cattura dello schermo
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controllo tastiera/mouse specifico della piattaforma
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: implementazione del copia e incolla dei file per Windows, Linux, macOS.
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: Sciter UI obsoleto (deprecato)
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: servizi audio/appunti/input/video e connessioni di rete
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: avvio di una connessione peer
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunica con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attende la connessione remota diretta (TCP hole punching) oppure indiretta (relayed)
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunica con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attende la connessione remota diretta (TCP hole punching) oppure indiretta (relayed)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: codice specifico della piattaforma
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: codice Flutter per desktop e mobile
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript per client web Flutter
## Schermate

View File

@@ -24,13 +24,6 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CON
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
## 無料のパブリックサーバー
下記のサーバーは、無料で使用できますが、後々変更されることがあります。これらのサーバーから遠い場合、接続が遅い可能性があります。
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## 依存関係
デスクトップ版ではGUIに [sciter](https://sciter.com/) が使われています。 sciter dynamic library をダウンロードしてください。

View File

@@ -24,13 +24,6 @@ RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/C
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
## 무료 퍼블릭 서버
표에 있는 서버는 무료로 사용할 수 있지만 추후 변경될 수도 있습니다. 이 서버에서 멀다면, 네트워크가 느려질 가능성도 있습니다.
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## 의존관계
데스크탑판에는 GUI에 [sciter](https://sciter.com/)가 사용되었습니다. sciter dynamic library 를 다운로드해주세요.

View File

@@ -19,13 +19,6 @@
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
## സൗജന്യ പൊതു സെർവറുകൾ
നിങ്ങൾ സൗജന്യമായി ഉപയോഗിക്കുന്ന സെർവറുകൾ ചുവടെയുണ്ട്, അത് സമയത്തിനനുസരിച്ച് മാറിയേക്കാം. നിങ്ങൾ ഇവയിലൊന്നിനോട് അടുത്തല്ലെങ്കിൽ, നിങ്ങളുടെ നെറ്റ്‌വർക്ക് സ്ലോ ആയേക്കാം.
| സ്ഥാനം | കച്ചവടക്കാരൻ | വിവരണം |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## ഡിപെൻഡൻസികൾ
ഡെസ്‌ക്‌ടോപ്പ് പതിപ്പുകൾ GUI-യ്‌ക്കായി [sciter](https://sciter.com/) ഉപയോഗിക്കുന്നു, ദയവായി സ്‌സൈറ്റർ ഡൈനാമിക് ലൈബ്രറി സ്വയം ഡൗൺലോഡ് ചെയ്യുക.

View File

@@ -27,22 +27,6 @@ RustDesk verwelkomt bijdragen van iedereen. Zie [`docs/CONTRIBUTING.md`](CONTRIB
alt="Download het op F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Gratis openbare servers
Hieronder staan de servers die u gratis gebruikt, ze kunnen in de loop van de tijd veranderen. Als u niet in de buurt van een van deze servers bevindt, kan uw vervinding langzamer zijn.
| Locatie | Aanbieder | Specificaties |
| --------- | ------------- | ------------------ |
| Duitsland | Hetzner | 2 vCPU / 4GB RAM |
| Oekraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dev Container
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Als u VS Code en Docker al hebt geinstalleerd, kunt u op de bovenstaande badge klikken om te beginnen. Door te klikken zal VS Code automatisch de Dev Containers-extensie installeren indien nodig, de broncode klonen naar een containervolume en een dev container opstarten voor gebruik.
Bekijk [DEVCONTAINER.md](docs/DEVCONTAINER.md) voor meer informatie.
## Afhankelijkheden
Desktop versies gebruiken [sciter](https://sciter.com/) of Flutter voor GUI, deze handleiding is alleen voor Sciter.

View File

@@ -29,22 +29,6 @@ RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING-PL.md`](C
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Darmowe Serwery Publiczne
Poniżej znajdują się serwery, z których można korzystać za darmo, może się to zmienić z upływem czasu. Jeśli nie znajdujesz się w pobliżu jednego z nich, Twoja prędkość połączenia może być niska.
| Lokalizacja | Dostawca | Specyfikacja |
| --------- | ------------- | ------------------ |
| Niemcy | Hetzner | 2 vCPU / 4GB RAM |
| Ukraina (Kijów) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Konterner Programisty (Dev Container)
[![Otwórz w Kontenerze programisty](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Jeżeli masz zainstalowany VS Code i Docker, możesz kliknąć w powyższy link, aby rozpocząć. Kliknięcie spowoduje automatyczną instalację rozszrzenia Kontenera Programisty w VS Code (jeżeli wymagany), sklonuje kod źródłowy do kontenera, i przygotuje kontener do użycia.
Więcej informacji w pliku [DEVCONTAINER-PL.md](docs/DEVCONTAINER-PL.md) for more info.
## Zależności
Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobrać samodzielnie bibliotekę sciter.

View File

@@ -19,14 +19,6 @@ RustDesk acolhe contribuições de todos. Leia [`docs/CONTRIBUTING.md`](CONTRIBU
[**DOWNLOAD DE BINÁRIOS**](https://github.com/rustdesk/rustdesk/releases)
## Servidores Públicos Grátis
Abaixo estão os servidores que você está utilizando de graça, ele pode mudar com o tempo. Se você não está próximo de algum deles, sua conexão pode ser lenta.
| Localização | Fornecedor | Especificações |
| ----------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## Dependências
Versões de desktop utilizam [sciter](https://sciter.com/) para a GUI, por favor baixe a biblioteca dinâmica sciter por conta própria.

View File

@@ -28,13 +28,6 @@ RustDesk приветствует вклад каждого. Ознакомьт
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Бесплатные общедоступные серверы
Ниже приведены бесплатные публичные сервера, используемые по умолчанию. Имейте ввиду, они могут меняться со временем. Также стоит отметить, что скорость работы сети зависит от вашего местоположения и расстояния до серверов. Подключение происходит к ближайшему доступному.
| Расположение | Поставщик | Технические характеристики |
| --------- | ------------- | ------------------ |
| Германия | Hetzner | 2 vCPU / 4GB RAM |
## Зависимости
Настольные версии используют [sciter](https://sciter.com/) для графического интерфейса, загрузите динамическую библиотеку sciter самостоятельно.

View File

@@ -30,23 +30,6 @@ RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs
alt="F-Droid'de Alın"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Ücretsiz Genel Sunucular
Aşağıda ücretsiz olarak kullandığınız sunucular listelenmiştir, zaman içinde değişebilirler. Eğer bunlardan birine yakın değilseniz, ağınız yavaş olabilir.
| Konum | Sağlayıcı | Özellikler |
| --------- | ------------- | ------------------ |
| Almanya | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
| Almanya | [Codext](https://codext.de) | 4 vCPU / 8 GB RAM |
| Ukrayna (Kiev) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## Geliştirici Konteyneri
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Eğer zaten VS Code ve Docker kurulu ise yukarıdaki rozete tıklayarak başlayabilirsiniz. Tıklamak, VS Code'un gerektiğinde Dev Konteyner eklentisini otomatik olarak yüklemesine, kaynak kodunu bir konteyner hacmine klonlamasına ve kullanım için bir geliştirici konteyneri başlatmasına neden olur.
Daha fazla bilgi için [DEVCONTAINER.md](docs/DEVCONTAINER-TR.md) belgesine bakabilirsiniz.
## Bağımlılıklar
Masaüstü sürümleri GUI için

View File

@@ -31,22 +31,6 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Безкоштовні загальнодоступні сервери
Нижче наведені сервери, для безкоштовного використання, вони можуть змінюватися з часом. Якщо ви не перебуваєте поруч з одним із них, ваша мережа може працювати повільно.
| Місцезнаходження | Постачальник | Технічні характеристики |
| --------- | ------------- | ------------------ |
| Німеччина | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM |
| Україна (Київ) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dev Container
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Якщо у вас уже встановлено VS Code та Docker, ви можете натиснути значок вище, щоб розпочати. Клацання призведе до того, що VS Code автоматично встановить розширення Dev Containers, якщо це необхідно, клонує вихідний код у том контейнера та розгорне контейнер dev для використання.
Дивіться [DEVCONTAINER.md](docs/DEVCONTAINER.md) для додаткової інформації
## Залежності
Стільничні версії використовують Flutter чи Sciter (застаріле) для графічного інтерфейсу. Ця інструкція лише для Sciter, оскільки він є більш простим та дружнім для початківців. Перегляньте [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) для збірки версії на Flutter.

View File

@@ -27,14 +27,6 @@ Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Các Máy Chủ Công Khai Miễn Phí
Dưới đây là những máy chủ mà bạn có thể sử dụng mà không mất phí, chú ý là máy chủ có thể thay đổi theo thời gian. Nếu địa điểm của bạn không gần một trong số những máy chủ này, thì kết nói có thể chậm.
| Địa điểm | Nhà cung cấp | Cấu hình |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## Dependencies
Phiên bản cho máy tính sử dụng [sciter](https://sciter.com/) cho giao diện của phần mềm, vậy nên bạn cần tự tải về thư viện sciter.

View File

@@ -30,23 +30,6 @@ RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.m
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## 免费的公共服务器
以下是您可以使用的、免费的、会随时更新的公共服务器列表,在国内也许网速会很慢或者无法访问。
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
| Germany | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## Dev Container
[![在 Dev Containers 中打开](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
如果你已经安装了 VS Code 和 Docker, 你可以点击上面的徽章开始使用. 点击后, VS Code 将自动安装 Dev Containers 扩展(如果需要),将源代码克隆到容器卷中, 并启动一个 Dev 容器供使用.
Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info.
## 依赖
桌面版本界面使用[sciter](https://sciter.com/), 请自行下载。

View File

@@ -20,7 +20,7 @@
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/com.rustdesk.RustDesk.desktop",
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/rustdesk-link.desktop",
"mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg",
"for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png logo.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
"for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png scalable.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
],
"cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
"sources": [
@@ -30,7 +30,7 @@
},
{
"type": "file",
"path": "../res/logo.svg"
"path": "../res/scalable.svg"
}
]
}

1
flutter/.gitignore vendored
View File

@@ -32,7 +32,6 @@
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,12 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi' hide Size;
import 'dart:io';
import 'dart:math';
import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@@ -25,7 +22,6 @@ import 'package:get/get.dart';
import 'package:uni_links/uni_links.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:uuid/uuid.dart';
import 'package:win32/win32.dart' as win32;
import 'package:window_manager/window_manager.dart';
import 'package:window_size/window_size.dart' as window_size;
@@ -33,20 +29,30 @@ import '../consts.dart';
import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart';
import 'desktop/pages/remote_page.dart' as desktop_remote;
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'models/input_model.dart';
import 'models/model.dart';
import 'models/platform_model.dart';
import 'package:flutter_hbb/native/win32.dart'
if (dart.library.html) 'package:flutter_hbb/web/win32.dart';
import 'package:flutter_hbb/native/common.dart'
if (dart.library.html) 'package:flutter_hbb/web/common.dart';
final globalKey = GlobalKey<NavigatorState>();
final navigationBarKey = GlobalKey();
final isAndroid = Platform.isAndroid;
final isIOS = Platform.isIOS;
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
var isWeb = false;
var isWebDesktop = false;
final isAndroid = isAndroid_;
final isIOS = isIOS_;
final isWindows = isWindows_;
final isMacOS = isMacOS_;
final isLinux = isLinux_;
final isDesktop = isDesktop_;
final isWeb = isWeb_;
final isWebDesktop = isWebDesktop_;
var isMobile = isAndroid || isIOS;
var version = "";
var version = '';
int androidVersion = 0;
/// only available for Windows target
@@ -56,6 +62,8 @@ DesktopType? desktopType;
bool get isMainDesktopWindow =>
desktopType == DesktopType.main || desktopType == DesktopType.cm;
String get screenInfo => screenInfo_;
/// Check if the app is running with single view mode.
bool isSingleViewApp() {
return desktopType == DesktopType.cm;
@@ -229,11 +237,13 @@ class MyTheme {
);
static SwitchThemeData switchTheme() {
return SwitchThemeData(splashRadius: isDesktop ? 0 : kRadialReactionRadius);
return SwitchThemeData(
splashRadius: (isDesktop || isWebDesktop) ? 0 : kRadialReactionRadius);
}
static RadioThemeData radioTheme() {
return RadioThemeData(splashRadius: isDesktop ? 0 : kRadialReactionRadius);
return RadioThemeData(
splashRadius: (isDesktop || isWebDesktop) ? 0 : kRadialReactionRadius);
}
// Checkbox
@@ -282,7 +292,7 @@ class MyTheme {
static EdgeInsets dialogContentPadding({bool actions = true}) {
final double p = dialogPadding;
return isDesktop
return (isDesktop || isWebDesktop)
? EdgeInsets.fromLTRB(p, p, p, actions ? (p - 4) : p)
: EdgeInsets.fromLTRB(p, p, p, actions ? (p / 2) : p);
}
@@ -290,12 +300,12 @@ class MyTheme {
static EdgeInsets dialogActionsPadding() {
final double p = dialogPadding;
return isDesktop
return (isDesktop || isWebDesktop)
? EdgeInsets.fromLTRB(p, 0, p, (p - 4))
: EdgeInsets.fromLTRB(p, 0, (p - mobileTextButtonPaddingLR), (p / 2));
}
static EdgeInsets dialogButtonPadding = isDesktop
static EdgeInsets dialogButtonPadding = (isDesktop || isWebDesktop)
? EdgeInsets.only(left: dialogPadding)
: EdgeInsets.only(left: dialogPadding / 3);
@@ -367,10 +377,10 @@ class MyTheme {
labelColor: Colors.black87,
),
tooltipTheme: tooltipTheme(),
splashColor: isDesktop ? Colors.transparent : null,
highlightColor: isDesktop ? Colors.transparent : null,
splashFactory: isDesktop ? NoSplash.splashFactory : null,
textButtonTheme: isDesktop
splashColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
highlightColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
splashFactory: (isDesktop || isWebDesktop) ? NoSplash.splashFactory : null,
textButtonTheme: (isDesktop || isWebDesktop)
? TextButtonThemeData(
style: TextButton.styleFrom(
splashFactory: NoSplash.splashFactory,
@@ -410,7 +420,9 @@ class MyTheme {
color: Colors.white,
shape: RoundedRectangleBorder(
side: BorderSide(
color: isDesktop ? Color(0xFFECECEC) : Colors.transparent),
color: (isDesktop || isWebDesktop)
? Color(0xFFECECEC)
: Colors.transparent),
borderRadius: BorderRadius.all(Radius.circular(8.0)),
)),
).copyWith(
@@ -436,7 +448,7 @@ class MyTheme {
),
),
scrollbarTheme: scrollbarThemeDark,
inputDecorationTheme: isDesktop
inputDecorationTheme: (isDesktop || isWebDesktop)
? InputDecorationTheme(
fillColor: Color(0xFF24252B),
filled: true,
@@ -463,10 +475,10 @@ class MyTheme {
labelColor: Colors.white70,
),
tooltipTheme: tooltipTheme(),
splashColor: isDesktop ? Colors.transparent : null,
highlightColor: isDesktop ? Colors.transparent : null,
splashFactory: isDesktop ? NoSplash.splashFactory : null,
textButtonTheme: isDesktop
splashColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
highlightColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
splashFactory: (isDesktop || isWebDesktop) ? NoSplash.splashFactory : null,
textButtonTheme: (isDesktop || isWebDesktop)
? TextButtonThemeData(
style: TextButton.styleFrom(
splashFactory: NoSplash.splashFactory,
@@ -631,8 +643,12 @@ closeConnection({String? id}) {
gFFI.chatModel.hideChatOverlay();
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
} else {
final controller = Get.find<DesktopTabController>();
controller.closeBy(id);
if (isWeb) {
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
} else {
final controller = Get.find<DesktopTabController>();
controller.closeBy(id);
}
}
}
@@ -810,7 +826,7 @@ class OverlayDialogManager {
Offstage(
offstage: !showCancel,
child: Center(
child: isDesktop
child: (isDesktop || isWebDesktop)
? dialogButton('Cancel', onPressed: cancel)
: TextButton(
style: flatButtonStyle,
@@ -1285,7 +1301,7 @@ class AndroidPermissionManager {
}
static Future<bool> check(String type) {
if (isDesktop) {
if (isDesktop || isWeb) {
return Future.value(true);
}
return gFFI.invokeMethod("check_permission", type);
@@ -1299,7 +1315,7 @@ class AndroidPermissionManager {
/// We use XXPermissions to request permissions,
/// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java
static Future<bool> request(String type) {
if (isDesktop) {
if (isDesktop || isWeb) {
return Future.value(true);
}
@@ -1521,6 +1537,12 @@ class LastWindowPosition {
}
}
String get windowFramePrefix =>
kWindowPrefix +
(bind.isIncomingOnly()
? "incoming_"
: (bind.isOutgoingOnly() ? "outgoing_" : ""));
/// Save window position and size on exit
/// Note that windowId must be provided if it's subwindow
Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
@@ -1533,10 +1555,10 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
late Size sz;
late bool isMaximized;
bool isFullscreen = stateGlobal.fullscreen.isTrue ||
(Platform.isMacOS && stateGlobal.closeOnFullscreen == true);
(isMacOS && stateGlobal.closeOnFullscreen == true);
setFrameIfMaximized() {
if (isMaximized) {
final pos = bind.getLocalFlutterOption(k: kWindowPrefix + type.name);
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
var lpos = LastWindowPosition.loadFromString(pos);
position = Offset(
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
@@ -1546,7 +1568,13 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
switch (type) {
case WindowType.Main:
isMaximized = await windowManager.isMaximized();
// Checking `bind.isIncomingOnly()` is a simple workaround for MacOS.
// `await windowManager.isMaximized()` will always return true
// if is not resizable. The reason is unknown.
//
// `windowManager.setResizable(!bind.isIncomingOnly());` in main.dart
isMaximized =
bind.isIncomingOnly() ? false : await windowManager.isMaximized();
position = await windowManager.getPosition();
sz = await windowManager.getSize();
setFrameIfMaximized();
@@ -1566,7 +1594,7 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
setFrameIfMaximized();
break;
}
if (Platform.isWindows) {
if (isWindows) {
const kMinOffset = -10000;
const kMaxOffset = 10000;
if (position.dx < kMinOffset ||
@@ -1584,7 +1612,7 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
"Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}");
await bind.setLocalFlutterOption(
k: kWindowPrefix + type.name, v: pos.toString());
k: windowFramePrefix + type.name, v: pos.toString());
if (type == WindowType.RemoteDesktop && windowId != null) {
await _saveSessionWindowPosition(
@@ -1599,7 +1627,7 @@ Future _saveSessionWindowPosition(WindowType windowType, int windowId,
getPeerPos(String peerId) {
if (isMaximized) {
final peerPos = bind.mainGetPeerFlutterOptionSync(
id: peerId, k: kWindowPrefix + windowType.name);
id: peerId, k: windowFramePrefix + windowType.name);
var lpos = LastWindowPosition.loadFromString(peerPos);
return LastWindowPosition(
lpos?.width ?? pos.offsetWidth,
@@ -1618,7 +1646,7 @@ Future _saveSessionWindowPosition(WindowType windowType, int windowId,
for (final peerId in remoteList.split(',')) {
bind.mainSetPeerFlutterOptionSync(
id: peerId,
k: kWindowPrefix + windowType.name,
k: windowFramePrefix + windowType.name,
v: getPeerPos(peerId));
}
}
@@ -1736,14 +1764,14 @@ Future<bool> restoreWindowPosition(WindowType type,
// Because the session may not be read at this time.
if (desktopType == DesktopType.main) {
pos = bind.mainGetPeerFlutterOptionSync(
id: peerId, k: kWindowPrefix + type.name);
id: peerId, k: windowFramePrefix + type.name);
} else {
pos = await bind.sessionGetFlutterOptionByPeerId(
id: peerId, k: kWindowPrefix + type.name);
id: peerId, k: windowFramePrefix + type.name);
}
isRemotePeerPos = pos != null;
}
pos ??= bind.getLocalFlutterOption(k: kWindowPrefix + type.name);
pos ??= bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
var lpos = LastWindowPosition.loadFromString(pos);
if (lpos == null) {
@@ -1790,9 +1818,13 @@ Future<bool> restoreWindowPosition(WindowType type,
}
if (lpos.isMaximized == true) {
await restorePos();
await windowManager.maximize();
if (!(bind.isIncomingOnly() || bind.isOutgoingOnly())) {
await windowManager.maximize();
}
} else {
await windowManager.setSize(size);
if (!bind.isIncomingOnly() || bind.isOutgoingOnly()) {
await windowManager.setSize(size);
}
await restorePos();
}
return true;
@@ -1833,13 +1865,14 @@ Future<bool> restoreWindowPosition(WindowType type,
/// initUniLinks should only be used on macos/windows.
/// we use dbus for linux currently.
Future<bool> initUniLinks() async {
if (Platform.isLinux) {
if (isLinux) {
return false;
}
// check cold boot
try {
final initialLink = await getInitialLink();
if (initialLink == null) {
print("initialLink: $initialLink");
if (initialLink == null || initialLink.isEmpty) {
return false;
}
return handleUriLink(uriString: initialLink);
@@ -1855,7 +1888,7 @@ Future<bool> initUniLinks() async {
///
/// Returns a [StreamSubscription] which can listen the uni links.
StreamSubscription? listenUniLinks({handleByFlutter = true}) {
if (Platform.isLinux) {
if (isLinux) {
return null;
}
@@ -1889,7 +1922,7 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
if (cmdArgs != null && cmdArgs.isNotEmpty) {
args = cmdArgs;
// rustdesk <uri link>
if (args[0].startsWith(kUniLinksPrefix)) {
if (args[0].startsWith(bind.mainUriPrefixSync())) {
final uri = Uri.tryParse(args[0]);
if (uri != null) {
args = urlLinkToCmdArgs(uri);
@@ -2003,7 +2036,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
command = '--connect';
id = uri.path.substring("/new/".length);
} else if (uri.authority == "config") {
if (Platform.isAndroid || Platform.isIOS) {
if (isAndroid || isIOS) {
final config = uri.path.substring("/".length);
// add a timer to make showToast work
Timer(Duration(seconds: 1), () {
@@ -2012,7 +2045,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
}
return null;
} else if (uri.authority == "password") {
if (Platform.isAndroid || Platform.isIOS) {
if (isAndroid || isIOS) {
final password = uri.path.substring("/".length);
if (password.isNotEmpty) {
Timer(Duration(seconds: 1), () async {
@@ -2079,19 +2112,28 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
return null;
}
connectMainDesktop(
String id, {
required bool isFileTransfer,
required bool isTcpTunneling,
required bool isRDP,
bool? forceRelay,
}) async {
connectMainDesktop(String id,
{required bool isFileTransfer,
required bool isTcpTunneling,
required bool isRDP,
bool? forceRelay,
String? password,
bool? isSharedPassword}) async {
if (isFileTransfer) {
await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay);
await rustDeskWinManager.newFileTransfer(id,
password: password,
isSharedPassword: isSharedPassword,
forceRelay: forceRelay);
} else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.newPortForward(id, isRDP, forceRelay: forceRelay);
await rustDeskWinManager.newPortForward(id, isRDP,
password: password,
isSharedPassword: isSharedPassword,
forceRelay: forceRelay);
} else {
await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay);
await rustDeskWinManager.newRemoteDesktop(id,
password: password,
isSharedPassword: isSharedPassword,
forceRelay: forceRelay);
}
}
@@ -2099,14 +2141,13 @@ connectMainDesktop(
/// If [isFileTransfer], starts a session only for file transfer.
/// If [isTcpTunneling], starts a session only for tcp tunneling.
/// If [isRDP], starts a session only for rdp.
connect(
BuildContext context,
String id, {
bool isFileTransfer = false,
bool isTcpTunneling = false,
bool isRDP = false,
bool forceRelay = false,
}) async {
connect(BuildContext context, String id,
{bool isFileTransfer = false,
bool isTcpTunneling = false,
bool isRDP = false,
bool forceRelay = false,
String? password,
bool? isSharedPassword}) async {
if (id == '') return;
if (!isDesktop || desktopType == DesktopType.main) {
try {
@@ -2134,6 +2175,8 @@ connect(
isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
password: password,
isSharedPassword: isSharedPassword,
forceRelay: forceRelay2,
);
} else {
@@ -2142,6 +2185,8 @@ connect(
'isFileTransfer': isFileTransfer,
'isTcpTunneling': isTcpTunneling,
'isRDP': isRDP,
'password': password,
'isSharedPassword': isSharedPassword,
'forceRelay': forceRelay,
});
}
@@ -2155,16 +2200,34 @@ connect(
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => FileManagerPage(id: id),
builder: (BuildContext context) => FileManagerPage(
id: id, password: password, isSharedPassword: isSharedPassword),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => RemotePage(id: id),
),
);
if (isWebDesktop) {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => desktop_remote.RemotePage(
key: ValueKey(id),
id: id,
toolbarState: ToolbarState(),
password: password,
forceRelay: forceRelay,
isSharedPassword: isSharedPassword,
),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => RemotePage(
id: id, password: password, isSharedPassword: isSharedPassword),
),
);
}
}
}
@@ -2218,7 +2281,7 @@ Future<void> reloadAllWindows() async {
/// [Note]
/// Portable build is only available on Windows.
bool isRunningInPortableMode() {
if (!Platform.isWindows) {
if (!isWindows) {
return false;
}
return bool.hasEnvironment(kEnvPortableExecutable);
@@ -2231,7 +2294,7 @@ Future<void> onActiveWindowChanged() async {
if (rustDeskWinManager.getActiveWindows().isEmpty) {
// close all sub windows
try {
if (Platform.isLinux) {
if (isLinux) {
await Future.wait([
saveWindowPosition(WindowType.Main),
rustDeskWinManager.closeAllSubWindows()
@@ -2245,7 +2308,7 @@ Future<void> onActiveWindowChanged() async {
debugPrint("Start closing RustDesk...");
await windowManager.setPreventClose(false);
await windowManager.close();
if (Platform.isMacOS) {
if (isMacOS) {
RdPlatformChannel.instance.terminate();
}
}
@@ -2261,7 +2324,7 @@ Timer periodic_immediate(Duration duration, Future<void> Function() callback) {
/// return a human readable windows version
WindowsTarget getWindowsTarget(int buildNumber) {
if (!Platform.isWindows) {
if (!isWindows) {
return WindowsTarget.naw;
}
if (buildNumber >= 22000) {
@@ -2287,35 +2350,7 @@ WindowsTarget getWindowsTarget(int buildNumber) {
/// [Note]
/// Please use this function wrapped with `Platform.isWindows`.
int getWindowsTargetBuildNumber() {
final rtlGetVersion = DynamicLibrary.open('ntdll.dll').lookupFunction<
Void Function(Pointer<win32.OSVERSIONINFOEX>),
void Function(Pointer<win32.OSVERSIONINFOEX>)>('RtlGetVersion');
final osVersionInfo = getOSVERSIONINFOEXPointer();
rtlGetVersion(osVersionInfo);
int buildNumber = osVersionInfo.ref.dwBuildNumber;
calloc.free(osVersionInfo);
return buildNumber;
}
/// Get Windows OS version pointer
///
/// [Note]
/// Please use this function wrapped with `Platform.isWindows`.
Pointer<win32.OSVERSIONINFOEX> getOSVERSIONINFOEXPointer() {
final pointer = calloc<win32.OSVERSIONINFOEX>();
pointer.ref
..dwOSVersionInfoSize = sizeOf<win32.OSVERSIONINFOEX>()
..dwBuildNumber = 0
..dwMajorVersion = 0
..dwMinorVersion = 0
..dwPlatformId = 0
..szCSDVersion = ''
..wServicePackMajor = 0
..wServicePackMinor = 0
..wSuiteMask = 0
..wProductType = 0
..wReserved = 0;
return pointer;
return getWindowsTargetBuildNumber_();
}
/// Indicating we need to use compatible ui mode.
@@ -2323,7 +2358,7 @@ Pointer<win32.OSVERSIONINFOEX> getOSVERSIONINFOEXPointer() {
/// [Conditions]
/// - Windows 7, window will overflow when we use frameless ui.
bool get kUseCompatibleUiMode =>
Platform.isWindows &&
isWindows &&
const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion);
class ServerConfig {
@@ -2387,7 +2422,7 @@ Widget dialogButton(String text,
Widget? icon,
TextStyle? style,
ButtonStyle? buttonStyle}) {
if (isDesktop) {
if (isDesktop || isWebDesktop) {
if (isOutline) {
return icon == null
? OutlinedButton(
@@ -2429,19 +2464,20 @@ int versionCmp(String v1, String v2) {
}
String getWindowName({WindowType? overrideType}) {
final name = bind.mainGetAppNameSync();
switch (overrideType ?? kWindowType) {
case WindowType.Main:
return "RustDesk";
return name;
case WindowType.FileTransfer:
return "File Transfer - RustDesk";
return "File Transfer - $name";
case WindowType.PortForward:
return "Port Forward - RustDesk";
return "Port Forward - $name";
case WindowType.RemoteDesktop:
return "Remote Desktop - RustDesk";
return "Remote Desktop - $name";
default:
break;
}
return "RustDesk";
return name;
}
String getWindowNameWithId(String id, {WindowType? overrideType}) {
@@ -2452,7 +2488,7 @@ Future<void> updateSystemWindowTheme() async {
// Set system window theme for macOS.
final userPreference = MyTheme.getThemeModePreference();
if (userPreference != ThemeMode.system) {
if (Platform.isMacOS) {
if (isMacOS) {
await RdPlatformChannel.instance.changeSystemWindowTheme(
userPreference == ThemeMode.light
? SystemWindowTheme.light
@@ -2540,7 +2576,7 @@ void onCopyFingerprint(String value) {
Future<bool> callMainCheckSuperUserPermission() async {
bool checked = await bind.mainCheckSuperUserPermission();
if (Platform.isMacOS) {
if (isMacOS) {
await windowManager.show();
}
return checked;
@@ -2548,7 +2584,7 @@ Future<bool> callMainCheckSuperUserPermission() async {
Future<void> start_service(bool is_start) async {
bool checked = !bind.mainIsInstalled() ||
!Platform.isMacOS ||
!isMacOS ||
await callMainCheckSuperUserPermission();
if (checked) {
bind.mainSetOption(key: "stop-service", value: is_start ? "" : "Y");
@@ -2973,7 +3009,6 @@ Future<bool> setServerConfig(
await bind.mainSetOption(key: 'relay-server', value: config.relayServer);
await bind.mainSetOption(key: 'api-server', value: config.apiServer);
await bind.mainSetOption(key: 'key', value: config.key);
final newApiServer = await bind.mainGetApiServer();
if (oldApiServer.isNotEmpty &&
oldApiServer != newApiServer &&
@@ -3070,3 +3105,53 @@ Color? disabledTextColor(BuildContext context, bool enabled) {
? null
: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
}
// max 300 x 60
Widget loadLogo() {
return FutureBuilder<ByteData>(
future: rootBundle.load('assets/logo.png'),
builder: (BuildContext context, AsyncSnapshot<ByteData> snapshot) {
if (snapshot.hasData) {
final image = Image.asset(
'assets/logo.png',
fit: BoxFit.contain,
errorBuilder: (ctx, error, stackTrace) {
return Container();
},
);
return Container(
constraints: BoxConstraints(maxWidth: 300, maxHeight: 60),
child: image,
).marginOnly(left: 12, right: 12, top: 12);
}
return const Offstage();
});
}
Widget loadIcon(double size) {
return Image.asset('assets/icon.png',
width: size,
height: size,
errorBuilder: (ctx, error, stackTrace) => SvgPicture.asset(
'assets/icon.svg',
width: size,
height: size,
));
}
var imcomingOnlyHomeSize = Size(280, 300);
Size getIncomingOnlyHomeSize() {
final magicWidth = isWindows ? 11.0 : 2.0;
final magicHeight = 10.0;
return imcomingOnlyHomeSize +
Offset(magicWidth, kDesktopRemoteTabBarHeight + magicHeight);
}
Size getIncomingOnlySettingsSize() {
return Size(768, 600);
}
bool isInHomePage() {
final controller = Get.find<DesktopTabController>();
return controller.state.value.selected == 0;
}

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart';
@@ -188,3 +189,79 @@ class RequestException implements Exception {
return "RequestException, statusCode: $statusCode, error: $cause";
}
}
enum ShareRule {
read(1),
readWrite(2),
fullControl(3);
const ShareRule(this.value);
final int value;
static String desc(int v) {
if (v == ShareRule.read.value) {
return translate('Read-only');
}
if (v == ShareRule.readWrite.value) {
return translate('Read/Write');
}
if (v == ShareRule.fullControl.value) {
return translate('Full Control');
}
return v.toString();
}
static String shortDesc(int v) {
if (v == ShareRule.read.value) {
return 'R';
}
if (v == ShareRule.readWrite.value) {
return 'RW';
}
if (v == ShareRule.fullControl.value) {
return 'F';
}
return v.toString();
}
static ShareRule? fromValue(int v) {
if (v == ShareRule.read.value) {
return ShareRule.read;
}
if (v == ShareRule.readWrite.value) {
return ShareRule.readWrite;
}
if (v == ShareRule.fullControl.value) {
return ShareRule.fullControl;
}
return null;
}
}
class AbProfile {
String guid;
String name;
String owner;
String? note;
int rule;
AbProfile(this.guid, this.name, this.owner, this.note, this.rule);
AbProfile.fromJson(Map<String, dynamic> json)
: guid = json['guid'] ?? '',
name = json['name'] ?? '',
owner = json['owner'] ?? '',
note = json['note'] ?? '',
rule = json['rule'] ?? 0;
}
class AbTag {
String name;
int color;
AbTag(this.name, this.color);
AbTag.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? '',
color = json['color'] ?? '';
}

View File

@@ -1,13 +1,16 @@
import 'dart:math';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart';
import 'package:flex_color_picker/flex_color_picker.dart';
@@ -43,27 +46,24 @@ class _AddressBookState extends State<AddressBook> {
child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login"))));
} else {
if (gFFI.abModel.abLoading.value && gFFI.abModel.emtpy) {
return const Center(
child: CircularProgressIndicator(),
);
}
return Column(
children: [
// NOT use Offstage to wrap LinearProgressIndicator
if (gFFI.abModel.retrying.value) LinearProgressIndicator(),
if (gFFI.abModel.currentAbLoading.value &&
gFFI.abModel.currentAbEmpty)
const LinearProgressIndicator(),
buildErrorBanner(context,
loading: gFFI.abModel.abLoading,
err: gFFI.abModel.pullError,
loading: gFFI.abModel.currentAbLoading,
err: gFFI.abModel.currentAbPullError,
retry: null,
close: () => gFFI.abModel.pullError.value = ''),
close: () => gFFI.abModel.currentAbPullError.value = ''),
buildErrorBanner(context,
loading: gFFI.abModel.abLoading,
err: gFFI.abModel.pushError,
retry: () => gFFI.abModel.pushAb(isRetry: true),
close: () => gFFI.abModel.pushError.value = ''),
loading: gFFI.abModel.currentAbLoading,
err: gFFI.abModel.currentAbPushError,
retry: null, // remove retry
close: () => gFFI.abModel.currentAbPushError.value = ''),
Expanded(
child: isDesktop
child: (isDesktop || isWebDesktop)
? _buildAddressBookDesktop()
: _buildAddressBookMobile())
],
@@ -82,19 +82,23 @@ class _AddressBookState extends State<AddressBook> {
border: Border.all(
color: Theme.of(context).colorScheme.background)),
child: Container(
width: 150,
width: 200,
height: double.infinity,
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
_buildTagHeader().marginOnly(left: 8.0, right: 0),
_buildAbDropdown(),
_buildTagHeader().marginOnly(
left: 8.0,
right: gFFI.abModel.legacyMode.value ? 8.0 : 0,
top: gFFI.abModel.legacyMode.value ? 8.0 : 0),
Expanded(
child: Container(
width: double.infinity,
height: double.infinity,
child: _buildTags(),
),
)
),
_buildAbPermission(),
],
),
),
@@ -119,11 +123,13 @@ class _AddressBookState extends State<AddressBook> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildAbDropdown(),
_buildTagHeader().marginOnly(left: 8.0, right: 0),
Container(
width: double.infinity,
child: _buildTags(),
),
_buildAbPermission(),
],
),
),
@@ -133,6 +139,131 @@ class _AddressBookState extends State<AddressBook> {
);
}
Widget _buildAbPermission() {
icon(IconData data, String tooltip) {
return Tooltip(
message: translate(tooltip),
waitDuration: Duration.zero,
child: Icon(data, size: 12.0).marginSymmetric(horizontal: 2.0));
}
return Obx(() {
if (gFFI.abModel.legacyMode.value) return Offstage();
if (gFFI.abModel.current.isPersonal()) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
icon(Icons.cloud_off, "Personal"),
],
);
} else {
List<Widget> children = [];
final rule = gFFI.abModel.current.sharedProfile()?.rule;
if (rule == ShareRule.read.value) {
children.add(
icon(Icons.visibility, ShareRule.desc(ShareRule.read.value)));
} else if (rule == ShareRule.readWrite.value) {
children
.add(icon(Icons.edit, ShareRule.desc(ShareRule.readWrite.value)));
} else if (rule == ShareRule.fullControl.value) {
children.add(icon(
Icons.security, ShareRule.desc(ShareRule.fullControl.value)));
}
final owner = gFFI.abModel.current.sharedProfile()?.owner;
if (owner != null) {
children.add(icon(Icons.person, "${translate("Owner")}: $owner"));
}
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: children,
);
}
});
}
Widget _buildAbDropdown() {
if (gFFI.abModel.legacyMode.value) {
return Offstage();
}
final names = gFFI.abModel.addressBookNames();
if (!names.contains(gFFI.abModel.currentName.value)) {
return Offstage();
}
final TextEditingController textEditingController = TextEditingController();
return DropdownButton2<String>(
value: gFFI.abModel.currentName.value,
onChanged: (value) {
if (value != null) {
gFFI.abModel.setCurrentName(value);
bind.setLocalFlutterOption(k: 'current-ab-name', v: value);
}
},
underline: Container(
height: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
buttonStyleData: ButtonStyleData(height: 48),
menuItemStyleData: MenuItemStyleData(height: 36),
items: names
.map((e) => DropdownMenuItem(
value: e,
child: Row(
children: [
Expanded(
child: Tooltip(
waitDuration: Duration(milliseconds: 500),
message: gFFI.abModel.translatedName(e),
child: Text(
gFFI.abModel.translatedName(e),
style: TextStyle(fontSize: 14.0),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)),
),
],
)))
.toList(),
isExpanded: true,
dropdownSearchData: DropdownSearchData(
searchController: textEditingController,
searchInnerWidgetHeight: 50,
searchInnerWidget: Container(
height: 50,
padding: const EdgeInsets.only(
top: 8,
bottom: 4,
right: 8,
left: 8,
),
child: TextFormField(
expands: true,
maxLines: null,
controller: textEditingController,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
hintText: translate('Search'),
hintStyle: const TextStyle(fontSize: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
searchMatchFn: (item, searchValue) {
return item.value
.toString()
.toLowerCase()
.contains(searchValue.toLowerCase());
},
),
);
}
Widget _buildTagHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -154,11 +285,12 @@ class _AddressBookState extends State<AddressBook> {
return Obx(() {
final List tags;
if (gFFI.abModel.sortTags.value) {
tags = gFFI.abModel.tags.toList();
tags = gFFI.abModel.currentAbTags.toList();
tags.sort();
} else {
tags = gFFI.abModel.tags;
tags = gFFI.abModel.currentAbTags;
}
final editPermission = gFFI.abModel.current.canWrite();
tagBuilder(String e) {
return AddressBookTag(
name: e,
@@ -169,7 +301,8 @@ class _AddressBookState extends State<AddressBook> {
} else {
gFFI.abModel.selectedTags.add(e);
}
});
},
showActionMenu: editPermission);
}
final gridView = DynamicGridView.builder(
@@ -181,7 +314,7 @@ class _AddressBookState extends State<AddressBook> {
return tagBuilder(e);
});
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop
return (isDesktop || isWebDesktop)
? gridView
: LimitedBox(maxHeight: maxHeight, child: gridView);
});
@@ -193,7 +326,7 @@ class _AddressBookState extends State<AddressBook> {
alignment: Alignment.topLeft,
child: AddressBookPeersView(
menuPadding: widget.menuPadding,
initPeers: gFFI.abModel.peers,
getInitPeers: () => gFFI.abModel.currentAbPeers,
)),
);
}
@@ -207,7 +340,7 @@ class _AddressBookState extends State<AddressBook> {
return shouldSyncAb();
},
setter: (bool v) async {
bind.mainSetLocalOption(key: syncAbOption, value: v ? 'Y' : '');
gFFI.abModel.setShouldAsync(v);
},
dismissOnClicked: true,
);
@@ -246,13 +379,22 @@ class _AddressBookState extends State<AddressBook> {
}
void _showMenu(RelativeRect pos) {
final canWrite = gFFI.abModel.current.canWrite();
final items = [
getEntry(translate("Add ID"), abAddId),
getEntry(translate("Add Tag"), abAddTag),
if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
sortMenuItem(),
syncMenuItem(),
filterMenuItem(),
if (!gFFI.abModel.legacyMode.value) MenuEntryDivider<String>(),
if (!gFFI.abModel.legacyMode.value)
getEntry(translate("ab_web_console_tip"), () async {
final url = await bind.mainGetApiServer();
if (await canLaunchUrlString(url)) {
launchUrlString(url);
}
}),
];
mod_menu.showMenu(
@@ -271,17 +413,20 @@ class _AddressBookState extends State<AddressBook> {
);
}
void abAddId() async {
if (gFFI.abModel.isFull(true)) {
void addIdToCurrentAb() async {
if (gFFI.abModel.isCurrentAbFull(true)) {
return;
}
var isInProgress = false;
var passwordVisible = false;
IDTextEditingController idController = IDTextEditingController(text: '');
TextEditingController aliasController = TextEditingController(text: '');
final tags = List.of(gFFI.abModel.tags);
TextEditingController passwordController = TextEditingController(text: '');
final tags = List.of(gFFI.abModel.currentAbTags);
var selectedTag = List<dynamic>.empty(growable: true).obs;
final style = TextStyle(fontSize: 14.0);
String? errorMsg;
final isCurrentAbShared = !gFFI.abModel.current.isPersonal();
gFFI.dialogManager.show((setState, close, context) {
submit() async {
@@ -293,22 +438,50 @@ class _AddressBookState extends State<AddressBook> {
if (id.isEmpty) {
// pass
} else {
if (gFFI.abModel.idContainBy(id)) {
if (gFFI.abModel.idContainByCurrent(id)) {
setState(() {
isInProgress = false;
errorMsg = translate('ID already exists');
});
return;
}
gFFI.abModel.addId(id, aliasController.text.trim(), selectedTag);
gFFI.abModel.pushAb();
this.setState(() {});
var password = '';
if (isCurrentAbShared) {
password = passwordController.text;
}
String? errMsg2 = await gFFI.abModel.addIdToCurrent(
id, aliasController.text.trim(), password, selectedTag);
if (errMsg2 != null) {
setState(() {
isInProgress = false;
errorMsg = errMsg2;
});
return;
}
// final currentPeers
}
close();
}
double marginBottom = 4;
row({required Widget lable, required Widget input}) {
return Row(
children: [
!isMobile
? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
child: lable.marginOnly(right: 10))
: SizedBox.shrink(),
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 200),
child: input),
),
],
).marginOnly(bottom: !isMobile ? 8 : 0);
}
return CustomAlertDialog(
title: Text(translate("Add ID")),
content: Column(
@@ -316,66 +489,103 @@ class _AddressBookState extends State<AddressBook> {
children: [
Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: Row(
children: [
Text(
'*',
style: TextStyle(color: Colors.red, fontSize: 14),
),
Text(
'ID',
style: style,
),
],
),
).marginOnly(bottom: marginBottom),
TextField(
controller: idController,
inputFormatters: [IDTextInputFormatter()],
decoration: InputDecoration(errorText: errorMsg),
),
Align(
alignment: Alignment.centerLeft,
child: Text(
row(
lable: Row(
children: [
Text(
'*',
style: TextStyle(color: Colors.red, fontSize: 14),
),
Text(
'ID',
style: style,
),
],
),
input: TextField(
controller: idController,
inputFormatters: [IDTextInputFormatter()],
decoration: InputDecoration(
labelText: !isMobile ? null : translate('ID'),
errorText: errorMsg,
errorMaxLines: 5),
)),
row(
lable: Text(
translate('Alias'),
style: style,
),
).marginOnly(top: 8, bottom: marginBottom),
TextField(
controller: aliasController,
input: TextField(
controller: aliasController,
decoration: InputDecoration(
labelText: !isMobile ? null : translate('Alias'),
)),
),
Align(
alignment: Alignment.centerLeft,
child: Text(
translate('Tags'),
style: style,
),
).marginOnly(top: 8, bottom: marginBottom),
Align(
alignment: Alignment.centerLeft,
child: Wrap(
children: tags
.map((e) => AddressBookTag(
name: e,
tags: selectedTag,
onTap: () {
if (selectedTag.contains(e)) {
selectedTag.remove(e);
} else {
selectedTag.add(e);
}
if (isCurrentAbShared)
row(
lable: Text(
translate('Password'),
style: style,
),
input: TextField(
controller: passwordController,
obscureText: !passwordVisible,
decoration: InputDecoration(
labelText: !isMobile ? null : translate('Password'),
suffixIcon: IconButton(
icon: Icon(
passwordVisible
? Icons.visibility
: Icons.visibility_off,
color: MyTheme.lightTheme.primaryColor),
onPressed: () {
setState(() {
passwordVisible = !passwordVisible;
});
},
showActionMenu: false))
.toList(growable: false),
),
),
)),
if (gFFI.abModel.currentAbTags.isNotEmpty)
Align(
alignment: Alignment.centerLeft,
child: Text(
translate('Tags'),
style: style,
),
).marginOnly(top: 8, bottom: marginBottom),
if (gFFI.abModel.currentAbTags.isNotEmpty)
Align(
alignment: Alignment.centerLeft,
child: Wrap(
children: tags
.map((e) => AddressBookTag(
name: e,
tags: selectedTag,
onTap: () {
if (selectedTag.contains(e)) {
selectedTag.remove(e);
} else {
selectedTag.add(e);
}
},
showActionMenu: false))
.toList(growable: false),
),
),
),
],
),
const SizedBox(
height: 4.0,
),
if (!gFFI.abModel.current.isPersonal())
Row(children: [
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
Text(
translate('share_warning_tip'),
style: TextStyle(fontSize: 12),
)
]).marginSymmetric(vertical: 10),
// NOT use Offstage to wrap LinearProgressIndicator
if (isInProgress) const LinearProgressIndicator(),
],
@@ -407,10 +617,7 @@ class _AddressBookState extends State<AddressBook> {
} else {
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
field = tags.join(',');
for (final tag in tags) {
gFFI.abModel.addTag(tag);
}
gFFI.abModel.pushAb();
gFFI.abModel.addTags(tags);
// final currentPeers
}
close();
@@ -491,7 +698,7 @@ class AddressBookTag extends StatelessWidget {
child: Obx(() => Container(
decoration: BoxDecoration(
color: tags.contains(name)
? gFFI.abModel.getTagColor(name)
? gFFI.abModel.getCurrentAbTagColor(name)
: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(4)),
margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
@@ -506,7 +713,7 @@ class AddressBookTag extends StatelessWidget {
shape: BoxShape.circle,
color: tags.contains(name)
? Colors.white
: gFFI.abModel.getTagColor(name)),
: gFFI.abModel.getCurrentAbTagColor(name)),
).marginOnly(right: radius / 2),
Expanded(
child: Text(name,
@@ -530,7 +737,8 @@ class AddressBookTag extends StatelessWidget {
if (newName == null || newName.isEmpty) {
return translate('Can not be empty');
}
if (newName != name && gFFI.abModel.tags.contains(newName)) {
if (newName != name &&
gFFI.abModel.currentAbTags.contains(newName)) {
return translate('Already exists');
}
return null;
@@ -538,7 +746,6 @@ class AddressBookTag extends StatelessWidget {
onSubmit: (String newName) {
if (name != newName) {
gFFI.abModel.renameTag(name, newName);
gFFI.abModel.pushAb();
}
Future.delayed(Duration.zero, () => Get.back());
},
@@ -548,7 +755,7 @@ class AddressBookTag extends StatelessWidget {
}),
getEntry(translate(translate('Change Color')), () async {
final model = gFFI.abModel;
Color oldColor = model.getTagColor(name);
Color oldColor = model.getCurrentAbTagColor(name);
Color newColor = await showColorPickerDialog(
context,
oldColor,
@@ -567,12 +774,10 @@ class AddressBookTag extends StatelessWidget {
);
if (oldColor != newColor) {
model.setTagColor(name, newColor);
model.pushAb();
}
}),
getEntry(translate("Delete"), () {
gFFI.abModel.deleteTag(name);
gFFI.abModel.pushAb();
Future.delayed(Duration.zero, () => Get.back());
}),
];

View File

@@ -9,9 +9,6 @@ import 'package:flutter_hbb/common/widgets/peer_card.dart';
Future<List<Peer>> getAllPeers() async {
Map<String, dynamic> recentPeers = jsonDecode(bind.mainLoadRecentPeersSync());
Map<String, dynamic> lanPeers = jsonDecode(bind.mainLoadLanPeersSync());
Map<String, dynamic> abPeers = jsonDecode(bind.mainLoadAbSync());
Map<String, dynamic> groupPeers = jsonDecode(bind.mainLoadGroupSync());
Map<String, dynamic> combinedPeers = {};
void mergePeers(Map<String, dynamic> peers) {
@@ -42,8 +39,16 @@ Future<List<Peer>> getAllPeers() async {
mergePeers(recentPeers);
mergePeers(lanPeers);
mergePeers(abPeers);
mergePeers(groupPeers);
for (var p in gFFI.abModel.allPeers()) {
if (!combinedPeers.containsKey(p.id)) {
combinedPeers[p.id] = p.toJson();
}
}
for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) {
if (!combinedPeers.containsKey(p.id)) {
combinedPeers[p.id] = p.toJson();
}
}
List<Peer> parsedPeers = [];
@@ -181,7 +186,7 @@ class AutocompletePeerTileState extends State<AutocompletePeerTile> {
],
))));
final colors = _frontN(widget.peer.tags, 25)
.map((e) => gFFI.abModel.getTagColor(e))
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList();
return Tooltip(
message: isMobile

View File

@@ -1,12 +1,14 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
@@ -78,7 +80,7 @@ void changeIdDialog() {
final Iterable violations = rules.where((r) => !r.validate(newId));
if (violations.isNotEmpty) {
setState(() {
msg = isDesktop
msg = (isDesktop || isWebDesktop)
? '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}'
: violations.map((r) => r.name).join(', ');
});
@@ -103,7 +105,7 @@ void changeIdDialog() {
}
setState(() {
isInProgress = false;
msg = isDesktop
msg = (isDesktop || isWebDesktop)
? '${translate('Prompt')}: ${translate(status)}'
: translate(status);
});
@@ -140,7 +142,7 @@ void changeIdDialog() {
const SizedBox(
height: 8.0,
),
isDesktop
(isDesktop || isWebDesktop)
? Obx(() => Wrap(
runSpacing: 8,
spacing: 4,
@@ -622,7 +624,7 @@ class _DialogVerificationCodeField extends State<DialogVerificationCodeField> {
// software secure keyboard will take the focus since flutter 3.13
// request focus again when android account password obtain focus
if (Platform.isAndroid && widget.reRequestFocus) {
if (isAndroid && widget.reRequestFocus) {
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
_timerReRequestFocus?.cancel();
@@ -691,7 +693,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
}
// software secure keyboard will take the focus since flutter 3.13
// request focus again when android account password obtain focus
if (Platform.isAndroid && widget.reRequestFocus) {
if (isAndroid && widget.reRequestFocus) {
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
_timerReRequestFocus?.cancel();
@@ -1106,7 +1108,7 @@ void showRequestElevationDialog(
errorText: errPwd.isEmpty ? null : errPwd.value,
),
],
).marginOnly(left: isDesktop ? 35 : 0),
).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0),
).marginOnly(top: 10),
],
),
@@ -1583,7 +1585,7 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
}
void deletePeerConfirmDialog(Function onSubmit, String title) async {
void deleteConfirmDialog(Function onSubmit, String title) async {
gFFI.dialogManager.show(
(setState, close, context) {
submit() async {
@@ -1631,7 +1633,7 @@ void editAbTagDialog(
List<dynamic> currentTags, Function(List<dynamic>) onSubmit) {
var isInProgress = false;
final tags = List.of(gFFI.abModel.tags);
final tags = List.of(gFFI.abModel.currentAbTags);
var selectedTag = currentTags.obs;
gFFI.dialogManager.show((setState, close, context) {
@@ -1909,3 +1911,207 @@ void showWindowsSessionsDialog(
);
});
}
void addPeersToAbDialog(
List<Peer> peers,
) async {
Future<bool> addTo(String abname) async {
final mapList = peers.map((e) {
var json = e.toJson();
// remove password when add to another address book to avoid re-share
json.remove('password');
json.remove('hash');
return json;
}).toList();
final errMsg = await gFFI.abModel.addPeersTo(mapList, abname);
if (errMsg == null) {
showToast(translate('Successful'));
return true;
} else {
BotToast.showText(text: errMsg, contentColor: Colors.red);
return false;
}
}
// if only one address book and it is personal, add to it directly
if (gFFI.abModel.addressbooks.length == 1 &&
gFFI.abModel.current.isPersonal()) {
await addTo(gFFI.abModel.currentName.value);
return;
}
RxBool isInProgress = false.obs;
final names = gFFI.abModel.addressBooksCanWrite();
RxString currentName = gFFI.abModel.currentName.value.obs;
TextEditingController controller = TextEditingController();
if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) {
names.remove(currentName.value);
}
if (names.isEmpty) {
debugPrint('no address book to add peers to, should not happen');
return;
}
if (!names.contains(currentName.value)) {
currentName.value = names[0];
}
gFFI.dialogManager.show((setState, close, context) {
submit() async {
if (controller.text != gFFI.abModel.translatedName(currentName.value)) {
BotToast.showText(
text: 'illegal address book name: ${controller.text}',
contentColor: Colors.red);
return;
}
isInProgress.value = true;
if (await addTo(currentName.value)) {
close();
}
isInProgress.value = false;
}
cancel() {
close();
}
return CustomAlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(IconFont.addressBook, color: MyTheme.accent),
Text(translate('Add to address book')).paddingOnly(left: 10),
],
),
content: Obx(() => Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// https://github.com/flutter/flutter/issues/145081
DropdownMenu(
initialSelection: currentName.value,
onSelected: (value) {
if (value != null) {
currentName.value = value;
}
},
dropdownMenuEntries: names
.map((e) => DropdownMenuEntry(
value: e, label: gFFI.abModel.translatedName(e)))
.toList(),
inputDecorationTheme: InputDecorationTheme(
isDense: true, border: UnderlineInputBorder()),
enableFilter: true,
controller: controller,
),
// NOT use Offstage to wrap LinearProgressIndicator
isInProgress.value ? const LinearProgressIndicator() : Offstage()
],
)),
actions: [
dialogButton(
"Cancel",
icon: Icon(Icons.close_rounded),
onPressed: cancel,
isOutline: true,
),
dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed: submit,
),
],
onSubmit: submit,
onCancel: cancel,
);
});
}
void setSharedAbPasswordDialog(String abName, Peer peer) {
TextEditingController controller = TextEditingController(text: '');
RxBool isInProgress = false.obs;
RxBool isInputEmpty = true.obs;
bool passwordVisible = false;
controller.addListener(() {
isInputEmpty.value = controller.text.isEmpty;
});
gFFI.dialogManager.show((setState, close, context) {
change(String password) async {
isInProgress.value = true;
bool res =
await gFFI.abModel.changeSharedPassword(abName, peer.id, password);
isInProgress.value = false;
if (res) {
showToast(translate('Successful'));
}
close();
}
cancel() {
close();
}
return CustomAlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.key, color: MyTheme.accent),
Text(translate(peer.password.isEmpty
? 'Set shared password'
: 'Change Password'))
.paddingOnly(left: 10),
],
),
content: Obx(() => Column(children: [
TextField(
controller: controller,
autofocus: true,
obscureText: !passwordVisible,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(
passwordVisible ? Icons.visibility : Icons.visibility_off,
color: MyTheme.lightTheme.primaryColor),
onPressed: () {
setState(() {
passwordVisible = !passwordVisible;
});
},
),
),
),
if (!gFFI.abModel.current.isPersonal())
Row(children: [
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
Text(
translate('share_warning_tip'),
style: TextStyle(fontSize: 12),
)
]).marginSymmetric(vertical: 10),
// NOT use Offstage to wrap LinearProgressIndicator
isInProgress.value ? const LinearProgressIndicator() : Offstage()
])),
actions: [
dialogButton(
"Cancel",
icon: Icon(Icons.close_rounded),
onPressed: cancel,
isOutline: true,
),
if (peer.password.isNotEmpty)
dialogButton(
"Remove",
icon: Icon(Icons.delete_outline_rounded),
onPressed: () => change(''),
buttonStyle: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.red)),
),
Obx(() => dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed:
isInputEmpty.value ? null : () => change(controller.text),
)),
],
onSubmit: isInputEmpty.value ? null : () => change(controller.text),
onCancel: cancel,
);
});
}

View File

@@ -47,7 +47,10 @@ class _MyGroupState extends State<MyGroup> {
err: gFFI.groupModel.groupLoadError,
retry: null,
close: () => gFFI.groupModel.groupLoadError.value = ''),
Expanded(child: isDesktop ? _buildDesktop() : _buildMobile())
Expanded(
child: (isDesktop || isWebDesktop)
? _buildDesktop()
: _buildMobile())
],
);
});
@@ -83,7 +86,7 @@ class _MyGroupState extends State<MyGroup> {
alignment: Alignment.topLeft,
child: MyGroupPeerView(
menuPadding: widget.menuPadding,
initPeers: gFFI.groupModel.peers)),
getInitPeers: () => gFFI.groupModel.peers)),
)
],
);
@@ -115,7 +118,7 @@ class _MyGroupState extends State<MyGroup> {
alignment: Alignment.topLeft,
child: MyGroupPeerView(
menuPadding: widget.menuPadding,
initPeers: gFFI.groupModel.peers)),
getInitPeers: () => gFFI.groupModel.peers)),
)
],
);
@@ -164,7 +167,7 @@ class _MyGroupState extends State<MyGroup> {
itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop
return (isDesktop || isWebDesktop)
? listView
: LimitedBox(maxHeight: maxHeight, child: listView);
});

View File

@@ -54,7 +54,7 @@ class DraggableChatWindow extends StatelessWidget {
resizeToAvoidBottomInset: false,
appBar: CustomAppBar(
onPanUpdate: onPanUpdate,
appBar: isDesktop
appBar: (isDesktop || isWebDesktop)
? _buildDesktopAppBar(context)
: _buildMobileAppBar(context),
),

View File

@@ -1,5 +1,4 @@
import 'dart:io';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
@@ -52,7 +51,7 @@ class _PeerCardState extends State<_PeerCard>
@override
Widget build(BuildContext context) {
super.build(context);
if (isDesktop) {
if (isDesktop || isWebDesktop) {
return _buildDesktop();
} else {
return _buildMobile();
@@ -70,12 +69,12 @@ class _PeerCardState extends State<_PeerCard>
peerTabModel.select(peer);
} else {
if (!isWebDesktop) {
connectInPeerTab(context, peer.id, widget.tab);
connectInPeerTab(context, peer, widget.tab);
}
}
},
onDoubleTap: isWebDesktop
? () => connectInPeerTab(context, peer.id, widget.tab)
? () => connectInPeerTab(context, peer, widget.tab)
: null,
onLongPress: () {
peerTabModel.select(peer);
@@ -140,21 +139,30 @@ class _PeerCardState extends State<_PeerCard>
mainAxisSize: MainAxisSize.max,
children: [
Container(
decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: isMobile
? BorderRadius.circular(_tileRadius)
: BorderRadius.only(
topLeft: Radius.circular(_tileRadius),
bottomLeft: Radius.circular(_tileRadius),
decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: isMobile
? BorderRadius.circular(_tileRadius)
: BorderRadius.only(
topLeft: Radius.circular(_tileRadius),
bottomLeft: Radius.circular(_tileRadius),
),
),
alignment: Alignment.center,
width: isMobile ? 50 : 42,
height: isMobile ? 50 : null,
child: Stack(
children: [
getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
.paddingAll(6),
if (_shouldBuildPasswordIcon(peer))
Positioned(
top: 1,
left: 1,
child: Icon(Icons.key, size: 6, color: Colors.white),
),
),
alignment: Alignment.center,
width: isMobile ? 50 : 42,
height: isMobile ? 50 : null,
child: getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
.paddingAll(6),
),
],
)),
Expanded(
child: Container(
decoration: BoxDecoration(
@@ -199,8 +207,9 @@ class _PeerCardState extends State<_PeerCard>
)
],
);
final colors =
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
final colors = _frontN(peer.tags, 25)
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList();
return Tooltip(
message: isMobile
? ''
@@ -310,14 +319,21 @@ class _PeerCardState extends State<_PeerCard>
),
);
final colors =
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
final colors = _frontN(peer.tags, 25)
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList();
return Tooltip(
message: peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '',
child: Stack(children: [
child,
if (_shouldBuildPasswordIcon(peer))
Positioned(
top: 4,
left: 12,
child: Icon(Icons.key, size: 12, color: Colors.white),
),
if (colors.isNotEmpty)
Positioned(
top: 4,
@@ -401,6 +417,12 @@ class _PeerCardState extends State<_PeerCard>
onPointerUp: (_) => _showPeerMenu(peer.id),
child: build_more(context));
bool _shouldBuildPasswordIcon(Peer peer) {
if (gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index) return false;
if (gFFI.abModel.current.isPersonal()) return false;
return peer.password.isNotEmpty;
}
/// Show the peer menu and handle user's choice.
/// User might remove the peer or send a file to the peer.
void _showPeerMenu(String id) async {
@@ -431,7 +453,7 @@ abstract class BasePeerCard extends StatelessWidget {
peer: peer,
tab: tab,
connect: (BuildContext context, String id) =>
connectInPeerTab(context, id, tab),
connectInPeerTab(context, peer, tab),
popupMenuEntryBuilder: _buildPopupMenuEntry,
);
}
@@ -453,7 +475,6 @@ abstract class BasePeerCard extends StatelessWidget {
MenuEntryBase<String> _connectCommonAction(
BuildContext context,
String id,
String title, {
bool isFileTransfer = false,
bool isTcpTunneling = false,
@@ -467,7 +488,7 @@ abstract class BasePeerCard extends StatelessWidget {
proc: () {
connectInPeerTab(
context,
peer.id,
peer,
tab,
isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling,
@@ -480,10 +501,9 @@ abstract class BasePeerCard extends StatelessWidget {
}
@protected
MenuEntryBase<String> _connectAction(BuildContext context, Peer peer) {
MenuEntryBase<String> _connectAction(BuildContext context) {
return _connectCommonAction(
context,
peer.id,
(peer.alias.isEmpty
? translate('Connect')
: '${translate('Connect')} ${peer.id}'),
@@ -491,20 +511,18 @@ abstract class BasePeerCard extends StatelessWidget {
}
@protected
MenuEntryBase<String> _transferFileAction(BuildContext context, String id) {
MenuEntryBase<String> _transferFileAction(BuildContext context) {
return _connectCommonAction(
context,
id,
translate('Transfer file'),
isFileTransfer: true,
);
}
@protected
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context, String id) {
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) {
return _connectCommonAction(
context,
id,
translate('TCP tunneling'),
isTcpTunneling: true,
);
@@ -541,7 +559,7 @@ abstract class BasePeerCard extends StatelessWidget {
],
)),
proc: () {
connectInPeerTab(context, id, tab, isRDP: true);
connectInPeerTab(context, peer, tab, isRDP: true);
},
padding: menuPadding,
dismissOnClicked: true,
@@ -648,9 +666,8 @@ abstract class BasePeerCard extends StatelessWidget {
onSubmit: (String newName) async {
if (newName != oldName) {
if (tab == PeerTabIndex.ab) {
gFFI.abModel.changeAlias(id: id, alias: newName);
await gFFI.abModel.changeAlias(id: id, alias: newName);
await bind.mainSetPeerAlias(id: id, alias: newName);
gFFI.abModel.pushAb();
} else {
await bind.mainSetPeerAlias(id: id, alias: newName);
showToast(translate('Successful'));
@@ -702,11 +719,7 @@ abstract class BasePeerCard extends StatelessWidget {
await bind.mainLoadLanPeers();
break;
case PeerTabIndex.ab:
gFFI.abModel.deletePeer(id);
final future = gFFI.abModel.pushAb();
if (await bind.mainPeerExists(id: peer.id)) {
gFFI.abModel.reSyncToast(future);
}
await gFFI.abModel.deletePeers([id]);
break;
case PeerTabIndex.group:
break;
@@ -716,7 +729,7 @@ abstract class BasePeerCard extends StatelessWidget {
}
}
deletePeerConfirmDialog(onSubmit,
deleteConfirmDialog(onSubmit,
'${translate('Delete')} "${peer.alias.isEmpty ? formatID(peer.id) : peer.alias}"?');
},
padding: menuPadding,
@@ -732,14 +745,16 @@ abstract class BasePeerCard extends StatelessWidget {
style: style,
),
proc: () async {
bool result = gFFI.abModel.changePassword(id, '');
bool succ = await gFFI.abModel.changePersonalHashPassword(id, '');
await bind.mainForgetPassword(id: id);
bool toast = false;
if (result) {
toast = tab == PeerTabIndex.ab;
gFFI.abModel.pushAb(toastIfFail: toast, toastIfSucc: toast);
if (succ) {
showToast(translate('Successful'));
} else {
if (tab.index == PeerTabIndex.ab.index) {
BotToast.showText(
contentColor: Colors.red, text: translate("Failed"));
}
}
if (!toast) showToast(translate('Successful'));
},
padding: menuPadding,
dismissOnClicked: true,
@@ -824,13 +839,7 @@ abstract class BasePeerCard extends StatelessWidget {
),
proc: () {
() async {
if (gFFI.abModel.isFull(true)) {
return;
}
if (!gFFI.abModel.idContainBy(peer.id)) {
gFFI.abModel.addPeer(peer);
gFFI.abModel.pushAb();
}
addPeersToAbDialog([Peer.copy(peer)]);
}();
},
padding: menuPadding,
@@ -858,25 +867,29 @@ class RecentPeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer),
_transferFileAction(context, peer.id),
_connectAction(context),
if (!isWeb) _transferFileAction(context),
];
final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
menuItems.add(_tcpTunnelingAction(context));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
if (!isWeb) {
menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
if (Platform.isWindows) {
if (isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
if (isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id));
}
if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
@@ -888,9 +901,7 @@ class RecentPeerCard extends BasePeerCard {
}
if (gFFI.userModel.userName.isNotEmpty) {
if (!gFFI.abModel.idContainBy(peer.id)) {
menuItems.add(_addToAb(peer));
}
menuItems.add(_addToAb(peer));
}
menuItems.add(MenuEntryDivider());
@@ -915,22 +926,26 @@ class FavoritePeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer),
_transferFileAction(context, peer.id),
_connectAction(context),
if (!isWeb) _transferFileAction(context),
];
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
menuItems.add(_tcpTunnelingAction(context));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
if (!isWeb) {
menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
if (Platform.isWindows) {
if (isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
if (isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id));
}
if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
@@ -939,9 +954,7 @@ class FavoritePeerCard extends BasePeerCard {
}));
if (gFFI.userModel.userName.isNotEmpty) {
if (!gFFI.abModel.idContainBy(peer.id)) {
menuItems.add(_addToAb(peer));
}
menuItems.add(_addToAb(peer));
}
menuItems.add(MenuEntryDivider());
@@ -966,22 +979,24 @@ class DiscoveredPeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer),
_transferFileAction(context, peer.id),
_connectAction(context),
if (!isWeb) _transferFileAction(context),
];
final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
menuItems.add(_tcpTunnelingAction(context));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
if (!isWeb) {
menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
menuItems.add(_wolAction(peer.id));
if (Platform.isWindows) {
if (isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
@@ -992,9 +1007,7 @@ class DiscoveredPeerCard extends BasePeerCard {
}
if (gFFI.userModel.userName.isNotEmpty) {
if (!gFFI.abModel.idContainBy(peer.id)) {
menuItems.add(_addToAb(peer));
}
menuItems.add(_addToAb(peer));
}
menuItems.add(MenuEntryDivider());
@@ -1019,37 +1032,55 @@ class AddressBookPeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer),
_transferFileAction(context, peer.id),
_connectAction(context),
if (!isWeb) _transferFileAction(context),
];
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
menuItems.add(_tcpTunnelingAction(context));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
// menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
if (Platform.isWindows) {
if (isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
if (peer.hash.isNotEmpty) {
menuItems.add(_unrememberPasswordAction(peer.id));
if (gFFI.abModel.current.canWrite()) {
menuItems.add(MenuEntryDivider());
if (isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id));
}
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
if (!gFFI.abModel.current.isPersonal()) {
menuItems.add(_changeSharedAbPassword());
}
if (gFFI.abModel.currentAbTags.isNotEmpty) {
menuItems.add(_editTagAction(peer.id));
}
}
if (gFFI.abModel.tags.isNotEmpty) {
menuItems.add(_editTagAction(peer.id));
final addressbooks = gFFI.abModel.addressBooksCanWrite();
if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) {
addressbooks.remove(gFFI.abModel.currentName.value);
}
if (addressbooks.isNotEmpty) {
menuItems.add(_addToAb(peer));
}
menuItems.add(_existIn());
if (gFFI.abModel.current.canWrite()) {
menuItems.add(MenuEntryDivider());
menuItems.add(_removeAction(peer.id));
}
menuItems.add(MenuEntryDivider());
menuItems.add(_removeAction(peer.id));
return menuItems;
}
// address book does not need to update
@protected
@override
void _update() => gFFI.abModel.pullAb(quiet: true);
void _update() =>
{}; //gFFI.abModel.pullAb(force: ForcePullAb.current, quiet: true);
@protected
MenuEntryBase<String> _editTagAction(String id) {
@@ -1060,8 +1091,7 @@ class AddressBookPeerCard extends BasePeerCard {
),
proc: () {
editAbTagDialog(gFFI.abModel.getPeerTags(id), (selectedTag) async {
gFFI.abModel.changeTagForPeer(id, selectedTag);
gFFI.abModel.pushAb();
await gFFI.abModel.changeTagForPeers([id], selectedTag);
});
},
padding: super.menuPadding,
@@ -1073,6 +1103,53 @@ class AddressBookPeerCard extends BasePeerCard {
@override
Future<String> _getAlias(String id) async =>
gFFI.abModel.find(id)?.alias ?? '';
MenuEntryBase<String> _changeSharedAbPassword() {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate(
peer.password.isEmpty ? 'Set shared password' : 'Change Password'),
style: style,
),
proc: () {
setSharedAbPasswordDialog(gFFI.abModel.currentName.value, peer);
},
padding: super.menuPadding,
dismissOnClicked: true,
);
}
MenuEntryBase<String> _existIn() {
final names = gFFI.abModel.idExistIn(peer.id);
final text = names.join(', ');
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Exist in'),
style: style,
),
proc: () {
gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text(translate('Exist in')),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text(text)]),
actions: [
dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed: close,
),
],
onSubmit: close,
onCancel: close,
);
});
},
padding: super.menuPadding,
dismissOnClicked: true,
);
}
}
class MyGroupPeerCard extends BasePeerCard {
@@ -1087,18 +1164,18 @@ class MyGroupPeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer),
_transferFileAction(context, peer.id),
_connectAction(context),
if (!isWeb) _transferFileAction(context),
];
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
menuItems.add(_tcpTunnelingAction(context));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
// menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
if (Platform.isWindows) {
if (isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
// menuItems.add(MenuEntryDivider());
@@ -1107,9 +1184,7 @@ class MyGroupPeerCard extends BasePeerCard {
// menuItems.add(_unrememberPasswordAction(peer.id));
// }
if (gFFI.userModel.userName.isNotEmpty) {
if (!gFFI.abModel.idContainBy(peer.id)) {
menuItems.add(_addToAb(peer));
}
menuItems.add(_addToAb(peer));
}
return menuItems;
}
@@ -1176,7 +1251,7 @@ void _rdpDialog(String id) async {
).marginOnly(bottom: isDesktop ? 8 : 0),
Row(
children: [
isDesktop
(isDesktop || isWebDesktop)
? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
@@ -1187,15 +1262,17 @@ void _rdpDialog(String id) async {
Expanded(
child: TextField(
decoration: InputDecoration(
labelText: isDesktop ? null : translate('Username')),
labelText: (isDesktop || isWebDesktop)
? null
: translate('Username')),
controller: userController,
),
),
],
).marginOnly(bottom: isDesktop ? 8 : 0),
).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0),
Row(
children: [
isDesktop
(isDesktop || isWebDesktop)
? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
@@ -1207,7 +1284,9 @@ void _rdpDialog(String id) async {
child: Obx(() => TextField(
obscureText: secure.value,
decoration: InputDecoration(
labelText: isDesktop ? null : translate('Password'),
labelText: (isDesktop || isWebDesktop)
? null
: translate('Password'),
suffixIcon: IconButton(
onPressed: () => secure.value = !secure.value,
icon: Icon(secure.value
@@ -1305,24 +1384,32 @@ class TagPainter extends CustomPainter {
}
}
void connectInPeerTab(BuildContext context, String id, PeerTabIndex tab,
void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
{bool isFileTransfer = false,
bool isTcpTunneling = false,
bool isRDP = false}) async {
var password = '';
bool isSharedPassword = false;
if (tab == PeerTabIndex.ab) {
// If recent peer's alias is empty, set it to ab's alias
// Because the platform is not set, it may not take effect, but it is more important not to display if the connection is not successful
Peer? p = gFFI.abModel.find(id);
if (p != null &&
p.alias.isNotEmpty &&
(await bind.mainGetPeerOption(id: id, key: "alias")).isEmpty) {
if (peer.alias.isNotEmpty &&
(await bind.mainGetPeerOption(id: peer.id, key: "alias")).isEmpty) {
await bind.mainSetPeerAlias(
id: id,
alias: p.alias,
id: peer.id,
alias: peer.alias,
);
}
if (!gFFI.abModel.current.isPersonal()) {
if (peer.password.isNotEmpty) {
password = peer.password;
isSharedPassword = true;
}
}
}
connect(context, id,
connect(context, peer.id,
password: password,
isSharedPassword: isSharedPassword,
isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP);

View File

@@ -13,6 +13,7 @@ import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_svg/flutter_svg.dart';
@@ -36,7 +37,7 @@ class _TabEntry {
}
EdgeInsets? _menuPadding() {
return isDesktop ? kDesktopMenuPadding : null;
return (isDesktop || isWebDesktop) ? kDesktopMenuPadding : null;
}
class _PeerTabPageState extends State<PeerTabPage>
@@ -61,7 +62,9 @@ class _PeerTabPageState extends State<PeerTabPage>
AddressBook(
menuPadding: _menuPadding(),
),
({dynamic hint}) => gFFI.abModel.pullAb(force: hint == null)),
({dynamic hint}) => gFFI.abModel.pullAb(
force: hint == null ? ForcePullAb.listAndCurrent : null,
quiet: false)),
_TabEntry(
MyGroup(
menuPadding: _menuPadding(),
@@ -110,7 +113,9 @@ class _PeerTabPageState extends State<PeerTabPage>
SizedBox(
height: 32,
child: Container(
padding: isDesktop ? null : EdgeInsets.symmetric(horizontal: 2),
padding: (isDesktop || isWebDesktop)
? null
: EdgeInsets.symmetric(horizontal: 2),
child: selectionWrap(Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@@ -124,7 +129,7 @@ class _PeerTabPageState extends State<PeerTabPage>
],
)),
),
).paddingOnly(right: isDesktop ? 12 : 0),
).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0),
_createPeersView(),
],
);
@@ -132,11 +137,13 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createSwitchBar(BuildContext context) {
final model = Provider.of<PeerTabModel>(context);
return ListView(
var counter = -1;
return ReorderableListView(
buildDefaultDragHandles: false,
onReorder: model.reorder,
scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(),
children: model.visibleIndexs.map((t) {
children: model.visibleEnabledOrderedIndexs.map((t) {
final selected = model.currentTab == t;
final color = selected
? MyTheme.tabbar(context).selectedTextColor
@@ -150,48 +157,54 @@ class _PeerTabPageState extends State<PeerTabPage>
border: Border(
bottom: BorderSide(width: 2, color: color!),
));
return Obx(() => InkWell(
child: Container(
decoration: (hover.value
? (selected ? decoBorder : deco)
: (selected ? decoBorder : null)),
child: Tooltip(
counter += 1;
return ReorderableDragStartListener(
key: ValueKey(t),
index: counter,
child: Obx(() => Tooltip(
preferBelow: false,
message: model.tabTooltip(t),
onTriggered: isMobile ? mobileShowTabVisibilityMenu : null,
child: Icon(model.tabIcon(t), color: color),
).paddingSymmetric(horizontal: 4),
).paddingSymmetric(horizontal: 4),
onTap: () async {
await handleTabSelection(t);
await bind.setLocalFlutterOption(
k: 'peer-tab-index', v: t.toString());
},
onHover: (value) => hover.value = value,
));
child: InkWell(
child: Container(
decoration: (hover.value
? (selected ? decoBorder : deco)
: (selected ? decoBorder : null)),
child: Icon(model.tabIcon(t), color: color)
.paddingSymmetric(horizontal: 4),
).paddingSymmetric(horizontal: 4),
onTap: () async {
await handleTabSelection(t);
await bind.setLocalFlutterOption(
k: PeerTabModel.kPeerTabIndex, v: t.toString());
},
onHover: (value) => hover.value = value,
),
)));
}).toList());
}
Widget _createPeersView() {
final model = Provider.of<PeerTabModel>(context);
Widget child;
if (model.visibleIndexs.isEmpty) {
if (model.visibleEnabledOrderedIndexs.isEmpty) {
child = visibleContextMenuListener(Row(
children: [Expanded(child: InkWell())],
));
} else {
if (model.visibleIndexs.contains(model.currentTab)) {
if (model.visibleEnabledOrderedIndexs.contains(model.currentTab)) {
child = entries[model.currentTab].widget;
} else {
debugPrint("should not happen! currentTab not in visibleIndexs");
Future.delayed(Duration.zero, () {
model.setCurrentTab(model.indexs[0]);
model.setCurrentTab(model.visibleEnabledOrderedIndexs[0]);
});
child = entries[0].widget;
}
}
return Expanded(
child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0));
child: child.marginSymmetric(
vertical: (isDesktop || isWebDesktop) ? 12.0 : 6.0));
}
Widget _createRefresh(
@@ -200,22 +213,23 @@ class _PeerTabPageState extends State<PeerTabPage>
final textColor = Theme.of(context).textTheme.titleLarge?.color;
return Offstage(
offstage: model.currentTab != index.index,
child: RefreshWidget(
onPressed: () {
if (gFFI.peerTabModel.currentTab < entries.length) {
entries[gFFI.peerTabModel.currentTab].load();
}
},
spinning: loading,
child: RotatedBox(
quarterTurns: 2,
child: Tooltip(
message: translate('Refresh'),
child: Icon(
Icons.refresh,
size: 18,
color: textColor,
)))),
child: Tooltip(
message: translate('Refresh'),
child: RefreshWidget(
onPressed: () {
if (gFFI.peerTabModel.currentTab < entries.length) {
entries[gFFI.peerTabModel.currentTab].load();
}
},
spinning: loading,
child: RotatedBox(
quarterTurns: 2,
child: Icon(
Icons.refresh,
size: 18,
color: textColor,
))),
),
);
}
@@ -227,6 +241,7 @@ class _PeerTabPageState extends State<PeerTabPage>
final textColor = Theme.of(context).textTheme.titleLarge?.color;
final model = Provider.of<PeerTabModel>(context);
return _hoverAction(
toolTip: translate('Select'),
context: context,
onTap: () {
model.setMultiSelectionMode(true);
@@ -234,30 +249,29 @@ class _PeerTabPageState extends State<PeerTabPage>
Navigator.pop(context);
}
},
child: Tooltip(
message: translate('Select'),
child: SvgPicture.asset(
"assets/checkbox-outline.svg",
width: 18,
height: 18,
colorFilter: svgColor(textColor),
)),
child: SvgPicture.asset(
"assets/checkbox-outline.svg",
width: 18,
height: 18,
colorFilter: svgColor(textColor),
),
);
}
void mobileShowTabVisibilityMenu() {
final model = gFFI.peerTabModel;
final items = List<PopupMenuItem>.empty(growable: true);
for (int i = 0; i < model.tabNames.length; i++) {
for (int i = 0; i < PeerTabModel.maxTabCount; i++) {
if (!model.isEnabled[i]) continue;
items.add(PopupMenuItem(
height: kMinInteractiveDimension * 0.8,
onTap: () => model.setTabVisible(i, !model.isVisible[i]),
onTap: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
child: Row(
children: [
Checkbox(
value: model.isVisible[i],
value: model.isVisibleEnabled[i],
onChanged: (_) {
model.setTabVisible(i, !model.isVisible[i]);
model.setTabVisible(i, !model.isVisibleEnabled[i]);
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
@@ -307,16 +321,17 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget visibleContextMenu(CancelFunc cancelFunc) {
final model = Provider.of<PeerTabModel>(context);
final menu = List<MenuEntrySwitch>.empty(growable: true);
for (int i = 0; i < model.tabNames.length; i++) {
menu.add(MenuEntrySwitch(
final menu = List<MenuEntrySwitchSync>.empty(growable: true);
for (int i = 0; i < model.orders.length; i++) {
int tabIndex = model.orders[i];
if (tabIndex < 0 || tabIndex >= PeerTabModel.maxTabCount) continue;
if (!model.isEnabled[tabIndex]) continue;
menu.add(MenuEntrySwitchSync(
switchType: SwitchType.scheckbox,
text: model.tabTooltip(i),
getter: () async {
return model.isVisible[i];
},
text: model.tabTooltip(tabIndex),
currentValue: model.isVisibleEnabled[tabIndex],
setter: (show) async {
model.setTabVisible(i, show);
model.setTabVisible(tabIndex, show);
cancelFunc();
}));
}
@@ -367,6 +382,7 @@ class _PeerTabPageState extends State<PeerTabPage>
}
return _hoverAction(
context: context,
toolTip: translate('Delete'),
onTap: () {
onSubmit() async {
final peers = model.selectedPeers;
@@ -392,21 +408,7 @@ class _PeerTabPageState extends State<PeerTabPage>
await bind.mainLoadLanPeers();
break;
case 3:
{
bool hasSynced = false;
if (shouldSyncAb()) {
for (var p in peers) {
if (await bind.mainPeerExists(id: p.id)) {
hasSynced = true;
}
}
}
gFFI.abModel.deletePeers(peers.map((p) => p.id).toList());
final future = gFFI.abModel.pushAb();
if (hasSynced) {
gFFI.abModel.reSyncToast(future);
}
}
await gFFI.abModel.deletePeers(peers.map((p) => p.id).toList());
break;
default:
break;
@@ -415,11 +417,9 @@ class _PeerTabPageState extends State<PeerTabPage>
if (model.currentTab != 3) showToast(translate('Successful'));
}
deletePeerConfirmDialog(onSubmit, translate('Delete'));
deleteConfirmDialog(onSubmit, translate('Delete'));
},
child: Tooltip(
message: translate('Delete'),
child: Icon(Icons.delete, color: Colors.red)));
child: Icon(Icons.delete, color: Colors.red));
}
Widget addSelectionToFav() {
@@ -429,6 +429,7 @@ class _PeerTabPageState extends State<PeerTabPage>
model.currentTab != PeerTabIndex.recent.index, // show based on recent
child: _hoverAction(
context: context,
toolTip: translate('Add to Favorites'),
onTap: () async {
final peers = model.selectedPeers;
final favs = (await bind.mainGetFav()).toList();
@@ -441,37 +442,28 @@ class _PeerTabPageState extends State<PeerTabPage>
model.setMultiSelectionMode(false);
showToast(translate('Successful'));
},
child: Tooltip(
message: translate('Add to Favorites'),
child: Icon(model.icons[PeerTabIndex.fav.index])),
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
).marginOnly(left: isMobile ? 11 : 6),
);
}
Widget addSelectionToAb() {
final model = Provider.of<PeerTabModel>(context);
final addressbooks = gFFI.abModel.addressBooksCanWrite();
if (model.currentTab == PeerTabIndex.ab.index) {
addressbooks.remove(gFFI.abModel.currentName.value);
}
return Offstage(
offstage:
!gFFI.userModel.isLogin || model.currentTab == PeerTabIndex.ab.index,
offstage: !gFFI.userModel.isLogin || addressbooks.isEmpty,
child: _hoverAction(
context: context,
toolTip: translate('Add to address book'),
onTap: () {
if (gFFI.abModel.isFull(true)) {
return;
}
final peers = model.selectedPeers;
gFFI.abModel.addPeers(peers);
final future = gFFI.abModel.pushAb();
final peers = model.selectedPeers.map((e) => Peer.copy(e)).toList();
addPeersToAbDialog(peers);
model.setMultiSelectionMode(false);
Future.delayed(Duration.zero, () async {
await future;
await Future.delayed(Duration(seconds: 2)); // toast
gFFI.abModel.isFull(true);
});
},
child: Tooltip(
message: translate('Add to address book'),
child: Icon(model.icons[PeerTabIndex.ab.index])),
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
).marginOnly(left: isMobile ? 11 : 6),
);
}
@@ -481,21 +473,20 @@ class _PeerTabPageState extends State<PeerTabPage>
return Offstage(
offstage: !gFFI.userModel.isLogin ||
model.currentTab != PeerTabIndex.ab.index ||
gFFI.abModel.tags.isEmpty,
gFFI.abModel.currentAbTags.isEmpty,
child: _hoverAction(
context: context,
toolTip: translate('Edit Tag'),
onTap: () {
editAbTagDialog(List.empty(), (selectedTags) async {
final peers = model.selectedPeers;
gFFI.abModel.changeTagForPeers(
await gFFI.abModel.changeTagForPeers(
peers.map((p) => p.id).toList(), selectedTags);
gFFI.abModel.pushAb();
model.setMultiSelectionMode(false);
showToast(translate('Successful'));
});
},
child: Tooltip(
message: translate('Edit Tag'), child: Icon(Icons.tag)))
child: Icon(Icons.tag))
.marginOnly(left: isMobile ? 11 : 6),
);
}
@@ -514,11 +505,11 @@ class _PeerTabPageState extends State<PeerTabPage>
model.selectedPeers.length >= model.currentTabCachedPeers.length,
child: _hoverAction(
context: context,
toolTip: translate('Select All'),
onTap: () {
model.selectAll();
},
child: Tooltip(
message: translate('Select All'), child: Icon(Icons.select_all)),
child: Icon(Icons.select_all),
).marginOnly(left: 6),
);
}
@@ -527,24 +518,23 @@ class _PeerTabPageState extends State<PeerTabPage>
final model = Provider.of<PeerTabModel>(context);
return _hoverAction(
context: context,
toolTip: translate('Close'),
onTap: () {
model.setMultiSelectionMode(false);
},
child:
Tooltip(message: translate('Close'), child: Icon(Icons.clear)))
child: Icon(Icons.clear))
.marginOnly(left: 6);
}
Widget _toggleTags() {
return _hoverAction(
context: context,
toolTip: translate('Toggle tags'),
hoverableWhenfalse: hideAbTagsPanel,
child: Tooltip(
message: translate('Toggle Tags'),
child: Icon(
Icons.tag_rounded,
size: 18,
)),
child: Icon(
Icons.tag_rounded,
size: 18,
),
onTap: () async {
await bind.mainSetLocalOption(
key: "hideAbTagsPanel", value: hideAbTagsPanel.value ? "" : "Y");
@@ -556,7 +546,8 @@ class _PeerTabPageState extends State<PeerTabPage>
final model = Provider.of<PeerTabModel>(context);
return [
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading),
_createRefresh(
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
_createRefresh(
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
Offstage(
@@ -580,7 +571,7 @@ class _PeerTabPageState extends State<PeerTabPage>
final screenWidth = MediaQuery.of(context).size.width;
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
final leftActionsSize =
(leftIconSize + (4 + 4) * 2) * model.visibleIndexs.length;
(leftIconSize + (4 + 4) * 2) * model.visibleEnabledOrderedIndexs.length;
final availableWidth = screenWidth - 10 * 2 - leftActionsSize - 2 * 2;
final searchWidth = 120;
final otherActionWidth = 18 + 10;
@@ -593,14 +584,13 @@ class _PeerTabPageState extends State<PeerTabPage>
(BuildContext context, Future<void> Function() showMenu) {
return _hoverAction(
context: context,
child: Tooltip(
message: translate('More'),
child: SvgPicture.asset(
"assets/chevron_up_chevron_down.svg",
width: 18,
height: 18,
colorFilter: svgColor(textColor),
)),
toolTip: translate('More'),
child: SvgPicture.asset(
"assets/chevron_up_chevron_down.svg",
width: 18,
height: 18,
colorFilter: svgColor(textColor),
),
onTap: showMenu,
);
},
@@ -624,7 +614,8 @@ class _PeerTabPageState extends State<PeerTabPage>
List<Widget> actions = [
const PeerSearchBar(),
if (model.currentTab == PeerTabIndex.ab.index)
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading),
_createRefresh(
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
if (model.currentTab == PeerTabIndex.group.index)
_createRefresh(
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
@@ -674,18 +665,17 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
? _buildSearchBar()
: _hoverAction(
context: context,
toolTip: translate('Search'),
padding: const EdgeInsets.only(right: 2),
onTap: () {
setState(() {
drawer = true;
});
},
child: Tooltip(
message: translate('Search'),
child: Icon(
Icons.search_rounded,
color: Theme.of(context).hintColor,
)));
child: Icon(
Icons.search_rounded,
color: Theme.of(context).hintColor,
));
}
Widget _buildSearchBar() {
@@ -826,16 +816,15 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
return _hoverAction(
context: context,
child: Tooltip(
message: translate('Change view'),
child: Icon(
peerCardUiType.value == PeerUiType.grid
? Icons.grid_view_rounded
: peerCardUiType.value == PeerUiType.tile
? Icons.view_list_rounded
: Icons.view_agenda_rounded,
size: 18,
)),
toolTip: translate('Change view'),
child: Icon(
peerCardUiType.value == PeerUiType.grid
? Icons.grid_view_rounded
: peerCardUiType.value == PeerUiType.tile
? Icons.view_list_rounded
: Icons.view_agenda_rounded,
size: 18,
),
onTapDown: (details) {
final x = details.globalPosition.dx;
final y = details.globalPosition.dy;
@@ -905,12 +894,11 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
return _hoverAction(
context: context,
child: Tooltip(
message: translate('Sort by'),
child: Icon(
Icons.sort_rounded,
size: 18,
)),
toolTip: translate('Sort by'),
child: Icon(
Icons.sort_rounded,
size: 18,
),
onTapDown: (details) {
final x = details.globalPosition.dx;
final y = details.globalPosition.dy;
@@ -992,6 +980,7 @@ Widget _hoverAction(
{required BuildContext context,
required Widget child,
required Function() onTap,
required String toolTip,
GestureTapDownCallback? onTapDown,
RxBool? hoverableWhenfalse,
EdgeInsetsGeometry padding = const EdgeInsets.all(4.0)}) {
@@ -1000,16 +989,19 @@ Widget _hoverAction(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6),
);
return Obx(
() => Container(
margin: EdgeInsets.symmetric(horizontal: 1),
decoration:
(hover.value || hoverableWhenfalse?.value == false) ? deco : null,
child: InkWell(
onHover: (value) => hover.value = value,
onTap: onTap,
onTapDown: onTapDown,
child: Container(padding: padding, child: child))),
return Tooltip(
message: toolTip,
child: Obx(
() => Container(
margin: EdgeInsets.symmetric(horizontal: 1),
decoration:
(hover.value || hoverableWhenfalse?.value == false) ? deco : null,
child: InkWell(
onHover: (value) => hover.value = value,
onTap: onTap,
onTapDown: onTapDown,
child: Container(padding: padding, child: child))),
),
);
}

View File

@@ -78,7 +78,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
LoadEvent.lan: 'empty_lan_tip',
LoadEvent.addressBook: 'empty_address_book_tip',
});
final space = isDesktop ? 12.0 : 8.0;
final space = (isDesktop || isWebDesktop) ? 12.0 : 8.0;
final _curPeers = <String>{};
var _lastChangeTime = DateTime.now();
var _lastQueryPeers = <String>{};
@@ -196,18 +196,25 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
// No need to listen the currentTab change event.
// Because the currentTab change event will trigger the peers change event,
// and the peers change event will trigger _buildPeersView().
final currentTab = Provider.of<PeerTabModel>(context, listen: false).currentTab;
final hideAbTagsPanel = bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
return isDesktop
final currentTab =
Provider.of<PeerTabModel>(context, listen: false).currentTab;
final hideAbTagsPanel =
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
return (isDesktop || isWebDesktop)
? Obx(
() => SizedBox(
width: peerCardUiType.value != PeerUiType.list
? 220
: currentTab == PeerTabIndex.group.index || (currentTab == PeerTabIndex.ab.index && !hideAbTagsPanel)
? windowWidth - 390 :
windowWidth - 227,
height:
peerCardUiType.value == PeerUiType.grid ? 140 : peerCardUiType.value != PeerUiType.list ? 42 : 45,
: currentTab == PeerTabIndex.group.index ||
(currentTab == PeerTabIndex.ab.index &&
!hideAbTagsPanel)
? windowWidth - 390
: windowWidth - 227,
height: peerCardUiType.value == PeerUiType.grid
? 140
: peerCardUiType.value != PeerUiType.list
? 42
: 45,
child: visibilityChild,
),
)
@@ -354,7 +361,7 @@ abstract class BasePeersView extends StatelessWidget {
final String loadEvent;
final PeerFilter? peerFilter;
final PeerCardBuilder peerCardBuilder;
final RxList<Peer>? initPeers;
final GetInitPeers? getInitPeers;
const BasePeersView({
Key? key,
@@ -362,13 +369,14 @@ abstract class BasePeersView extends StatelessWidget {
required this.loadEvent,
this.peerFilter,
required this.peerCardBuilder,
required this.initPeers,
required this.getInitPeers,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return _PeersView(
peers: Peers(name: name, loadEvent: loadEvent, initPeers: initPeers),
peers:
Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers),
peerFilter: peerFilter,
peerCardBuilder: peerCardBuilder);
}
@@ -385,7 +393,7 @@ class RecentPeersView extends BasePeersView {
peer: peer,
menuPadding: menuPadding,
),
initPeers: null,
getInitPeers: null,
);
@override
@@ -407,7 +415,7 @@ class FavoritePeersView extends BasePeersView {
peer: peer,
menuPadding: menuPadding,
),
initPeers: null,
getInitPeers: null,
);
@override
@@ -429,7 +437,7 @@ class DiscoveredPeersView extends BasePeersView {
peer: peer,
menuPadding: menuPadding,
),
initPeers: null,
getInitPeers: null,
);
@override
@@ -445,7 +453,7 @@ class AddressBookPeersView extends BasePeersView {
{Key? key,
EdgeInsets? menuPadding,
ScrollController? scrollController,
required RxList<Peer> initPeers})
required GetInitPeers getInitPeers})
: super(
key: key,
name: 'address book peer',
@@ -456,7 +464,7 @@ class AddressBookPeersView extends BasePeersView {
peer: peer,
menuPadding: menuPadding,
),
initPeers: initPeers,
getInitPeers: getInitPeers,
);
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
@@ -486,7 +494,7 @@ class MyGroupPeerView extends BasePeersView {
{Key? key,
EdgeInsets? menuPadding,
ScrollController? scrollController,
required RxList<Peer> initPeers})
required GetInitPeers getInitPeers})
: super(
key: key,
name: 'group peer',
@@ -496,7 +504,7 @@ class MyGroupPeerView extends BasePeersView {
peer: peer,
menuPadding: menuPadding,
),
initPeers: initPeers,
getInitPeers: getInitPeers,
);
static bool filter(Peer peer) {

View File

@@ -77,7 +77,7 @@ class _RawTouchGestureDetectorRegionState
FFI get ffi => widget.ffi;
FfiModel get ffiModel => widget.ffiModel;
InputModel get inputModel => widget.inputModel;
bool get handleTouch => isDesktop || ffiModel.touchMode;
bool get handleTouch => (isDesktop || isWebDesktop) || ffiModel.touchMode;
SessionID get sessionId => ffi.sessionId;
@override
@@ -183,7 +183,7 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (isDesktop || !ffiModel.touchMode) {
if ((isDesktop || isWebDesktop) || !ffiModel.touchMode) {
inputModel.tap(MouseButtons.right);
}
}
@@ -262,7 +262,7 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (isDesktop) {
if ((isDesktop || isWebDesktop)) {
final scale = ((d.scale - _scale) * 1000).toInt();
_scale = d.scale;
@@ -286,7 +286,7 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (isDesktop) {
if ((isDesktop || isWebDesktop)) {
bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode(
@@ -409,7 +409,9 @@ class RawPointerMouseRegion extends StatelessWidget {
onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate,
onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd,
child: MouseRegion(
cursor: cursor ?? MouseCursor.defer,
cursor: inputModel.isViewOnly
? MouseCursor.defer
: (cursor ?? MouseCursor.defer),
onEnter: onEnter,
onExit: onExit,
child: child,

View File

@@ -209,10 +209,10 @@ List<Widget> ServerConfigImportExportWidgets(
List<(String, String)> otherDefaultSettings() {
List<(String, String)> v = [
('View Mode', 'view_only'),
if (isDesktop) ('show_monitors_tip', kKeyShowMonitorsToolbar),
if (isDesktop) ('Collapse toolbar', 'collapse_toolbar'),
if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar),
if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'),
('Show remote cursor', 'show_remote_cursor'),
if (isDesktop) ('Zoom cursor', 'zoom-cursor'),
if ((isDesktop || isWebDesktop)) ('Zoom cursor', 'zoom-cursor'),
('Show quality monitor', 'show_quality_monitor'),
('Mute', 'disable_audio'),
if (isDesktop) ('Enable file copy and paste', 'enable_file_transfer'),
@@ -221,7 +221,7 @@ List<(String, String)> otherDefaultSettings() {
('Privacy mode', 'privacy_mode'),
if (isMobile) ('Touch mode', 'touch-mode'),
('True color (4:4:4)', 'i444'),
('Reverse mouse wheel', 'reverse_mouse_wheel'),
('Reverse mouse wheel', kKeyReverseMouseWheel),
('swap-left-right-mouse', 'swap-left-right-mouse'),
if (isDesktop && useTextureRender)
(

View File

@@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -95,7 +94,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
]),
trailingIcon: Transform.scale(
scale: isDesktop ? 0.8 : 1,
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
child: IconButton(
onPressed: () {
if (isMobile && Navigator.canPop(context)) {
@@ -161,7 +160,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
);
}
// divider
if (isDesktop) {
if (isDesktop || isWebDesktop) {
v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true));
}
// ctrlAltDel
@@ -230,7 +229,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
));
}
// record
if (!isDesktop &&
if (!(isDesktop || isWeb) &&
(ffi.recordingModel.start || (perms["recording"] != false))) {
v.add(TTextMenu(
child: Row(
@@ -251,7 +250,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
onPressed: () => ffi.recordingModel.toggle()));
}
// fingerprint
if (!isDesktop) {
if (!(isDesktop || isWebDesktop)) {
v.add(TTextMenu(
child: Text(translate('Copy Fingerprint')),
onPressed: () => onCopyFingerprint(FingerprintState.find(id).value),
@@ -512,8 +511,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Show displays as individual windows'))));
}
final screenList = await getScreenRectList();
if (useTextureRender && pi.isSupportMultiDisplay && screenList.length > 1) {
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
if (useTextureRender && pi.isSupportMultiDisplay && isMultiScreens) {
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
sessionId: ffi.sessionId) ==
'Y';
@@ -633,8 +632,8 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
// swap key
if (ffiModel.keyboard &&
((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) ||
(!Platform.isMacOS && pi.platform == kPeerPlatformMacOS))) {
((isMacOS && pi.platform != kPeerPlatformMacOS) ||
(!isMacOS && pi.platform == kPeerPlatformMacOS))) {
final option = 'allow_swap_key';
final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
@@ -655,7 +654,7 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
var optionValue =
bind.sessionGetReverseMouseWheelSync(sessionId: sessionId) ?? '';
if (optionValue == '') {
optionValue = bind.mainGetUserDefaultOption(key: 'reverse_mouse_wheel');
optionValue = bind.mainGetUserDefaultOption(key: kKeyReverseMouseWheel);
}
onChanged(bool? value) async {
if (value == null) return;

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -23,7 +21,8 @@ const String kPlatformAdditionsHeadless = "headless";
const String kPlatformAdditionsIsInstalled = "is_installed";
const String kPlatformAdditionsVirtualDisplays = "virtual_displays";
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
const String kPlatformAdditionsSupportedPrivacyModeImpl = "supported_privacy_mode_impl";
const String kPlatformAdditionsSupportedPrivacyModeImpl =
"supported_privacy_mode_impl";
const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";
@@ -68,8 +67,8 @@ const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
const String kOptionOpenInTabs = "allow-open-in-tabs";
const String kOptionOpenInWindows = "allow-open-in-windows";
const String kOptionForceAlwaysRelay = "force-always-relay";
const String kOptionViewOnly = "view-only";
const String kUniLinksPrefix = "rustdesk://";
const String kUrlActionClose = "close";
const String kTabLabelHomePage = "Home";
@@ -86,6 +85,7 @@ const String kKeyShowDisplaysAsIndividualWindows =
const String kKeyUseAllMyDisplaysForTheRemoteSession =
'use_all_my_displays_for_the_remote_session';
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
const String kKeyReverseMouseWheel = "reverse_mouse_wheel";
// the executable name of the portable version
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
@@ -113,16 +113,16 @@ const double kDefaultQuality = 50;
const double kMaxQuality = 100;
const double kMaxMoreQuality = 2000;
double kNewWindowOffset = Platform.isWindows
double kNewWindowOffset = isWindows
? 56.0
: Platform.isLinux
: isLinux
? 50.0
: Platform.isMacOS
: isMacOS
? 30.0
: 50.0;
EdgeInsets get kDragToResizeAreaPadding =>
!kUseCompatibleUiMode && Platform.isLinux
!kUseCompatibleUiMode && isLinux
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
? EdgeInsets.zero
: EdgeInsets.all(5.0)
@@ -150,7 +150,7 @@ const kDefaultScrollDuration = Duration(milliseconds: 50);
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
const kFullScreenEdgeSize = 0.0;
const kMaximizeEdgeSize = 0.0;
var kWindowEdgeSize = Platform.isWindows ? 1.0 : 5.0;
var kWindowEdgeSize = isWindows ? 1.0 : 5.0;
const kWindowBorderWidth = 1.0;
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);

View File

@@ -2,7 +2,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
@@ -20,6 +19,173 @@ import '../../common/widgets/autocomplete.dart';
import '../../models/platform_model.dart';
import '../widgets/button.dart';
class OnlineStatusWidget extends StatefulWidget {
const OnlineStatusWidget({Key? key, this.onSvcStatusChanged})
: super(key: key);
final VoidCallback? onSvcStatusChanged;
@override
State<OnlineStatusWidget> createState() => _OnlineStatusWidgetState();
}
/// State for the connection page.
class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
final _svcStopped = Get.find<RxBool>(tag: 'stop-service');
final _svcIsUsingPublicServer = true.obs;
Timer? _updateTimer;
double get em => 14.0;
double? get height => bind.isIncomingOnly() ? null : em * 3;
void onUsePublicServerGuide() {
const url = "https://rustdesk.com/pricing.html";
canLaunchUrlString(url).then((can) {
if (can) {
launchUrlString(url);
}
});
}
@override
void initState() {
super.initState();
_updateTimer = periodic_immediate(Duration(seconds: 1), () async {
updateStatus();
});
}
@override
void dispose() {
_updateTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isIncomingOnly = bind.isIncomingOnly();
startServiceWidget() => Offstage(
offstage: !_svcStopped.value,
child: InkWell(
onTap: () async {
await start_service(true);
},
child: Text(translate("Start service"),
style: TextStyle(
decoration: TextDecoration.underline, fontSize: em)))
.marginOnly(left: em),
);
setupServerWidget() => Flexible(
child: Offstage(
offstage: !(!_svcStopped.value &&
stateGlobal.svcStatus.value == SvcStatus.ready &&
_svcIsUsingPublicServer.value),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(', ', style: TextStyle(fontSize: em)),
Flexible(
child: InkWell(
onTap: onUsePublicServerGuide,
child: Row(
children: [
Flexible(
child: Text(
translate('setup_server_tip'),
style: TextStyle(
decoration: TextDecoration.underline,
fontSize: em),
),
),
],
),
),
)
],
),
),
);
basicWidget() => Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 8,
width: 8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: _svcStopped.value ||
stateGlobal.svcStatus.value == SvcStatus.connecting
? kColorWarn
: (stateGlobal.svcStatus.value == SvcStatus.ready
? Color.fromARGB(255, 50, 190, 166)
: Color.fromARGB(255, 224, 79, 95)),
),
).marginSymmetric(horizontal: em),
Container(
width: isIncomingOnly ? 226 : null,
child: _buildConnStatusMsg(),
),
// stop
if (!isIncomingOnly) startServiceWidget(),
// ready && public
// No need to show the guide if is custom client.
if (!isIncomingOnly) setupServerWidget(),
],
);
return Container(
height: height,
child: Obx(() => isIncomingOnly
? Column(
children: [
basicWidget(),
Align(
child: startServiceWidget(),
alignment: Alignment.centerLeft)
.marginOnly(top: 2.0, left: 22.0),
],
)
: basicWidget()),
).paddingOnly(right: isIncomingOnly ? 8 : 0);
}
_buildConnStatusMsg() {
widget.onSvcStatusChanged?.call();
return Text(
_svcStopped.value
? translate("Service is not running")
: stateGlobal.svcStatus.value == SvcStatus.connecting
? translate("connecting_status")
: stateGlobal.svcStatus.value == SvcStatus.notReady
? translate("not_ready_status")
: translate('Ready'),
style: TextStyle(fontSize: em),
);
}
updateStatus() async {
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;
}
_svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer();
}
}
/// Connection page for connecting to a remote peer.
class ConnectionPage extends StatefulWidget {
const ConnectionPage({Key? key}) : super(key: key);
@@ -34,13 +200,8 @@ class _ConnectionPageState extends State<ConnectionPage>
/// Controller for the id input bar.
final _idController = IDTextEditingController();
Timer? _updateTimer;
final RxBool _idInputFocused = false.obs;
var svcStopped = Get.find<RxBool>(tag: 'stop-service');
var svcIsUsingPublicServer = true.obs;
bool isWindowMinimized = false;
List<Peer> peers = [];
@@ -60,9 +221,6 @@ class _ConnectionPageState extends State<ConnectionPage>
}
}();
}
_updateTimer = periodic_immediate(Duration(seconds: 1), () async {
updateStatus();
});
Get.put<IDTextEditingController>(_idController);
windowManager.addListener(this);
}
@@ -70,7 +228,6 @@ class _ConnectionPageState extends State<ConnectionPage>
@override
void dispose() {
_idController.dispose();
_updateTimer?.cancel();
windowManager.removeListener(this);
if (Get.isRegistered<IDTextEditingController>()) {
Get.delete<IDTextEditingController>();
@@ -87,7 +244,7 @@ class _ConnectionPageState extends State<ConnectionPage>
if (eventName == 'minimize') {
isWindowMinimized = true;
} else if (eventName == 'maximize' || eventName == 'restore') {
if (isWindowMinimized && Platform.isWindows) {
if (isWindowMinimized && isWindows) {
// windows can't update when minimized.
Get.forceAppUpdate();
}
@@ -116,6 +273,7 @@ class _ConnectionPageState extends State<ConnectionPage>
@override
Widget build(BuildContext context) {
final isOutgoingOnly = bind.isOutgoingOnly();
return Column(
children: [
Expanded(
@@ -131,8 +289,8 @@ class _ConnectionPageState extends State<ConnectionPage>
Expanded(child: PeerTabPage()),
],
).paddingOnly(left: 12.0)),
const Divider(height: 1),
buildStatus()
if (!isOutgoingOnly) const Divider(height: 1),
if (!isOutgoingOnly) OnlineStatusWidget()
],
);
}
@@ -214,6 +372,7 @@ class _ConnectionPageState extends State<ConnectionPage>
platform: '',
tags: [],
hash: '',
password: '',
forceAlwaysRelay: false,
rdpPort: '',
rdpUsername: '',
@@ -289,6 +448,9 @@ class _ConnectionPageState extends State<ConnectionPage>
onChanged: (v) {
_idController.id = v;
},
onSubmitted: (_) {
onConnect();
},
));
},
onSelected: (option) {
@@ -383,111 +545,4 @@ class _ConnectionPageState extends State<ConnectionPage>
return Container(
constraints: const BoxConstraints(maxWidth: 600), child: w);
}
Widget buildStatus() {
final em = 14.0;
return Container(
height: 3 * em,
child: Obx(() => Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 8,
width: 8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: svcStopped.value ||
stateGlobal.svcStatus.value == SvcStatus.connecting
? kColorWarn
: (stateGlobal.svcStatus.value == SvcStatus.ready
? Color.fromARGB(255, 50, 190, 166)
: Color.fromARGB(255, 224, 79, 95)),
),
).marginSymmetric(horizontal: em),
Text(
svcStopped.value
? translate("Service is not running")
: stateGlobal.svcStatus.value == SvcStatus.connecting
? translate("connecting_status")
: stateGlobal.svcStatus.value == SvcStatus.notReady
? translate("not_ready_status")
: translate('Ready'),
style: TextStyle(fontSize: em)),
// stop
Offstage(
offstage: !svcStopped.value,
child: InkWell(
onTap: () async {
await start_service(true);
},
child: Text(translate("Start service"),
style: TextStyle(
decoration: TextDecoration.underline,
fontSize: em)))
.marginOnly(left: em),
),
// ready && public
Flexible(
child: Offstage(
offstage: !(!svcStopped.value &&
stateGlobal.svcStatus.value == SvcStatus.ready &&
svcIsUsingPublicServer.value),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(', ', style: TextStyle(fontSize: em)),
Flexible(
child: InkWell(
onTap: onUsePublicServerGuide,
child: Row(
children: [
Flexible(
child: Text(
translate('setup_server_tip'),
style: TextStyle(
decoration: TextDecoration.underline,
fontSize: em),
),
),
],
),
),
)
],
),
),
)
],
)),
);
}
void onUsePublicServerGuide() {
const url = "https://rustdesk.com/pricing.html";
canLaunchUrlString(url).then((can) {
if (can) {
launchUrlString(url);
}
});
}
updateStatus() async {
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;
}
svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer();
}
}

View File

@@ -20,6 +20,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:window_manager/window_manager.dart';
import 'package:window_size/window_size.dart' as window_size;
import '../widgets/button.dart';
@@ -50,50 +51,167 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Timer? _updateTimer;
bool isCardClosed = false;
final RxBool _editHover = false.obs;
final GlobalKey _childKey = GlobalKey();
@override
Widget build(BuildContext context) {
super.build(context);
final isIncomingOnly = bind.isIncomingOnly();
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildLeftPane(context),
const VerticalDivider(width: 1),
Expanded(
child: buildRightPane(context),
),
if (!isIncomingOnly) const VerticalDivider(width: 1),
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
],
);
}
Widget buildPresetPasswordWarning() {
return FutureBuilder<bool>(
future: bind.isPresetPassword(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
} else if (snapshot.hasError) {
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: 20,
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
} else {
return SizedBox
.shrink(); // Show nothing if the Future completed with false or null
}
},
);
}
Widget buildLeftPane(BuildContext context) {
final isIncomingOnly = bind.isIncomingOnly();
final isOutgoingOnly = bind.isOutgoingOnly();
final children = <Widget>[
if (!isOutgoingOnly) buildPresetPasswordWarning(),
if (bind.isCustomClient())
Align(
alignment: Alignment.center,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
launchUrl(Uri.parse('https://rustdesk.com'));
},
child: Opacity(
opacity: 0.5,
child: Text(
translate("powered_by_me"),
overflow: TextOverflow.clip,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 9, decoration: TextDecoration.underline),
)),
),
).marginOnly(top: 6),
),
Align(
alignment: Alignment.center,
child: loadLogo(),
),
buildTip(context),
if (!isOutgoingOnly) buildIDBoard(context),
if (!isOutgoingOnly) buildPasswordBoard(context),
FutureBuilder<Widget>(
future: buildHelpCards(),
builder: (_, data) {
if (data.hasData) {
if (isIncomingOnly) {
if (isInHomePage()) {
Future.delayed(Duration(milliseconds: 300), () {
_updateWindowSize();
});
}
}
return data.data!;
} else {
return const Offstage();
}
},
),
buildPluginEntry(),
];
if (isIncomingOnly) {
children.addAll([
Divider(),
OnlineStatusWidget(
onSvcStatusChanged: () {
if (isInHomePage()) {
Future.delayed(Duration(milliseconds: 300), () {
_updateWindowSize();
});
}
},
).marginOnly(bottom: 6, right: 6)
]);
}
final textColor = Theme.of(context).textTheme.titleLarge?.color;
return ChangeNotifierProvider.value(
value: gFFI.serverModel,
child: Container(
width: 200,
width: isIncomingOnly ? 280.0 : 200.0,
color: Theme.of(context).colorScheme.background,
child: DesktopScrollWrapper(
scrollController: _leftPaneScrollController,
child: SingleChildScrollView(
controller: _leftPaneScrollController,
physics: DraggableNeverScrollableScrollPhysics(),
child: Column(
children: [
buildTip(context),
buildIDBoard(context),
buildPasswordBoard(context),
FutureBuilder<Widget>(
future: buildHelpCards(),
builder: (_, data) {
if (data.hasData) {
return data.data!;
} else {
return const Offstage();
}
},
child: Stack(
children: [
SingleChildScrollView(
controller: _leftPaneScrollController,
physics: DraggableNeverScrollableScrollPhysics(),
child: Column(
key: _childKey,
children: children,
),
buildPluginEntry()
],
),
),
if (isOutgoingOnly)
Positioned(
bottom: 6,
left: 12,
child: Align(
alignment: Alignment.centerLeft,
child: InkWell(
child: Obx(
() => Icon(
Icons.settings,
color: _editHover.value
? textColor
: Colors.grey.withOpacity(0.5),
size: 22,
),
),
onTap: () => DesktopSettingPage.switch2page(0),
onHover: (value) => _editHover.value = value,
),
),
)
],
),
),
),
@@ -266,21 +384,22 @@ class _DesktopHomePageState extends State<DesktopHomePage>
)))),
onHover: (value) => refreshHover.value = value,
).marginOnly(right: 8, top: 4),
InkWell(
child: Obx(
() => Tooltip(
message: translate('Change Password'),
child: Icon(
Icons.edit,
color: editHover.value
? textColor
: Color(0xFFDDDDDD),
size: 22,
)).marginOnly(right: 8, top: 4),
if (!bind.isDisableSettings())
InkWell(
child: Obx(
() => Tooltip(
message: translate('Change Password'),
child: Icon(
Icons.edit,
color: editHover.value
? textColor
: Color(0xFFDDDDDD),
size: 22,
)).marginOnly(right: 8, top: 4),
),
onTap: () => DesktopSettingPage.switch2page(0),
onHover: (value) => editHover.value = value,
),
onTap: () => DesktopSettingPage.switch2page(1),
onHover: (value) => editHover.value = value,
),
],
),
],
@@ -293,6 +412,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}
buildTip(BuildContext context) {
final isOutgoingOnly = bind.isOutgoingOnly();
return Padding(
padding:
const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5),
@@ -300,29 +420,43 @@ class _DesktopHomePageState extends State<DesktopHomePage>
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
translate("Your Desktop"),
style: Theme.of(context).textTheme.titleLarge,
// style: TextStyle(
// // color: MyTheme.color(context).text,
// fontWeight: FontWeight.normal,
// fontSize: 19),
Column(
children: [
if (!isOutgoingOnly)
Align(
alignment: Alignment.centerLeft,
child: Text(
translate("Your Desktop"),
style: Theme.of(context).textTheme.titleLarge,
),
),
],
),
SizedBox(
height: 10.0,
),
Text(
translate("desk_tip"),
overflow: TextOverflow.clip,
style: Theme.of(context).textTheme.bodySmall,
)
if (!isOutgoingOnly)
Text(
translate("desk_tip"),
overflow: TextOverflow.clip,
style: Theme.of(context).textTheme.bodySmall,
),
if (isOutgoingOnly)
Text(
translate("outgoing_only_desk_tip"),
overflow: TextOverflow.clip,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}
Future<Widget> buildHelpCards() async {
if (updateUrl.isNotEmpty && !isCardClosed) {
if (!bind.isCustomClient() &&
updateUrl.isNotEmpty &&
!isCardClosed &&
bind.mainUriPrefixSync().contains('rustdesk')) {
return buildInstallCard(
"Status",
"There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
@@ -334,9 +468,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
if (systemError.isNotEmpty) {
return buildInstallCard("", systemError, "", () {});
}
if (Platform.isWindows) {
if (isWindows && !bind.isDisableInstallation()) {
if (!bind.mainIsInstalled()) {
return buildInstallCard("", "install_tip", "Install", () async {
return buildInstallCard(
"", bind.isOutgoingOnly() ? "" : "install_tip", "Install",
() async {
await rustDeskWinManager.closeAllSubWindows();
bind.mainGotoInstall();
});
@@ -348,8 +485,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
bind.mainUpdateMe();
});
}
} else if (Platform.isMacOS) {
if (!bind.mainIsCanScreenRecording(prompt: false)) {
} else if (isMacOS) {
if (!(bind.isOutgoingOnly() ||
bind.mainIsCanScreenRecording(prompt: false))) {
return buildInstallCard("Permissions", "config_screen", "Configure",
() async {
bind.mainIsCanScreenRecording(prompt: true);
@@ -383,7 +521,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
// watchIsCanRecordAudio = true;
// });
// }
} else if (Platform.isLinux) {
} else if (isLinux) {
if (bind.isOutgoingOnly()) {
return Container();
}
final LinuxCards = <Widget>[];
if (bind.isSelinuxEnforcing()) {
// Check is SELinux enforcing, but show user a tip of is SELinux enabled for simple.
@@ -422,6 +563,21 @@ class _DesktopHomePageState extends State<DesktopHomePage>
);
}
}
if (bind.isIncomingOnly()) {
return Align(
alignment: Alignment.centerRight,
child: OutlinedButton(
onPressed: () {
SystemNavigator.pop(); // Close the application
// https://github.com/flutter/flutter/issues/66631
if (isWindows) {
exit(0);
}
},
child: Text(translate('Quit')),
),
).marginAll(14);
}
return Container();
}
@@ -450,7 +606,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
return Stack(
children: [
Container(
margin: EdgeInsets.only(top: marginTop),
margin: EdgeInsets.fromLTRB(
0, marginTop, 0, bind.isIncomingOnly() ? marginTop : 0),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
@@ -478,14 +635,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
]
: <Widget>[]) +
<Widget>[
Text(
translate(content),
style: TextStyle(
height: 1.5,
color: Colors.white,
fontWeight: FontWeight.normal,
fontSize: 13),
).marginOnly(bottom: 20)
if (content.isNotEmpty)
Text(
translate(content),
style: TextStyle(
height: 1.5,
color: Colors.white,
fontWeight: FontWeight.normal,
fontSize: 13),
).marginOnly(bottom: 20)
] +
(btnText.isNotEmpty
? <Widget>[
@@ -582,7 +740,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}
}
if (watchIsCanRecordAudio) {
if (Platform.isMacOS) {
if (isMacOS) {
Future.microtask(() async {
if ((await osxCanRecordAudio() ==
PermissionAuthorizeType.authorized)) {
@@ -642,6 +800,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
isFileTransfer: call.arguments['isFileTransfer'],
isTcpTunneling: call.arguments['isTcpTunneling'],
isRDP: call.arguments['isRDP'],
password: call.arguments['password'],
forceRelay: call.arguments['forceRelay'],
);
} else if (call.method == kWindowEventMoveTabToNewWindow) {
@@ -668,6 +827,26 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}
});
_uniLinksSubscription = listenUniLinks();
if (bind.isIncomingOnly()) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_updateWindowSize();
});
}
}
_updateWindowSize() {
RenderObject? renderObject = _childKey.currentContext?.findRenderObject();
if (renderObject == null) {
return;
}
if (renderObject is RenderBox) {
final size = renderObject.size;
if (size != imcomingOnlyHomeSize) {
imcomingOnlyHomeSize = size;
windowManager.setSize(getIncomingOnlyHomeSize());
}
}
}
@override

View File

@@ -106,33 +106,34 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
List<_TabInfo> _settingTabs() {
final List<_TabInfo> settingTabs = <_TabInfo>[
_TabInfo('General', Icons.settings_outlined, Icons.settings),
_TabInfo('Security', Icons.enhanced_encryption_outlined,
Icons.enhanced_encryption),
_TabInfo('Network', Icons.link_outlined, Icons.link),
_TabInfo(
'Display', Icons.desktop_windows_outlined, Icons.desktop_windows),
_TabInfo('Account', Icons.person_outline, Icons.person),
if (!bind.isOutgoingOnly() && !bind.isDisableSettings())
_TabInfo('Security', Icons.enhanced_encryption_outlined,
Icons.enhanced_encryption),
if (!bind.isDisableSettings())
_TabInfo('Network', Icons.link_outlined, Icons.link),
if (!bind.isIncomingOnly())
_TabInfo(
'Display', Icons.desktop_windows_outlined, Icons.desktop_windows),
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
_TabInfo('Plugin', Icons.extension_outlined, Icons.extension),
if (!bind.isDisableAccount())
_TabInfo('Account', Icons.person_outline, Icons.person),
_TabInfo('About', Icons.info_outline, Icons.info)
];
if (bind.pluginFeatureIsEnabled()) {
settingTabs.insert(
4, _TabInfo('Plugin', Icons.extension_outlined, Icons.extension));
}
return settingTabs;
}
List<Widget> _children() {
final children = [
_General(),
_Safety(),
_Network(),
_Display(),
_Account(),
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(),
];
if (bind.pluginFeatureIsEnabled()) {
children.insert(4, _Plugin());
}
return children;
}
@@ -303,6 +304,10 @@ class _GeneralState extends State<_General> {
}
Widget service() {
if (bind.isOutgoingOnly()) {
return const Offstage();
}
return _Card(title: 'Service', children: [
Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
() async {
@@ -318,31 +323,34 @@ class _GeneralState extends State<_General> {
}
Widget other() {
final children = [
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
'enable-confirm-closing-tabs',
isServer: false),
final children = <Widget>[
if (!bind.isIncomingOnly())
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
'enable-confirm-closing-tabs',
isServer: false),
_OptionCheckBox(context, 'Adaptive bitrate', 'enable-abr'),
wallpaper(),
_OptionCheckBox(
context,
'Open connection in new tab',
kOptionOpenNewConnInTabs,
isServer: false,
),
if (!bind.isIncomingOnly()) ...[
_OptionCheckBox(
context,
'Open connection in new tab',
kOptionOpenNewConnInTabs,
isServer: false,
),
// though this is related to GUI, but opengl problem affects all users, so put in config rather than local
Tooltip(
message: translate('software_render_tip'),
child: _OptionCheckBox(context, "Always use software rendering",
'allow-always-software-render'),
),
_OptionCheckBox(
context,
'Check for software update on startup',
'enable-check-update',
isServer: false,
)
],
];
// though this is related to GUI, but opengl problem affects all users, so put in config rather than local
children.add(Tooltip(
message: translate('software_render_tip'),
child: _OptionCheckBox(context, "Always use software rendering",
'allow-always-software-render'),
));
children.add(_OptionCheckBox(
context,
'Check for software update on startup',
'enable-check-update',
isServer: false,
));
if (bind.mainShowOption(key: 'allow-linux-headless')) {
children.add(_OptionCheckBox(
context, 'Allow linux headless', 'allow-linux-headless'));
@@ -351,6 +359,10 @@ class _GeneralState extends State<_General> {
}
Widget wallpaper() {
if (bind.isOutgoingOnly()) {
return const Offstage();
}
return futureBuilder(future: () async {
final support = await bind.mainSupportRemoveWallpaper();
return support;
@@ -398,8 +410,12 @@ class _GeneralState extends State<_General> {
}
Widget audio(BuildContext context) {
if (bind.isOutgoingOnly()) {
return const Offstage();
}
String getDefault() {
if (Platform.isWindows) return translate('System Sound');
if (isWindows) return translate('System Sound');
return '';
}
@@ -419,7 +435,7 @@ class _GeneralState extends State<_General> {
return futureBuilder(future: () async {
List<String> devices = (await bind.mainGetSoundInputs()).toList();
if (Platform.isWindows) {
if (isWindows) {
devices.insert(0, translate('System Sound'));
}
String current = await getValue();
@@ -690,7 +706,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
_OptionCheckBox(
context, 'Enable recording session', 'enable-record-session',
enabled: enabled, fakeValue: fakeValue),
if (Platform.isWindows)
if (isWindows)
_OptionCheckBox(
context, 'Enable blocking user input', 'enable-block-input',
enabled: enabled, fakeValue: fakeValue),
@@ -830,7 +846,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
bool value = bind.mainIsShareRdp();
return Offstage(
offstage: !(Platform.isWindows && bind.mainIsRdpServiceOpen()),
offstage: !(isWindows && bind.mainIsInstalled()),
child: GestureDetector(
child: Row(
children: [
@@ -1622,7 +1638,7 @@ class _AboutState extends State<_About> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Copyright © 2023 Purslane Ltd.\n$license',
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
style: const TextStyle(color: Colors.white),
),
Text(
@@ -2029,7 +2045,7 @@ void changeSocks5Proxy() async {
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
'${translate("Hostname")}:',
'${translate("Server")}:',
textAlign: TextAlign.right,
).marginOnly(right: 10)),
Expanded(

View File

@@ -1,11 +1,10 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
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';
@@ -53,6 +52,17 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
page: DesktopHomePage(
key: const ValueKey(kTabLabelHomePage),
)));
if (bind.isIncomingOnly()) {
tabController.onSelected = (key) {
if (key == kTabLabelHomePage) {
windowManager.setSize(getIncomingOnlyHomeSize());
windowManager.setResizable(false);
} else {
windowManager.setSize(getIncomingOnlySettingsSize());
windowManager.setResizable(true);
}
};
}
}
@override
@@ -68,14 +78,17 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(
controller: tabController,
tail: ActionIcon(
message: 'Settings',
icon: IconFont.menu,
onTap: DesktopTabPage.onAddSetting,
isClose: false,
tail: Offstage(
offstage: bind.isIncomingOnly() || bind.isDisableSettings(),
child: ActionIcon(
message: 'Settings',
icon: IconFont.menu,
onTap: DesktopTabPage.onAddSetting,
isClose: false,
),
),
)));
return Platform.isMacOS || kUseCompatibleUiMode
return isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(
() => DragToResizeArea(

View File

@@ -53,11 +53,13 @@ class FileManagerPage extends StatefulWidget {
{Key? key,
required this.id,
required this.password,
required this.isSharedPassword,
required this.tabController,
this.forceRelay})
: super(key: key);
final String id;
final String? password;
final bool? isSharedPassword;
final bool? forceRelay;
final DesktopTabController tabController;
@@ -84,13 +86,14 @@ class _FileManagerPageState extends State<FileManagerPage>
_ffi.start(widget.id,
isFileTransfer: true,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay);
WidgetsBinding.instance.addPostFrameCallback((_) {
_ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
});
Get.put(_ffi, tag: 'ft_${widget.id}');
if (!Platform.isLinux) {
if (!isLinux) {
WakelockPlus.enable();
}
debugPrint("File manager page init success with id ${widget.id}");
@@ -103,7 +106,7 @@ class _FileManagerPageState extends State<FileManagerPage>
model.close().whenComplete(() {
_ffi.close();
_ffi.dialogManager.dismissAll();
if (!Platform.isLinux) {
if (!isLinux) {
WakelockPlus.disable();
}
Get.delete<FFI>(tag: 'ft_${widget.id}');
@@ -1295,7 +1298,7 @@ class _FileManagerViewState extends State<FileManagerView> {
onPointerSignal: (e) {
if (e is PointerScrollEvent) {
final sc = _breadCrumbScroller;
final scale = Platform.isWindows ? 2 : 4;
final scale = isWindows ? 2 : 4;
sc.jumpTo(sc.offset + e.scrollDelta.dy / scale);
}
},

View File

@@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
@@ -45,6 +44,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
key: ValueKey(params['id']),
id: params['id'],
password: params['password'],
isSharedPassword: params['isSharedPassword'],
tabController: tabController,
forceRelay: params['forceRelay'],
)));
@@ -74,6 +74,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
key: ValueKey(id),
id: id,
password: args['password'],
isSharedPassword: args['isSharedPassword'],
tabController: tabController,
forceRelay: args['forceRelay'],
)));
@@ -102,7 +103,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
labelGetter: DesktopTab.tablabelGetter,
)),
);
return Platform.isMacOS || kUseCompatibleUiMode
return isMacOS || kUseCompatibleUiMode
? tabWidget
: SubWindowDragToResizeArea(
child: tabWidget,

View File

@@ -25,19 +25,21 @@ class _PortForward {
}
class PortForwardPage extends StatefulWidget {
const PortForwardPage(
{Key? key,
required this.id,
required this.password,
required this.tabController,
required this.isRDP,
this.forceRelay})
: super(key: key);
const PortForwardPage({
Key? key,
required this.id,
required this.password,
required this.tabController,
required this.isRDP,
required this.isSharedPassword,
this.forceRelay,
}) : super(key: key);
final String id;
final String? password;
final DesktopTabController tabController;
final bool isRDP;
final bool? forceRelay;
final bool? isSharedPassword;
@override
State<PortForwardPage> createState() => _PortForwardPageState();
@@ -58,6 +60,7 @@ class _PortForwardPageState extends State<PortForwardPage>
_ffi.start(widget.id,
isPortForward: true,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay,
isRdp: widget.isRDP);
Get.put(_ffi, tag: 'pf_${widget.id}');

View File

@@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
@@ -44,6 +43,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
key: ValueKey(params['id']),
id: params['id'],
password: params['password'],
isSharedPassword: params['isSharedPassword'],
tabController: tabController,
isRDP: isRDP,
forceRelay: params['forceRelay'],
@@ -79,6 +79,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
key: ValueKey(args['id']),
id: id,
password: args['password'],
isSharedPassword: args['isSharedPassword'],
isRDP: isRDP,
tabController: tabController,
forceRelay: args['forceRelay'],
@@ -111,7 +112,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
labelGetter: DesktopTab.tablabelGetter,
)),
);
return Platform.isMacOS || kUseCompatibleUiMode
return isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(
() => SubWindowDragToResizeArea(

View File

@@ -1,15 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_custom_cursor/cursor_manager.dart'
as custom_cursor_manager;
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_custom_cursor/flutter_custom_cursor.dart';
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
import '../../consts.dart';
@@ -27,6 +23,9 @@ import '../widgets/remote_toolbar.dart';
import '../widgets/kb_layout_type_chooser.dart';
import '../widgets/tabbar_widget.dart';
import 'package:flutter_hbb/native/custom_cursor.dart'
if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart';
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
// Used to skip session close if "move to new window" is clicked.
@@ -36,15 +35,16 @@ class RemotePage extends StatefulWidget {
RemotePage({
Key? key,
required this.id,
required this.sessionId,
required this.tabWindowId,
required this.display,
required this.displays,
required this.password,
required this.toolbarState,
required this.tabController,
this.sessionId,
this.tabWindowId,
this.password,
this.display,
this.displays,
this.tabController,
this.switchUuid,
this.forceRelay,
this.isSharedPassword,
}) : super(key: key);
final String id;
@@ -56,8 +56,9 @@ class RemotePage extends StatefulWidget {
final ToolbarState toolbarState;
final String? switchUuid;
final bool? forceRelay;
final bool? isSharedPassword;
final SimpleWrapper<State<RemotePage>?> _lastState = SimpleWrapper(null);
final DesktopTabController tabController;
final DesktopTabController? tabController;
FFI get ffi => (_lastState.value! as _RemotePageState)._ffi;
@@ -111,6 +112,7 @@ class _RemotePageState extends State<RemotePage>
_ffi.start(
widget.id,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
switchUuid: widget.switchUuid,
forceRelay: widget.forceRelay,
tabWindowId: widget.tabWindowId,
@@ -122,12 +124,12 @@ class _RemotePageState extends State<RemotePage>
_ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
});
if (!Platform.isLinux) {
if (!isLinux) {
WakelockPlus.enable();
}
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
// Session option should be set after models.dart/FFI.start
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
@@ -148,7 +150,7 @@ class _RemotePageState extends State<RemotePage>
// }
_blockableOverlayState.applyFfi(_ffi);
widget.tabController.onSelected?.call(widget.id);
widget.tabController?.onSelected?.call(widget.id);
}
@override
@@ -157,7 +159,7 @@ class _RemotePageState extends State<RemotePage>
// On windows, we use `focus` way to handle keyboard better.
// Now on Linux, there's some rdev issues which will break the input.
// We disable the `focus` way for non-Windows temporarily.
if (Platform.isWindows) {
if (isWindows) {
_isWindowBlur = true;
// unfocus the primary-focus when the whole window is lost focus,
// and let OS to handle events instead.
@@ -169,7 +171,7 @@ class _RemotePageState extends State<RemotePage>
void onWindowFocus() {
super.onWindowFocus();
// See [onWindowBlur].
if (Platform.isWindows) {
if (isWindows) {
_isWindowBlur = false;
}
}
@@ -179,10 +181,10 @@ class _RemotePageState extends State<RemotePage>
super.onWindowRestore();
// On windows, we use `onWindowRestore` way to handle window restore from
// a minimized state.
if (Platform.isWindows) {
if (isWindows) {
_isWindowBlur = false;
}
if (!Platform.isLinux) {
if (!isLinux) {
WakelockPlus.enable();
}
}
@@ -191,7 +193,7 @@ class _RemotePageState extends State<RemotePage>
@override
void onWindowMaximize() {
super.onWindowMaximize();
if (!Platform.isLinux) {
if (!isLinux) {
WakelockPlus.enable();
}
}
@@ -199,7 +201,7 @@ class _RemotePageState extends State<RemotePage>
@override
void onWindowMinimize() {
super.onWindowMinimize();
if (!Platform.isLinux) {
if (!isLinux) {
WakelockPlus.disable();
}
}
@@ -225,7 +227,7 @@ class _RemotePageState extends State<RemotePage>
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
}
if (!Platform.isLinux) {
if (!isLinux) {
await WakelockPlus.disable();
}
await Get.delete<FFI>(tag: widget.id);
@@ -263,7 +265,7 @@ class _RemotePageState extends State<RemotePage>
debugPrint(
"onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
// See [onWindowBlur].
if (Platform.isWindows) {
if (isWindows) {
if (_isWindowBlur) {
imageFocused = false;
Future.delayed(Duration.zero, () {
@@ -359,7 +361,7 @@ class _RemotePageState extends State<RemotePage>
}
}
// See [onWindowBlur].
if (!Platform.isWindows) {
if (!isWindows) {
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
@@ -382,7 +384,7 @@ class _RemotePageState extends State<RemotePage>
}
}
// See [onWindowBlur].
if (!Platform.isWindows) {
if (!isWindows) {
_ffi.inputModel.enterOrLeave(false);
}
}
@@ -429,9 +431,9 @@ class _RemotePageState extends State<RemotePage>
Widget getBodyForDesktop(BuildContext context) {
var paints = <Widget>[
MouseRegion(onEnter: (evt) {
bind.hostStopSystemKeyPropagate(stopped: false);
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
}, onExit: (evt) {
bind.hostStopSystemKeyPropagate(stopped: true);
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
}, child: LayoutBuilder(builder: (context, constraints) {
Future.delayed(Duration.zero, () {
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
@@ -534,7 +536,7 @@ class _ImagePaintState extends State<ImagePaint> {
double getCursorScale() {
var c = Provider.of<CanvasModel>(context);
var cursorScale = 1.0;
if (Platform.isWindows) {
if (isWindows) {
// debug win10
if (zoomCursor.value && isViewAdaptive()) {
cursorScale = s * c.devicePixelRatio;
@@ -601,8 +603,8 @@ class _ImagePaintState extends State<ImagePaint> {
c,
s,
Offset(
Platform.isLinux ? c.x.toInt().toDouble() : c.x,
Platform.isLinux ? c.y.toInt().toDouble() : c.y,
isLinux ? c.x.toInt().toDouble() : c.x,
isLinux ? c.y.toInt().toDouble() : c.y,
),
c.size,
isViewOriginal())
@@ -665,43 +667,16 @@ class _ImagePaintState extends State<ImagePaint> {
);
}
MouseCursor _buildCursorOfCache(
CursorModel cursor, double scale, CursorData? cache) {
if (cache == null) {
return MouseCursor.defer;
} else {
final key = cache.updateGetKey(scale);
if (!cursor.cachedKeys.contains(key)) {
debugPrint(
"Register custom cursor with key $key (${cache.hotx},${cache.hoty})");
// [Safety]
// It's ok to call async registerCursor in current synchronous context,
// because activating the cursor is also an async call and will always
// be executed after this.
custom_cursor_manager.CursorManager.instance
.registerCursor(custom_cursor_manager.CursorData()
..buffer = cache.data!
..height = (cache.height * cache.scale).toInt()
..width = (cache.width * cache.scale).toInt()
..hotX = cache.hotx
..hotY = cache.hoty
..name = key);
cursor.addKey(key);
}
return FlutterCustomMemoryImageCursor(key: key);
}
}
MouseCursor _buildCustomCursor(BuildContext context, double scale) {
final cursor = Provider.of<CursorModel>(context);
final cache = cursor.cache ?? preDefaultCursor.cache;
return _buildCursorOfCache(cursor, scale, cache);
return buildCursorOfCache(cursor, scale, cache);
}
MouseCursor _buildDisabledCursor(BuildContext context, double scale) {
final cursor = Provider.of<CursorModel>(context);
final cache = preForbiddenCursor.cache;
return _buildCursorOfCache(cursor, scale, cache);
return buildCursorOfCache(cursor, scale, cache);
}
Widget _buildCrossScrollbarFromLayout(

View File

@@ -1,6 +1,5 @@
import 'dart:convert';
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
@@ -95,6 +94,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
tabController: tabController,
switchUuid: params['switch_uuid'],
forceRelay: params['forceRelay'],
isSharedPassword: params['isSharedPassword'],
),
));
_update_remote_count();
@@ -126,7 +126,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
tryMoveToScreenAndSetFullscreen(screenRect);
if (tabController.length == 0) {
// Show the hidden window.
if (Platform.isMacOS && stateGlobal.closeOnFullscreen == true) {
if (isMacOS && stateGlobal.closeOnFullscreen == true) {
stateGlobal.setFullscreen(true);
}
// Reset the state
@@ -153,6 +153,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
tabController: tabController,
switchUuid: switchUuid,
forceRelay: args['forceRelay'],
isSharedPassword: args['isSharedPassword'],
),
));
} else if (call.method == kWindowDisableGrabKeyboard) {
@@ -326,7 +327,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
),
),
);
return Platform.isMacOS || kUseCompatibleUiMode
return isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(() => SubWindowDragToResizeArea(
key: contentKey,

View File

@@ -1,7 +1,6 @@
// original cm window in Sciter version.
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
@@ -52,7 +51,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
@override
void onWindowClose() {
Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) {
if (Platform.isMacOS) {
if (isMacOS) {
RdPlatformChannel.instance.terminate();
} else {
windowManager.setPreventClose(false);
@@ -327,11 +326,7 @@ class _AppIcon extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 4.0),
child: SvgPicture.asset(
'assets/logo.svg',
width: 30,
height: 30,
),
child: loadIcon(30),
);
}
}
@@ -655,7 +650,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
translate('Enable recording session'),
),
// only windows support block input
if (Platform.isWindows)
if (isWindows)
buildPermissionIcon(
client.blockInput,
Icons.block,

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart';
@@ -28,7 +26,7 @@ class DesktopRemoteScreen extends StatelessWidget {
child: Scaffold(
// Set transparent background for padding the resize area out of the flutter view.
// This allows the wallpaper goes through our resize area. (Linux only now).
backgroundColor: Platform.isLinux ? Colors.transparent : null,
backgroundColor: isLinux ? Colors.transparent : null,
body: ConnectionTabPage(
params: params,
),

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_hbb/consts.dart';
@@ -175,9 +174,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) {
return localPlatform;
}
if (Platform.isWindows) {
if (isWindows) {
localPlatform = kPeerPlatformWindows;
} else if (Platform.isLinux) {
} else if (isLinux) {
localPlatform = kPeerPlatformLinux;
}
// to-do: web desktop support ?

View File

@@ -568,6 +568,47 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
}
}
// Compatible with MenuEntrySwitch, it uses value instead of getter
class MenuEntrySwitchSync<T> extends MenuEntrySwitchBase<T> {
final SwitchSetter setter;
final RxBool _curOption = false.obs;
MenuEntrySwitchSync({
required SwitchType switchType,
required String text,
required bool currentValue,
required this.setter,
Rx<TextStyle>? textStyle,
EdgeInsets? padding,
dismissOnClicked = false,
RxBool? enabled,
dismissCallback,
}) : super(
switchType: switchType,
text: text,
textStyle: textStyle,
padding: padding,
dismissOnClicked: dismissOnClicked,
enabled: enabled,
dismissCallback: dismissCallback,
) {
_curOption.value = currentValue;
}
@override
RxBool get curOption => _curOption;
@override
setOption(bool? option) async {
if (option != null) {
await setter(option);
// Notice: no ensure with getter, best used on menus that are destroyed on click
if (_curOption.value != option) {
_curOption.value = option;
}
}
}
}
typedef Switch2Getter = RxBool Function();
typedef Switch2Setter = Future<void> Function(bool);

View File

@@ -1,6 +1,5 @@
import 'dart:convert';
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -112,10 +111,10 @@ class _ToolbarTheme {
static const double iconRadius = 8;
static const double elevation = 3;
static double dividerSpaceToAction = Platform.isWindows ? 8 : 14;
static double dividerSpaceToAction = isWindows ? 8 : 14;
static double menuBorderRadius = Platform.isWindows ? 5.0 : 7.0;
static EdgeInsets menuPadding = Platform.isWindows
static double menuBorderRadius = isWindows ? 5.0 : 7.0;
static EdgeInsets menuPadding = isWindows
? EdgeInsets.fromLTRB(4, 12, 4, 12)
: EdgeInsets.fromLTRB(6, 14, 6, 14);
static const double menuButtonBorderRadius = 3.0;
@@ -492,7 +491,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
}
toolbarItems.add(_RecordMenu());
if (!isWeb) toolbarItems.add(_RecordMenu());
toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi));
final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0));
return Column(
@@ -637,7 +636,7 @@ class _MonitorMenu extends StatelessWidget {
menuStyle: MenuStyle(
padding:
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
menuChildren: [buildMonitorSubmenuWidget()]);
menuChildrenGetter: () => [buildMonitorSubmenuWidget()]);
}
Widget buildMultiMonitorMenu() {
@@ -759,15 +758,18 @@ class _MonitorMenu extends StatelessWidget {
final children = <Widget>[];
for (var i = 0; i < pi.displays.length; i++) {
final d = pi.displays[i];
final fontSize = (d.width * scale < d.height * scale
? d.width * scale
: d.height * scale) *
double s = d.scale;
int dWidth = d.width.toDouble() ~/ s;
int dHeight = d.height.toDouble() ~/ s;
final fontSize = (dWidth * scale < dHeight * scale
? dWidth * scale
: dHeight * scale) *
0.65;
children.add(Positioned(
left: (d.x - rect.left) * scale + startX,
top: (d.y - rect.top) * scale + startY,
width: d.width * scale,
height: d.height * scale,
width: dWidth * scale,
height: dHeight * scale,
child: Container(
decoration: BoxDecoration(
border: Border.all(
@@ -841,17 +843,17 @@ class _ControlMenu extends StatelessWidget {
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
ffi: ffi,
menuChildren: toolbarControls(context, id, ffi).map((e) {
if (e.divider) {
return Divider();
} else {
return MenuButton(
child: e.child,
onPressed: e.onPressed,
ffi: ffi,
trailingIcon: e.trailingIcon);
}
}).toList());
menuChildrenGetter: () => toolbarControls(context, id, ffi).map((e) {
if (e.divider) {
return Divider();
} else {
return MenuButton(
child: e.child,
onPressed: e.onPressed,
ffi: ffi,
trailingIcon: e.trailingIcon);
}
}).toList());
}
}
@@ -938,13 +940,12 @@ class ScreenAdjustor {
}
updateScreen() async {
final v = await rustDeskWinManager.call(
WindowType.Main, kWindowGetWindowInfo, '');
final String valueStr = v.result;
if (valueStr.isEmpty) {
final String info =
isWeb ? screenInfo : await _getScreenInfoDesktop() ?? '';
if (info.isEmpty) {
_screen = null;
} else {
final screenMap = jsonDecode(valueStr);
final screenMap = jsonDecode(info);
_screen = window_size.Screen(
Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'],
screenMap['frame']['r'], screenMap['frame']['b']),
@@ -957,15 +958,23 @@ class ScreenAdjustor {
}
}
_getScreenInfoDesktop() async {
final v = await rustDeskWinManager.call(
WindowType.Main, kWindowGetWindowInfo, '');
return v.result;
}
Future<bool> isWindowCanBeAdjusted() async {
final viewStyle =
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
if (viewStyle != kRemoteViewStyleOriginal) {
return false;
}
final remoteCount = RemoteCountState.find().value;
if (remoteCount != 1) {
return false;
if (!isWeb) {
final remoteCount = RemoteCountState.find().value;
if (remoteCount != 1) {
return false;
}
}
if (_screen == null) {
return false;
@@ -1037,53 +1046,56 @@ class _DisplayMenuState extends State<_DisplayMenu> {
Widget build(BuildContext context) {
_screenAdjustor.updateScreen();
final menuChildren = <Widget>[
_screenAdjustor.adjustWindow(context),
viewStyle(),
scrollStyle(),
imageQuality(),
codec(),
_ResolutionsMenu(
id: widget.id,
ffi: widget.ffi,
screenAdjustor: _screenAdjustor,
),
// We may add this feature if it is needed and we have an EV certificate.
// _VirtualDisplayMenu(
// id: widget.id,
// ffi: widget.ffi,
// ),
Divider(),
toggles(),
];
// privacy mode
if (ffiModel.keyboard && pi.features.privacyMode) {
final privacyModeState = PrivacyModeState.find(id);
final privacyModeList =
toolbarPrivacyMode(privacyModeState, context, id, ffi);
if (privacyModeList.length == 1) {
menuChildren.add(CkbMenuButton(
value: privacyModeList[0].value,
onChanged: privacyModeList[0].onChanged,
child: privacyModeList[0].child,
ffi: ffi));
} else if (privacyModeList.length > 1) {
menuChildren.addAll([
Divider(),
_SubmenuButton(
ffi: widget.ffi,
child: Text(translate('Privacy mode')),
menuChildren: privacyModeList
.map((e) => CkbMenuButton(
value: e.value,
onChanged: e.onChanged,
child: e.child,
ffi: ffi))
.toList()),
]);
menuChildrenGetter() {
final menuChildren = <Widget>[
_screenAdjustor.adjustWindow(context),
viewStyle(),
scrollStyle(),
imageQuality(),
codec(),
_ResolutionsMenu(
id: widget.id,
ffi: widget.ffi,
screenAdjustor: _screenAdjustor,
),
// We may add this feature if it is needed and we have an EV certificate.
// _VirtualDisplayMenu(
// id: widget.id,
// ffi: widget.ffi,
// ),
Divider(),
toggles(),
];
// privacy mode
if (ffiModel.keyboard && pi.features.privacyMode) {
final privacyModeState = PrivacyModeState.find(id);
final privacyModeList =
toolbarPrivacyMode(privacyModeState, context, id, ffi);
if (privacyModeList.length == 1) {
menuChildren.add(CkbMenuButton(
value: privacyModeList[0].value,
onChanged: privacyModeList[0].onChanged,
child: privacyModeList[0].child,
ffi: ffi));
} else if (privacyModeList.length > 1) {
menuChildren.addAll([
Divider(),
_SubmenuButton(
ffi: widget.ffi,
child: Text(translate('Privacy mode')),
menuChildren: privacyModeList
.map((e) => CkbMenuButton(
value: e.value,
onChanged: e.onChanged,
child: e.child,
ffi: ffi))
.toList()),
]);
}
}
menuChildren.add(widget.pluginItem);
return menuChildren;
}
menuChildren.add(widget.pluginItem);
return _IconSubmenuButton(
tooltip: 'Display Settings',
@@ -1091,7 +1103,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
ffi: widget.ffi,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildren: menuChildren,
menuChildrenGetter: menuChildrenGetter,
);
}
@@ -1244,7 +1256,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
FFI get ffi => widget.ffi;
PeerInfo get pi => widget.ffi.ffiModel.pi;
FfiModel get ffiModel => widget.ffi.ffiModel;
Rect? get rect => ffiModel.rect;
Rect? get rect => scaledRect();
List<Resolution> get resolutions => pi.resolutions;
bool get isWayland => bind.mainCurrentIsWayland();
@@ -1254,6 +1266,20 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
_getLocalResolutionWayland();
}
Rect? scaledRect() {
final scale = pi.scaleOfDisplay(pi.currentDisplay);
final rect = ffiModel.rect;
if (rect == null) {
return null;
}
return Rect.fromLTWH(
rect.left,
rect.top,
rect.width / scale,
rect.height / scale,
);
}
@override
Widget build(BuildContext context) {
final isVirtualDisplay = ffiModel.isVirtualDisplayResolution;
@@ -1287,7 +1313,8 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
if (lastGroupValue == _kCustomResolutionValue) {
_groupValue = _kCustomResolutionValue;
} else {
_groupValue = '${rect?.width.toInt()}x${rect?.height.toInt()}';
_groupValue =
'${(rect?.width ?? 0).toInt()}x${(rect?.height ?? 0).toInt()}';
}
}
@@ -1321,6 +1348,14 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
final display = json.decode(mainDisplay);
if (display['w'] != null && display['h'] != null) {
_localResolution = Resolution(display['w'], display['h']);
if (isWeb) {
if (display['scaleFactor'] != null) {
_localResolution = Resolution(
(display['w'] / display['scaleFactor']).toInt(),
(display['h'] / display['scaleFactor']).toInt(),
);
}
}
}
} catch (e) {
debugPrint('Failed to decode $mainDisplay, $e');
@@ -1386,6 +1421,11 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
if (display == null) {
return Offstage();
}
if (!resolutions.any((e) =>
e.width == display.originalWidth &&
e.height == display.originalHeight)) {
return Offstage();
}
return Offstage(
offstage: !showOriginalBtn,
child: MenuButton(
@@ -1586,19 +1626,7 @@ class _KeyboardMenu extends StatelessWidget {
Widget build(BuildContext context) {
var ffiModel = Provider.of<FfiModel>(context);
if (!ffiModel.keyboard) return Offstage();
// If use flutter to grab keys, we can only use one mode.
// Map mode and Legacy mode, at least one of them is supported.
String? modeOnly;
if (isInputSourceFlutter) {
if (bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
modeOnly = kKeyMapMode;
} else if (bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: kKeyLegacyMode)) {
modeOnly = kKeyLegacyMode;
}
}
final toolbarToggles = toolbarKeyboardToggles(ffi)
toolbarToggles() => toolbarKeyboardToggles(ffi)
.map((e) => CkbMenuButton(
value: e.value, onChanged: e.onChanged, child: e.child, ffi: ffi))
.toList();
@@ -1608,18 +1636,18 @@ class _KeyboardMenu extends StatelessWidget {
ffi: ffi,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildren: [
keyboardMode(modeOnly),
localKeyboardType(),
inputSource(),
Divider(),
viewMode(),
Divider(),
...toolbarToggles,
]);
menuChildrenGetter: () => [
keyboardMode(),
localKeyboardType(),
inputSource(),
Divider(),
viewMode(),
Divider(),
...toolbarToggles(),
]);
}
keyboardMode(String? modeOnly) {
keyboardMode() {
return futureBuilder(future: () async {
return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ??
kKeyLegacyMode;
@@ -1639,6 +1667,19 @@ class _KeyboardMenu extends StatelessWidget {
await ffi.inputModel.updateKeyboardMode();
}
// If use flutter to grab keys, we can only use one mode.
// Map mode and Legacy mode, at least one of them is supported.
String? modeOnly;
if (isInputSourceFlutter) {
if (bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
modeOnly = kKeyMapMode;
} else if (bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: kKeyLegacyMode)) {
modeOnly = kKeyLegacyMode;
}
}
for (InputModeMenu mode in modes) {
if (modeOnly != null && mode.key != modeOnly) {
continue;
@@ -1732,7 +1773,7 @@ class _KeyboardMenu extends StatelessWidget {
? (value) async {
if (value == null) return;
await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: 'view-only');
sessionId: ffi.sessionId, value: kOptionViewOnly);
ffiModel.setViewOnly(id, value);
}
: null,
@@ -1767,7 +1808,7 @@ class _ChatMenuState extends State<_ChatMenu> {
ffi: widget.ffi,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildren: [textChat(), voiceCall()]);
menuChildrenGetter: () => [textChat(), voiceCall()]);
}
textChat() {
@@ -1971,7 +2012,7 @@ class _IconSubmenuButton extends StatefulWidget {
final Widget? icon;
final Color color;
final Color hoverColor;
final List<Widget> menuChildren;
final List<Widget> Function() menuChildrenGetter;
final MenuStyle? menuStyle;
final FFI ffi;
final double? width;
@@ -1983,7 +2024,7 @@ class _IconSubmenuButton extends StatefulWidget {
required this.tooltip,
required this.color,
required this.hoverColor,
required this.menuChildren,
required this.menuChildrenGetter,
required this.ffi,
this.menuStyle,
this.width,
@@ -2027,7 +2068,8 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
color: hover ? widget.hoverColor : widget.color,
),
child: icon))),
menuChildren: widget.menuChildren
menuChildren: widget
.menuChildrenGetter()
.map((e) => _buildPointerTrackWidget(e, widget.ffi))
.toList()));
return MenuBar(children: [

View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:ui' as ui;
@@ -13,7 +12,6 @@ import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:scroll_pos/scroll_pos.dart';
@@ -166,7 +164,8 @@ class DesktopTabController {
}));
}
});
if (callOnSelected) {
if ((isDesktop && (bind.isIncomingOnly() || bind.isOutgoingOnly())) ||
callOnSelected) {
if (state.value.tabs.length > index) {
final key = state.value.tabs[index].key;
onSelected?.call(key);
@@ -375,7 +374,8 @@ class DesktopTab extends StatelessWidget {
Expanded(
child: GestureDetector(
// custom double tap handler
onTap: showMaximize
onTap: !(bind.isIncomingOnly() && isInHomePage()) &&
showMaximize
? () {
final current = DateTime.now().millisecondsSinceEpoch;
final elapsed = current - _lastClickTime;
@@ -391,20 +391,17 @@ class DesktopTab extends StatelessWidget {
child: Row(
children: [
Offstage(
offstage: !Platform.isMacOS,
offstage: !isMacOS,
child: const SizedBox(
width: 78,
)),
Offstage(
offstage: kUseCompatibleUiMode || Platform.isMacOS,
offstage: kUseCompatibleUiMode || isMacOS,
child: Row(children: [
Offstage(
offstage: !showLogo,
child: SvgPicture.asset(
'assets/logo.svg',
width: 16,
height: 16,
)),
offstage: !showLogo,
child: loadIcon(16),
),
Offstage(
offstage: !showTitle,
child: const Text(
@@ -586,8 +583,8 @@ class WindowActionPanelState extends State<WindowActionPanel>
mainWindowClose() async => await windowManager.hide();
notMainWindowClose(WindowController controller) async {
if (widget.tabController.length != 0) {
debugPrint("close not emtpy multiwindow from taskbar");
if (Platform.isWindows) {
debugPrint("close not empty multiwindow from taskbar");
if (isWindows) {
await controller.show();
await controller.focus();
final res = await widget.onClose?.call() ?? true;
@@ -622,7 +619,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
}
// macOS specific workaround, the window is not hiding when in fullscreen.
if (Platform.isMacOS && await windowManager.isFullScreen()) {
if (isMacOS && await windowManager.isFullScreen()) {
stateGlobal.closeOnFullscreen ??= true;
await windowManager.setFullScreen(false);
await macOSWindowClose(
@@ -636,7 +633,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
} else {
// it's safe to hide the subwindow
final controller = WindowController.fromWindowId(kWindowId!);
if (Platform.isMacOS) {
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.
@@ -672,7 +669,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
child: Row(
children: [
Offstage(
offstage: !widget.showMinimize || Platform.isMacOS,
offstage: !widget.showMinimize || isMacOS,
child: ActionIcon(
message: 'Minimize',
icon: IconFont.min,
@@ -686,7 +683,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
isClose: false,
)),
Offstage(
offstage: !widget.showMaximize || Platform.isMacOS,
offstage: !widget.showMaximize || isMacOS,
child: Obx(() => ActionIcon(
message: stateGlobal.isMaximized.isTrue
? 'Restore'
@@ -694,11 +691,13 @@ class WindowActionPanelState extends State<WindowActionPanel>
icon: stateGlobal.isMaximized.isTrue
? IconFont.restore
: IconFont.max,
onTap: _toggleMaximize,
onTap: bind.isIncomingOnly() && isInHomePage()
? null
: _toggleMaximize,
isClose: false,
))),
Offstage(
offstage: !widget.showClose || Platform.isMacOS,
offstage: !widget.showClose || isMacOS,
child: ActionIcon(
message: 'Close',
icon: IconFont.close,
@@ -1110,7 +1109,7 @@ class _CloseButton extends StatelessWidget {
class ActionIcon extends StatefulWidget {
final String? message;
final IconData icon;
final Function() onTap;
final GestureTapCallback? onTap;
final bool isClose;
final double iconSize;
final double boxSize;
@@ -1119,7 +1118,7 @@ class ActionIcon extends StatefulWidget {
{Key? key,
this.message,
required this.icon,
required this.onTap,
this.onTap,
this.isClose = false,
this.iconSize = _kActionIconSize,
this.boxSize = _kTabBarHeight - 1})
@@ -1143,24 +1142,30 @@ class _ActionIconState extends State<ActionIcon> {
return Tooltip(
message: widget.message != null ? translate(widget.message!) : "",
waitDuration: const Duration(seconds: 1),
child: Obx(
() => InkWell(
hoverColor: widget.isClose
? const Color.fromARGB(255, 196, 43, 28)
: MyTheme.tabbar(context).hoverColor,
onHover: (value) => hover.value = value,
onTap: widget.onTap,
child: SizedBox(
height: widget.boxSize,
width: widget.boxSize,
child: Icon(
widget.icon,
color: hover.value && widget.isClose
? Colors.white
: MyTheme.tabbar(context).unSelectedIconColor,
size: widget.iconSize,
),
),
child: InkWell(
hoverColor: widget.isClose
? const Color.fromARGB(255, 196, 43, 28)
: MyTheme.tabbar(context).hoverColor,
onHover: (value) => hover.value = value,
onTap: widget.onTap,
child: SizedBox(
height: widget.boxSize,
width: widget.boxSize,
child: widget.onTap == null
? Icon(
widget.icon,
color: Colors.grey,
size: widget.iconSize,
)
: Obx(
() => Icon(
widget.icon,
color: hover.value && widget.isClose
? Colors.white
: MyTheme.tabbar(context).unSelectedIconColor,
size: widget.iconSize,
),
),
),
),
);

View File

@@ -14,21 +14,21 @@ import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/plugin/handlers.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
// import 'package:window_manager/window_manager.dart';
import 'common.dart';
import 'consts.dart';
import 'mobile/pages/home_page.dart';
import 'mobile/pages/server_page.dart';
import 'models/platform_model.dart';
import 'package:flutter_hbb/plugin/handlers.dart'
if (dart.library.html) 'package:flutter_hbb/web/plugin/handlers.dart';
/// Basic window and launch properties.
int? kWindowId;
WindowType? kWindowType;
@@ -36,6 +36,7 @@ late List<String> kBootArgs;
Future<void> main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
debugPrint("launch args: $args");
kBootArgs = List.from(args);
@@ -47,7 +48,7 @@ Future<void> main(List<String> args) async {
if (args.isNotEmpty && args.first == 'multi_window') {
kWindowId = int.parse(args[1]);
stateGlobal.setWindowId(kWindowId!);
if (!Platform.isMacOS) {
if (!isMacOS) {
WindowController.fromWindowId(kWindowId!).showTitleBar(false);
}
final argument = args[2].isEmpty
@@ -58,14 +59,12 @@ Future<void> main(List<String> args) async {
// Because stateGlobal.windowId is a global value.
argument['windowId'] = kWindowId;
kWindowType = type.windowType;
final windowName = getWindowName();
switch (kWindowType) {
case WindowType.RemoteDesktop:
desktopType = DesktopType.remote;
runMultiWindow(
argument,
kAppTypeDesktopRemote,
windowName,
);
break;
case WindowType.FileTransfer:
@@ -73,7 +72,6 @@ Future<void> main(List<String> args) async {
runMultiWindow(
argument,
kAppTypeDesktopFileTransfer,
windowName,
);
break;
case WindowType.PortForward:
@@ -81,7 +79,6 @@ Future<void> main(List<String> args) async {
runMultiWindow(
argument,
kAppTypeDesktopPortForward,
windowName,
);
break;
default:
@@ -128,6 +125,7 @@ void runMainApp(bool startService) async {
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser();
runApp(App());
// Set window option.
WindowOptions windowOptions = getHiddenTitleBarWindowOptions();
windowManager.waitUntilReadyToShow(windowOptions, () async {
@@ -146,25 +144,26 @@ void runMainApp(bool startService) async {
}
windowManager.setOpacity(1);
windowManager.setTitle(getWindowName());
windowManager.setResizable(!bind.isIncomingOnly());
});
}
void runMobileApp() async {
await initEnv(kAppTypeMain);
if (isAndroid) androidChannelInit();
platformFFI.syncAndroidServiceAppDirConfigPath();
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser();
runApp(App());
await initUniLinks();
if (!isWeb) await initUniLinks();
}
void runMultiWindow(
Map<String, dynamic> argument,
String appType,
String title,
) async {
await initEnv(appType);
final title = getWindowName();
// set prevent close to true, we handle close event manually
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
late Widget widget;

View File

@@ -53,7 +53,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
@override
void initState() {
super.initState();
_uniLinksSubscription = listenUniLinks();
if (!isWeb) _uniLinksSubscription = listenUniLinks();
if (_idController.text.isEmpty) {
() async {
final lastRemoteId = await bind.mainGetLastRemoteId();
@@ -166,6 +166,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
platform: '',
tags: [],
hash: '',
password: '',
forceAlwaysRelay: false,
rdpPort: '',
rdpUsername: '',

View File

@@ -3,7 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:toggle_switch/toggle_switch.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
@@ -12,8 +11,12 @@ import '../../common.dart';
import '../../common/widgets/dialog.dart';
class FileManagerPage extends StatefulWidget {
FileManagerPage({Key? key, required this.id}) : super(key: key);
FileManagerPage(
{Key? key, required this.id, this.password, this.isSharedPassword})
: super(key: key);
final String id;
final String? password;
final bool? isSharedPassword;
@override
State<StatefulWidget> createState() => _FileManagerPageState();
@@ -68,7 +71,10 @@ class _FileManagerPageState extends State<FileManagerPage> {
@override
void initState() {
super.initState();
gFFI.start(widget.id, isFileTransfer: true);
gFFI.start(widget.id,
isFileTransfer: true,
password: widget.password,
isSharedPassword: widget.isSharedPassword);
WidgetsBinding.instance.addPostFrameCallback((_) {
gFFI.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);

View File

@@ -25,9 +25,12 @@ import '../widgets/dialog.dart';
final initText = '1' * 1024;
class RemotePage extends StatefulWidget {
RemotePage({Key? key, required this.id}) : super(key: key);
RemotePage({Key? key, required this.id, this.password, this.isSharedPassword})
: super(key: key);
final String id;
final String? password;
final bool? isSharedPassword;
@override
State<RemotePage> createState() => _RemotePageState();
@@ -54,7 +57,12 @@ class _RemotePageState extends State<RemotePage> {
@override
void initState() {
super.initState();
gFFI.start(widget.id);
gFFI.ffiModel.updateEventListener(sessionId, widget.id);
gFFI.start(
widget.id,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
gFFI.dialogManager
@@ -62,7 +70,6 @@ class _RemotePageState extends State<RemotePage> {
});
WakelockPlus.enable();
_physicalFocusNode.requestFocus();
gFFI.ffiModel.updateEventListener(sessionId, widget.id);
gFFI.inputModel.listenToMouse(true);
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
keyboardSubscription =
@@ -288,25 +295,26 @@ class _RemotePageState extends State<RemotePage> {
: Offstage(),
],
)),
body: getRawPointerAndKeyBody(Overlay(
initialEntries: [
OverlayEntry(builder: (context) {
return Container(
body: Obx(
() => getRawPointerAndKeyBody(Overlay(
initialEntries: [
OverlayEntry(builder: (context) {
return Container(
color: Colors.black,
child: isWebDesktop
? getBodyForDesktopWithListener(keyboard)
: SafeArea(child:
OrientationBuilder(builder: (ctx, orientation) {
if (_currentOrientation != orientation) {
Timer(const Duration(milliseconds: 200), () {
gFFI.dialogManager
.resetMobileActionsOverlay(ffi: gFFI);
_currentOrientation = orientation;
gFFI.canvasModel.updateViewStyle();
});
}
return Obx(
() => Container(
: SafeArea(
child:
OrientationBuilder(builder: (ctx, orientation) {
if (_currentOrientation != orientation) {
Timer(const Duration(milliseconds: 200), () {
gFFI.dialogManager
.resetMobileActionsOverlay(ffi: gFFI);
_currentOrientation = orientation;
gFFI.canvasModel.updateViewStyle();
});
}
return Container(
color: MyTheme.canvasColor,
child: inputModel.isPhysicalMouse.value
? getBodyForMobile()
@@ -314,12 +322,14 @@ class _RemotePageState extends State<RemotePage> {
child: getBodyForMobile(),
ffi: gFFI,
),
),
);
})));
})
],
))),
);
}),
),
);
})
],
)),
)),
);
}

View File

@@ -8,7 +8,7 @@ import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'package:zxing2/qrcode.dart';
import '../../common.dart';
import '../../consts.dart';
import '../../models/platform_model.dart';
import '../widgets/dialog.dart';
class ScanPage extends StatefulWidget {
@@ -61,7 +61,7 @@ class _ScanPageState extends State<ScanPage> {
var reader = QRCodeReader();
try {
var result = reader.decode(bitmap);
if (result.text.startsWith(kUniLinksPrefix)) {
if (result.text.startsWith(bind.mainUriPrefixSync())) {
handleUriLink(uriString: result.text);
} else {
showServerSettingFromQr(result.text);

View File

@@ -42,25 +42,21 @@ class ServerPage extends StatefulWidget implements PageShape {
return [
PopupMenuItem(
enabled: gFFI.serverModel.connectStatus > 0,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
value: "changeID",
child: Text(translate("Change ID")),
),
const PopupMenuDivider(),
PopupMenuItem(
padding: const EdgeInsets.symmetric(horizontal: 0.0),
value: 'AcceptSessionsViaPassword',
child: listTile(
'Accept sessions via password', approveMode == 'password'),
),
PopupMenuItem(
padding: const EdgeInsets.symmetric(horizontal: 0.0),
value: 'AcceptSessionsViaClick',
child:
listTile('Accept sessions via click', approveMode == 'click'),
),
PopupMenuItem(
padding: const EdgeInsets.symmetric(horizontal: 0.0),
value: "AcceptSessionsViaBoth",
child: listTile("Accept sessions via both",
approveMode != 'password' && approveMode != 'click'),
@@ -69,35 +65,30 @@ class ServerPage extends StatefulWidget implements PageShape {
if (showPasswordOption &&
verificationMethod != kUseTemporaryPassword)
PopupMenuItem(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
value: "setPermanentPassword",
child: Text(translate("Set permanent password")),
),
if (showPasswordOption &&
verificationMethod != kUsePermanentPassword)
PopupMenuItem(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
value: "setTemporaryPasswordLength",
child: Text(translate("One-time password length")),
),
if (showPasswordOption) const PopupMenuDivider(),
if (showPasswordOption)
PopupMenuItem(
padding: const EdgeInsets.symmetric(horizontal: 0.0),
value: kUseTemporaryPassword,
child: listTile('Use one-time password',
verificationMethod == kUseTemporaryPassword),
),
if (showPasswordOption)
PopupMenuItem(
padding: const EdgeInsets.symmetric(horizontal: 0.0),
value: kUsePermanentPassword,
child: listTile('Use permanent password',
verificationMethod == kUsePermanentPassword),
),
if (showPasswordOption)
PopupMenuItem(
padding: const EdgeInsets.symmetric(horizontal: 0.0),
value: kUseBothPasswords,
child: listTile(
'Use both passwords',
@@ -392,7 +383,7 @@ class ScamWarningDialogState extends State<ScamWarningDialog> {
Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
primary: Colors.blueAccent,
backgroundColor: Colors.blueAccent,
),
child: Text(
translate("Decline"),

File diff suppressed because it is too large Load Diff

View File

@@ -3,13 +3,16 @@ import 'package:flutter_gpu_texture_renderer/flutter_gpu_texture_renderer.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:get/get.dart';
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
import '../../common.dart';
import './platform_model.dart';
final useTextureRender =
bind.mainHasPixelbufferTextureRender() || bind.mainHasGpuTextureRender();
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
// Feature flutter_texture_render need to be enabled if feature gpucodec is enabled.
final useTextureRender = !isWeb &&
(bind.mainHasPixelbufferTextureRender() || bind.mainHasGpuTextureRender());
class _PixelbufferTexture {
int _textureKey = -1;

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
@@ -292,7 +291,7 @@ class FileController {
name: isLocal ? "local_show_hidden" : "remote_show_hidden"))
.isNotEmpty;
options.value.isWindows = isLocal
? Platform.isWindows
? isWindows
: rootState.target?.ffiModel.pi.platform == kPeerPlatformWindows;
await Future.delayed(Duration(milliseconds: 100));

View File

@@ -193,6 +193,7 @@ class InputModel {
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
InputModel(this.parent) {
sessionId = parent.target!.sessionId;
@@ -207,7 +208,7 @@ class InputModel {
updateKeyboardMode() async {
// * Currently mobile does not enable map mode
if (isDesktop) {
if (isDesktop || isWebDesktop) {
if (keyboardMode.isEmpty) {
keyboardMode =
await bind.sessionGetKeyboardMode(sessionId: sessionId) ??
@@ -217,7 +218,8 @@ class InputModel {
}
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
if (isDesktop && !isInputSourceFlutter) {
if (isViewOnly) return KeyEventResult.handled;
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) {
return KeyEventResult.handled;
}
@@ -256,7 +258,7 @@ class InputModel {
}
// * Currently mobile does not enable map mode
if (isDesktop && keyboardMode == 'map') {
if ((isDesktop || isWebDesktop) && keyboardMode == 'map') {
mapKeyboardMode(e);
} else {
legacyKeyboardMode(e);
@@ -467,6 +469,7 @@ class InputModel {
void onPointHoverImage(PointerHoverEvent e) {
_stopFling = true;
if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (!isPhysicalMouse.value) {
isPhysicalMouse.value = true;
@@ -479,7 +482,7 @@ class InputModel {
void onPointerPanZoomStart(PointerPanZoomStartEvent e) {
_lastScale = 1.0;
_stopFling = true;
if (isViewOnly) return;
if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', 'pan_start', e.position);
}
@@ -487,6 +490,7 @@ class InputModel {
// https://docs.flutter.dev/release/breaking-changes/trackpad-gestures
void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) {
if (isViewOnly) return;
if (peerPlatform != kPeerPlatformAndroid) {
final scale = ((e.scale - _lastScale) * 1000).toInt();
_lastScale = e.scale;
@@ -612,6 +616,7 @@ class InputModel {
void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage ${e.kind}");
_stopFling = true;
if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) {
if (isPhysicalMouse.value) {
isPhysicalMouse.value = false;
@@ -623,6 +628,7 @@ class InputModel {
}
void onPointUpImage(PointerUpEvent e) {
if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) {
handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
@@ -630,6 +636,7 @@ class InputModel {
}
void onPointMoveImage(PointerMoveEvent e) {
if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) {
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
@@ -637,6 +644,7 @@ class InputModel {
}
void onPointerSignalImage(PointerSignalEvent e) {
if (isViewOnly) return;
if (e is PointerScrollEvent) {
var dx = e.scrollDelta.dx.toInt();
var dy = e.scrollDelta.dy.toInt();
@@ -902,9 +910,11 @@ class InputModel {
int minX = rect.left.toInt();
// https://github.com/rustdesk/rustdesk/issues/6678
// For Windows, [0,maxX], [0,maxY] should be set to enable window snapping.
int maxX = (rect.left + rect.width).toInt() - (peerPlatform == kPeerPlatformWindows ? 0 : 1);
int maxX = (rect.left + rect.width).toInt() -
(peerPlatform == kPeerPlatformWindows ? 0 : 1);
int minY = rect.top.toInt();
int maxY = (rect.top + rect.height).toInt() - (peerPlatform == kPeerPlatformWindows ? 0 : 1);
int maxY = (rect.top + rect.height).toInt() -
(peerPlatform == kPeerPlatformWindows ? 0 : 1);
evtX = trySetNearestRange(evtX, minX, maxX, 5);
evtY = trySetNearestRange(evtY, minY, maxY, 5);
if (kind == kPointerEventKindMouse) {

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui;
@@ -9,7 +8,6 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/generated_bridge.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/cm_file_model.dart';
@@ -27,7 +25,6 @@ import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:tuple/tuple.dart';
import 'package:image/image.dart' as img2;
import 'package:flutter_custom_cursor/cursor_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:uuid/uuid.dart';
@@ -39,6 +36,11 @@ import '../common/widgets/dialog.dart';
import 'input_model.dart';
import 'platform_model.dart';
import 'package:flutter_hbb/generated_bridge.dart'
if (dart.library.html) 'package:flutter_hbb/web/bridge.dart';
import 'package:flutter_hbb/native/custom_cursor.dart'
if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart';
typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
final _constSessionId = Uuid().v4obj();
@@ -138,21 +140,29 @@ class FfiModel with ChangeNotifier {
sessionId = parent.target!.sessionId;
}
Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays);
Rect? displaysRect() => _getDisplaysRect(_pi.getCurDisplays());
Rect? _getDisplaysRect(List<Display> displays) {
Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true);
Rect? displaysRect() => _getDisplaysRect(_pi.getCurDisplays(), false);
Rect? _getDisplaysRect(List<Display> displays, bool useDisplayScale) {
if (displays.isEmpty) {
return null;
}
int scale(int len, double s) {
if (useDisplayScale) {
return len.toDouble() ~/ s;
} else {
return len;
}
}
double l = displays[0].x;
double t = displays[0].y;
double r = displays[0].x + displays[0].width;
double b = displays[0].y + displays[0].height;
double r = displays[0].x + scale(displays[0].width, displays[0].scale);
double b = displays[0].y + scale(displays[0].height, displays[0].scale);
for (var display in displays.sublist(1)) {
l = min(l, display.x);
t = min(t, display.y);
r = max(r, display.x + display.width);
b = max(b, display.y + display.height);
r = max(r, display.x + scale(display.width, display.scale));
b = max(b, display.y + scale(display.height, display.scale));
}
return Rect.fromLTRB(l, t, r, b);
}
@@ -342,30 +352,43 @@ class FfiModel with ChangeNotifier {
handleReloading(evt);
} else if (name == 'plugin_option') {
handleOption(evt);
} else if (name == "sync_peer_password_to_ab") {
} else if (name == "sync_peer_hash_password_to_personal_ab") {
if (desktopType == DesktopType.main) {
final id = evt['id'];
final password = evt['password'];
if (id != null && password != null) {
if (gFFI.abModel
.changePassword(id.toString(), password.toString())) {
gFFI.abModel.pushAb(toastIfFail: false, toastIfSucc: false);
}
final hash = evt['hash'];
if (id != null && hash != null) {
gFFI.abModel
.changePersonalHashPassword(id.toString(), hash.toString());
}
}
} else if (name == "cm_file_transfer_log") {
if (isDesktop) {
gFFI.cmFileModel.onFileTransferLog(evt);
}
} else if (name == 'sync_peer_option') {
_handleSyncPeerOption(evt, peerId);
} else {
debugPrint('Unknown event name: $name');
}
};
}
_handleSyncPeerOption(Map<String, dynamic> evt, String peer) {
final k = evt['k'];
final v = evt['v'];
if (k == kOptionViewOnly) {
setViewOnly(peer, v as bool);
} else if (k == 'keyboard_mode') {
parent.target?.inputModel.updateKeyboardMode();
} else if (k == 'input_source') {
stateGlobal.getInputSource(force: true);
}
}
onUrlSchemeReceived(Map<String, dynamic> evt) {
final url = evt['url'].toString().trim();
if (url.startsWith(kUniLinksPrefix) && handleUriLink(uriString: url)) {
if (url.startsWith(bind.mainUriPrefixSync()) &&
handleUriLink(uriString: url)) {
return;
}
switch (url) {
@@ -407,7 +430,7 @@ class FfiModel with ChangeNotifier {
}
handleAliasChanged(Map<String, dynamic> evt) {
if (!isDesktop) return;
if (!(isDesktop || isWebDesktop)) return;
final String peerId = evt['id'];
final String alias = evt['alias'];
String label = getDesktopTabLabel(peerId, alias);
@@ -462,6 +485,7 @@ class FfiModel with ChangeNotifier {
int.tryParse(evt['original_width']) ?? kInvalidResolutionValue;
newDisplay.originalHeight =
int.tryParse(evt['original_height']) ?? kInvalidResolutionValue;
newDisplay._scale = _pi.scaleOfDisplay(display);
_pi.displays[display] = newDisplay;
if (!_pi.isSupportMultiUiSession || _pi.currentDisplay == display) {
@@ -667,7 +691,7 @@ class FfiModel with ChangeNotifier {
// Because this function is asynchronous, there's an "await" in this function.
cachedPeerData.peerInfo = {...evt};
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
// Recent peer is updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
bind.mainLoadRecentPeers();
parent.target?.dialogManager.dismissAll();
@@ -728,7 +752,7 @@ class FfiModel with ChangeNotifier {
setViewOnly(
peerId,
bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: 'view-only'));
sessionId: sessionId, arg: kOptionViewOnly));
}
if (connType == ConnType.defaultConn) {
final platformAdditions = evt['platform_additions'];
@@ -744,7 +768,7 @@ class FfiModel with ChangeNotifier {
_pi.isSet.value = true;
stateGlobal.resetLastResolutionGroupValues(peerId);
if (isDesktop) {
if (isDesktop || isWebDesktop) {
checkDesktopKeyboardMode();
}
@@ -845,7 +869,16 @@ class FfiModel with ChangeNotifier {
handleResolutions(String id, dynamic resolutions) {
try {
final List<dynamic> dynamicArray = jsonDecode(resolutions as String);
final resolutionsObj = json.decode(resolutions as String);
late List<dynamic> dynamicArray;
if (resolutionsObj is Map) {
// The web version
dynamicArray = (resolutionsObj as Map<String, dynamic>)['resolutions']
as List<dynamic>;
} else {
// The rust version
dynamicArray = resolutionsObj as List<dynamic>;
}
List<Resolution> arr = List.empty(growable: true);
for (int i = 0; i < dynamicArray.length; i++) {
var width = dynamicArray[i]["width"];
@@ -876,6 +909,8 @@ class FfiModel with ChangeNotifier {
d.cursorEmbedded = evt['cursor_embedded'] == 1;
d.originalWidth = evt['original_width'] ?? kInvalidResolutionValue;
d.originalHeight = evt['original_height'] ?? kInvalidResolutionValue;
double v = (evt['scale']?.toDouble() ?? 100.0) / 100;
d._scale = v > 1.0 ? v : 1.0;
return d;
}
@@ -1080,7 +1115,7 @@ class ImageModel with ChangeNotifier {
update(ui.Image? image) async {
if (_image == null && image != null) {
if (isWebDesktop || isDesktop) {
if (isDesktop || isWebDesktop) {
await parent.target?.canvasModel.updateViewStyle();
await parent.target?.canvasModel.updateScrollStyle();
} else {
@@ -1254,18 +1289,15 @@ class CanvasModel with ChangeNotifier {
double get scrollX => _scrollX;
double get scrollY => _scrollY;
static double get leftToEdge => (isDesktop || isWebDesktop)
? windowBorderWidth + kDragToResizeAreaPadding.left
: 0;
static double get rightToEdge => (isDesktop || isWebDesktop)
? windowBorderWidth + kDragToResizeAreaPadding.right
: 0;
static double get topToEdge => (isDesktop || isWebDesktop)
static double get leftToEdge =>
isDesktop ? windowBorderWidth + kDragToResizeAreaPadding.left : 0;
static double get rightToEdge =>
isDesktop ? windowBorderWidth + kDragToResizeAreaPadding.right : 0;
static double get topToEdge => isDesktop
? tabBarHeight + windowBorderWidth + kDragToResizeAreaPadding.top
: 0;
static double get bottomToEdge => (isDesktop || isWebDesktop)
? windowBorderWidth + kDragToResizeAreaPadding.bottom
: 0;
static double get bottomToEdge =>
isDesktop ? windowBorderWidth + kDragToResizeAreaPadding.bottom : 0;
updateViewStyle({refreshMousePos = true}) async {
Size getSize() {
@@ -1388,7 +1420,7 @@ class CanvasModel with ChangeNotifier {
// If keyboard is not permitted, do not move cursor when mouse is moving.
if (parent.target != null && parent.target!.ffiModel.keyboard) {
// Draw cursor if is not desktop.
if (!isDesktop) {
if (!(isDesktop || isWebDesktop)) {
parent.target!.cursorModel.moveLocal(x, y);
} else {
try {
@@ -1511,7 +1543,7 @@ class CursorData {
}
if (_doubleToInt(oldScale) != _doubleToInt(scale)) {
if (Platform.isWindows) {
if (isWindows) {
data = img2
.copyResize(
image,
@@ -1590,7 +1622,7 @@ class PredefinedCursor {
data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888);
double scale = 1.0;
if (Platform.isWindows) {
if (isWindows) {
data = _image2!.getBytes(order: img2.ChannelOrder.bgra);
} else {
data = Uint8List.fromList(img2.encodePng(_image2!));
@@ -1815,7 +1847,7 @@ class CursorModel with ChangeNotifier {
Uint8List? data;
img2.Image imgOrigin = img2.Image.fromBytes(
width: w, height: h, bytes: rgba.buffer, order: img2.ChannelOrder.rgba);
if (Platform.isWindows) {
if (isWindows) {
data = imgOrigin.getBytes(order: img2.ChannelOrder.bgra);
} else {
ByteData? imgBytes =
@@ -1920,7 +1952,7 @@ class CursorModel with ChangeNotifier {
final keys = {...cachedKeys};
for (var k in keys) {
debugPrint("deleting cursor with key $k");
CursorManager.instance.deleteCursor(k);
deleteCustomCursor(k);
}
}
}
@@ -2153,6 +2185,7 @@ class FFI {
bool isRdp = false,
String? switchUuid,
String? password,
bool? isSharedPassword,
bool? forceRelay,
int? tabWindowId,
int? display,
@@ -2173,6 +2206,7 @@ class FFI {
imageModel.id = id;
cursorModel.peerId = id;
}
// If tabWindowId != null, this session is a "tab -> window" one.
// Else this session is a new one.
if (tabWindowId == null) {
@@ -2186,6 +2220,7 @@ class FFI {
switchUuid: switchUuid ?? '',
forceRelay: forceRelay ?? false,
password: password ?? '',
isSharedPassword: isSharedPassword ?? false,
);
} else if (display != null) {
if (displays == null) {
@@ -2203,7 +2238,18 @@ class FFI {
sessionId: sessionId, displays: Int32List.fromList(displays));
ffiModel.pi.currentDisplay = display;
}
if (connType == ConnType.defaultConn && useTextureRender) {
textureModel.updateCurrentDisplay(display ?? 0);
}
final stream = bind.sessionStart(sessionId: sessionId, id: id);
if (isWeb) {
platformFFI.setRgbaCallback((int display, Uint8List data) {
onEvent2UIRgba();
imageModel.onRgba(display, data);
});
return;
}
final cb = ffiModel.startEventListener(sessionId, id);
// Force refresh displays.
@@ -2370,6 +2416,8 @@ class Display {
bool cursorEmbedded = false;
int originalWidth = kInvalidResolutionValue;
int originalHeight = kInvalidResolutionValue;
double _scale = 1.0;
double get scale => _scale > 1.0 ? _scale : 1.0;
Display() {
width = (isDesktop || isWebDesktop)
@@ -2445,7 +2493,8 @@ class PeerInfo with ChangeNotifier {
List<int> get virtualDisplays => List<int>.from(
platformAdditions[kPlatformAdditionsVirtualDisplays] ?? []);
bool get isSupportMultiDisplay => isDesktop && isSupportMultiUiSession;
bool get isSupportMultiDisplay =>
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
@@ -2489,6 +2538,13 @@ class PeerInfo with ChangeNotifier {
}
}
}
double scaleOfDisplay(int display) {
if (display >= 0 && display < displays.length) {
return displays[display].scale;
}
return 1.0;
}
}
const canvasKey = 'canvas';

View File

@@ -111,13 +111,13 @@ class PlatformFFI {
/// Init the FFI class, loads the native Rust core library.
Future<void> init(String appType) async {
_appType = appType;
final dylib = Platform.isAndroid
final dylib = isAndroid
? DynamicLibrary.open('librustdesk.so')
: Platform.isLinux
: isLinux
? DynamicLibrary.open('librustdesk.so')
: Platform.isWindows
: isWindows
? DynamicLibrary.open('librustdesk.dll')
: Platform.isMacOS
: isMacOS
? DynamicLibrary.open("liblibrustdesk.dylib")
: DynamicLibrary.process();
debugPrint('initializing FFI $_appType');
@@ -130,11 +130,12 @@ class PlatformFFI {
debugPrint('Failed to get documents directory: $e');
}
_ffiBind = RustdeskImpl(dylib);
if (Platform.isLinux) {
if (isLinux) {
// Start a dbus service, no need to await
_ffiBind.mainStartDbusServer();
_ffiBind.mainStartPa();
} else if (Platform.isMacOS && isMain) {
} else if (isMacOS && isMain) {
// Start ipc service for uri links.
_ffiBind.mainStartIpcUrlServer();
}
@@ -154,20 +155,20 @@ class PlatformFFI {
String id = 'NA';
String name = 'Flutter';
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
if (isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
name = '${androidInfo.brand}-${androidInfo.model}';
id = androidInfo.id.hashCode.toString();
androidVersion = androidInfo.version.sdkInt;
} else if (Platform.isIOS) {
} else if (isIOS) {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
name = iosInfo.utsname.machine;
id = iosInfo.identifierForVendor.hashCode.toString();
} else if (Platform.isLinux) {
} else if (isLinux) {
LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo;
name = linuxInfo.name;
id = linuxInfo.machineId ?? linuxInfo.id;
} else if (Platform.isWindows) {
} else if (isWindows) {
try {
// request windows build number to fix overflow on win7
windowsBuildNumber = getWindowsTargetBuildNumber();
@@ -179,7 +180,7 @@ class PlatformFFI {
name = "unknown";
id = "unknown";
}
} else if (Platform.isMacOS) {
} else if (isMacOS) {
MacOsDeviceInfo macOsInfo = await deviceInfo.macOsInfo;
name = macOsInfo.computerName;
id = macOsInfo.systemGUID ?? '';
@@ -246,7 +247,7 @@ class PlatformFFI {
_eventCallback = fun;
}
void setRgbaCallback(void Function(Uint8List) fun) async {}
void setRgbaCallback(void Function(int, Uint8List) fun) async {}
void startDesktopWebListener() {}

View File

@@ -7,7 +7,8 @@ import 'package:collection/collection.dart';
class Peer {
final String id;
String hash;
String hash; // personal ab hash password
String password; // shared ab password
String username; // pc username
String hostname;
String platform;
@@ -18,6 +19,7 @@ class Peer {
String rdpUsername;
bool online = false;
String loginName; //login username
bool? sameServer;
String getId() {
if (alias != '') {
@@ -29,6 +31,7 @@ class Peer {
Peer.fromJson(Map<String, dynamic> json)
: id = json['id'] ?? '',
hash = json['hash'] ?? '',
password = json['password'] ?? '',
username = json['username'] ?? '',
hostname = json['hostname'] ?? '',
platform = json['platform'] ?? '',
@@ -37,12 +40,14 @@ class Peer {
forceAlwaysRelay = json['forceAlwaysRelay'] == 'true',
rdpPort = json['rdpPort'] ?? '',
rdpUsername = json['rdpUsername'] ?? '',
loginName = json['loginName'] ?? '';
loginName = json['loginName'] ?? '',
sameServer = json['same_server'];
Map<String, dynamic> toJson() {
return <String, dynamic>{
"id": id,
"hash": hash,
"password": password,
"username": username,
"hostname": hostname,
"platform": platform,
@@ -52,19 +57,23 @@ class Peer {
"rdpPort": rdpPort,
"rdpUsername": rdpUsername,
'loginName': loginName,
'same_server': sameServer,
};
}
Map<String, dynamic> toAbUploadJson() {
return <String, dynamic>{
Map<String, dynamic> toCustomJson({required bool includingHash}) {
var res = <String, dynamic>{
"id": id,
"hash": hash,
"username": username,
"hostname": hostname,
"platform": platform,
"alias": alias,
"tags": tags,
};
if (includingHash) {
res['hash'] = hash;
}
return res;
}
Map<String, dynamic> toGroupCacheJson() {
@@ -80,6 +89,7 @@ class Peer {
Peer({
required this.id,
required this.hash,
required this.password,
required this.username,
required this.hostname,
required this.platform,
@@ -89,12 +99,14 @@ class Peer {
required this.rdpPort,
required this.rdpUsername,
required this.loginName,
this.sameServer,
});
Peer.loading()
: this(
id: '...',
hash: '',
password: '',
username: '...',
hostname: '...',
platform: '...',
@@ -108,6 +120,7 @@ class Peer {
bool equal(Peer other) {
return id == other.id &&
hash == other.hash &&
password == other.password &&
username == other.username &&
hostname == other.hostname &&
platform == other.platform &&
@@ -121,33 +134,38 @@ class Peer {
Peer.copy(Peer other)
: this(
id: other.id,
hash: other.hash,
username: other.username,
hostname: other.hostname,
platform: other.platform,
alias: other.alias,
tags: other.tags.toList(),
forceAlwaysRelay: other.forceAlwaysRelay,
rdpPort: other.rdpPort,
rdpUsername: other.rdpUsername,
loginName: other.loginName,
);
id: other.id,
hash: other.hash,
password: other.password,
username: other.username,
hostname: other.hostname,
platform: other.platform,
alias: other.alias,
tags: other.tags.toList(),
forceAlwaysRelay: other.forceAlwaysRelay,
rdpPort: other.rdpPort,
rdpUsername: other.rdpUsername,
loginName: other.loginName,
sameServer: other.sameServer);
}
enum UpdateEvent { online, load }
typedef GetInitPeers = RxList<Peer> Function();
class Peers extends ChangeNotifier {
final String name;
final String loadEvent;
List<Peer> peers = List.empty(growable: true);
final RxList<Peer>? initPeers;
final GetInitPeers? getInitPeers;
UpdateEvent event = UpdateEvent.load;
static const _cbQueryOnlines = 'callback_query_onlines';
Peers(
{required this.name, required this.initPeers, required this.loadEvent}) {
peers = initPeers ?? [];
{required this.name,
required this.getInitPeers,
required this.loadEvent}) {
peers = getInitPeers?.call() ?? [];
platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) async {
_updateOnlineState(evt);
});
@@ -198,8 +216,8 @@ class Peers extends ChangeNotifier {
void _updatePeers(Map<String, dynamic> evt) {
final onlineStates = _getOnlineStates();
if (initPeers != null) {
peers = initPeers!;
if (getInitPeers != null) {
peers = getInitPeers?.call() ?? [];
} else {
peers = _decodePeers(evt['peers']);
}

View File

@@ -21,24 +21,43 @@ class PeerTabModel with ChangeNotifier {
WeakReference<FFI> parent;
int get currentTab => _currentTab;
int _currentTab = 0; // index in tabNames
List<String> tabNames = [
static const int maxTabCount = 5;
static const String kPeerTabIndex = 'peer-tab-index';
static const String kPeerTabOrder = 'peer-tab-order';
static const String kPeerTabVisible = 'peer-tab-visible';
static const List<String> tabNames = [
'Recent sessions',
'Favorites',
'Discovered',
'Address book',
'Group',
];
final List<IconData> icons = [
static const List<IconData> icons = [
Icons.access_time_filled,
Icons.star,
Icons.explore,
IconFont.addressBook,
Icons.group,
];
final List<bool> _isVisible = List.filled(5, true, growable: false);
List<bool> get isVisible => _isVisible;
List<int> get indexs => List.generate(tabNames.length, (index) => index);
List<int> get visibleIndexs => indexs.where((e) => _isVisible[e]).toList();
List<bool> isEnabled = List.from([
true,
true,
!isWeb,
!(bind.isDisableAb() || bind.isDisableAccount()),
!bind.isDisableAccount(),
]);
final List<bool> _isVisible = List.filled(maxTabCount, true, growable: false);
List<bool> get isVisibleEnabled => () {
final list = _isVisible.toList();
for (int i = 0; i < maxTabCount; i++) {
list[i] = list[i] && isEnabled[i];
}
return list;
}();
final List<int> orders =
List.generate(maxTabCount, (index) => index, growable: false);
List<int> get visibleEnabledOrderedIndexs =>
orders.where((e) => isVisibleEnabled[e]).toList();
List<Peer> _selectedPeers = List.empty(growable: true);
List<Peer> get selectedPeers => _selectedPeers;
bool _multiSelectionMode = false;
@@ -53,7 +72,7 @@ class PeerTabModel with ChangeNotifier {
PeerTabModel(this.parent) {
// visible
try {
final option = bind.getLocalFlutterOption(k: 'peer-tab-visible');
final option = bind.getLocalFlutterOption(k: kPeerTabVisible);
if (option.isNotEmpty) {
List<dynamic> decodeList = jsonDecode(option);
if (decodeList.length == _isVisible.length) {
@@ -67,13 +86,37 @@ class PeerTabModel with ChangeNotifier {
} catch (e) {
debugPrint("failed to get peer tab visible list:$e");
}
// order
try {
final option = bind.getLocalFlutterOption(k: kPeerTabOrder);
if (option.isNotEmpty) {
List<dynamic> decodeList = jsonDecode(option);
if (decodeList.length == maxTabCount) {
var sortedList = decodeList.toList();
sortedList.sort();
bool valid = true;
for (int i = 0; i < maxTabCount; i++) {
if (sortedList[i] is! int || sortedList[i] != i) {
valid = false;
}
}
if (valid) {
for (int i = 0; i < orders.length; i++) {
orders[i] = decodeList[i];
}
}
}
}
} catch (e) {
debugPrint("failed to get peer tab order list: $e");
}
// init currentTab
_currentTab =
int.tryParse(bind.getLocalFlutterOption(k: 'peer-tab-index')) ?? 0;
if (_currentTab < 0 || _currentTab >= tabNames.length) {
int.tryParse(bind.getLocalFlutterOption(k: kPeerTabIndex)) ?? 0;
if (_currentTab < 0 || _currentTab >= maxTabCount) {
_currentTab = 0;
}
_trySetCurrentTabToFirstVisible();
_trySetCurrentTabToFirstVisibleEnabled();
}
setCurrentTab(int index) {
@@ -87,15 +130,13 @@ class PeerTabModel with ChangeNotifier {
if (index >= 0 && index < tabNames.length) {
return translate(tabNames[index]);
}
assert(false);
return index.toString();
}
IconData tabIcon(int index) {
if (index >= 0 && index < tabNames.length) {
if (index >= 0 && index < icons.length) {
return icons[index];
}
assert(false);
return Icons.help;
}
@@ -171,29 +212,54 @@ class PeerTabModel with ChangeNotifier {
}
setTabVisible(int index, bool visible) {
if (index >= 0 && index < _isVisible.length) {
if (index >= 0 && index < maxTabCount) {
if (_isVisible[index] != visible) {
_isVisible[index] = visible;
if (index == _currentTab && !visible) {
_trySetCurrentTabToFirstVisible();
} else if (visible && visibleIndexs.length == 1) {
_trySetCurrentTabToFirstVisibleEnabled();
} else if (visible && visibleEnabledOrderedIndexs.length == 1) {
_currentTab = index;
}
try {
bind.setLocalFlutterOption(
k: 'peer-tab-visible', v: jsonEncode(_isVisible));
k: kPeerTabVisible, v: jsonEncode(_isVisible));
} catch (_) {}
notifyListeners();
}
}
}
_trySetCurrentTabToFirstVisible() {
if (!_isVisible[_currentTab]) {
int firstVisible = _isVisible.indexWhere((e) => e);
if (firstVisible >= 0) {
_currentTab = firstVisible;
_trySetCurrentTabToFirstVisibleEnabled() {
if (!visibleEnabledOrderedIndexs.contains(_currentTab)) {
if (visibleEnabledOrderedIndexs.isNotEmpty) {
_currentTab = visibleEnabledOrderedIndexs.first;
}
}
}
reorder(int oldIndex, int newIndex) {
if (oldIndex < newIndex) {
newIndex -= 1;
}
if (oldIndex < 0 || oldIndex >= visibleEnabledOrderedIndexs.length) {
return;
}
if (newIndex < 0 || newIndex >= visibleEnabledOrderedIndexs.length) {
return;
}
final oldTabValue = visibleEnabledOrderedIndexs[oldIndex];
final newTabValue = visibleEnabledOrderedIndexs[newIndex];
int oldValueIndex = orders.indexOf(oldTabValue);
int newValueIndex = orders.indexOf(newTabValue);
final list = orders.toList();
if (oldIndex != -1 && newIndex != -1) {
list.removeAt(oldValueIndex);
list.insert(newValueIndex, oldTabValue);
for (int i = 0; i < list.length; i++) {
orders[i] = list[i];
}
bind.setLocalFlutterOption(k: kPeerTabOrder, v: jsonEncode(orders));
notifyListeners();
}
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter_hbb/generated_bridge.dart';
import 'native_model.dart' if (dart.library.html) 'web_model.dart';
import 'package:flutter_hbb/generated_bridge.dart'
if (dart.library.html) 'package:flutter_hbb/web/bridge.dart';
final platformFFI = PlatformFFI.instance;
final localeName = PlatformFFI.localeName;

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
@@ -383,7 +382,7 @@ class ServerModel with ChangeNotifier {
// ugly is here, because for desktop, below is useless
await bind.mainStartService();
updateClientState();
if (Platform.isAndroid) {
if (isAndroid) {
WakelockPlus.enable();
}
}
@@ -395,7 +394,7 @@ class ServerModel with ChangeNotifier {
await parent.target?.invokeMethod("stop_service");
await bind.mainStopService();
notifyListeners();
if (!Platform.isLinux) {
if (!isLinux) {
// current linux is not supported
WakelockPlus.disable();
}

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
@@ -60,7 +58,7 @@ class StateGlobal {
_resizeEdgeSize.value =
isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
}
if (!Platform.isMacOS) {
if (!isMacOS) {
_windowBorderWidth.value = v ? 0 : kWindowBorderWidth;
}
}
@@ -84,7 +82,7 @@ class StateGlobal {
final wc = WindowController.fromWindowId(windowId);
wc.setFullscreen(_fullscreen.isTrue).then((_) {
// https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982
if (Platform.isWindows && !v) {
if (isWindows && !v) {
Future.delayed(Duration.zero, () async {
final frame = await wc.getFrame();
final newRect = Rect.fromLTWH(

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
@@ -22,6 +23,7 @@ class UserModel {
UserModel(this.parent);
void refreshCurrentUser() async {
if (bind.isDisableAccount()) return;
final token = bind.mainGetLocalOption(key: 'access_token');
if (token == '') {
await updateOtherModels();
@@ -102,7 +104,10 @@ class UserModel {
// update ab and group status
static Future<void> updateOtherModels() async {
await Future.wait([gFFI.abModel.pullAb(), gFFI.groupModel.pull()]);
await Future.wait([
gFFI.abModel.pullAb(force: ForcePullAb.listAndCurrent, quiet: false),
gFFI.groupModel.pull()
]);
}
Future<void> logOut({String? apiServer}) async {

View File

@@ -3,15 +3,23 @@
import 'dart:convert';
import 'dart:typed_data';
import 'dart:js';
import '../common.dart';
import 'dart:html';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_hbb/web/bridge.dart';
import 'package:flutter_hbb/common.dart';
final List<StreamSubscription<MouseEvent>> mouseListeners = [];
final List<StreamSubscription<KeyboardEvent>> keyListeners = [];
typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
class PlatformFFI {
final _eventHandlers = <String, Map<String, HandleEvent>>{};
final RustdeskImpl _ffiBind = RustdeskImpl();
static String getByName(String name, [String arg = '']) {
return context.callMethod('getByName', [name, arg]);
}
@@ -24,15 +32,89 @@ class PlatformFFI {
static final PlatformFFI instance = PlatformFFI._();
static get localeName => window.navigator.language;
RustdeskImpl get ffiBind => _ffiBind;
static Future<void> init(String appType) async {
isWeb = true;
isWebDesktop = !context.callMethod('isMobile');
context.callMethod('init');
version = getByName('version');
static Future<String> getVersion() async {
throw UnimplementedError();
}
static void setEventCallback(void Function(Map<String, dynamic>) fun) {
bool registerEventHandler(
String eventName, String handlerName, HandleEvent handler) {
debugPrint('registerEventHandler $eventName $handlerName');
var handlers = _eventHandlers[eventName];
if (handlers == null) {
_eventHandlers[eventName] = {handlerName: handler};
return true;
} else {
if (handlers.containsKey(handlerName)) {
return false;
} else {
handlers[handlerName] = handler;
return true;
}
}
}
void unregisterEventHandler(String eventName, String handlerName) {
debugPrint('unregisterEventHandler $eventName $handlerName');
var handlers = _eventHandlers[eventName];
if (handlers != null) {
handlers.remove(handlerName);
}
}
Future<bool> tryHandle(Map<String, dynamic> evt) async {
final name = evt['name'];
if (name != null) {
final handlers = _eventHandlers[name];
if (handlers != null) {
if (handlers.isNotEmpty) {
for (var handler in handlers.values) {
await handler(evt);
}
return true;
}
}
}
return false;
}
String translate(String name, String locale) =>
_ffiBind.translate(name: name, locale: locale);
Uint8List? getRgba(SessionID sessionId, int display, int bufSize) {
throw UnimplementedError();
}
int getRgbaSize(SessionID sessionId, int display) =>
_ffiBind.sessionGetRgbaSize(sessionId: sessionId, display: display);
void nextRgba(SessionID sessionId, int display) =>
_ffiBind.sessionNextRgba(sessionId: sessionId, display: display);
void registerPixelbufferTexture(SessionID sessionId, int display, int ptr) =>
_ffiBind.sessionRegisterPixelbufferTexture(
sessionId: sessionId, display: display, ptr: ptr);
void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
_ffiBind.sessionRegisterGpuTexture(
sessionId: sessionId, display: display, ptr: ptr);
Future<void> init(String appType) async {
context.callMethod('init');
version = getByName('version');
window.onContextMenu.listen((event) {
event.preventDefault();
});
context['onRegisteredEvent'] = (String message) {
try {
Map<String, dynamic> event = json.decode(message);
tryHandle(event);
} catch (e) {
print('json.decode fail(): $e');
}
};
}
void setEventCallback(void Function(Map<String, dynamic>) fun) {
context["onGlobalEvent"] = (String message) {
try {
Map<String, dynamic> event = json.decode(message);
@@ -43,20 +125,20 @@ class PlatformFFI {
};
}
static void setRgbaCallback(void Function(Uint8List) fun) {
context["onRgba"] = (Uint8List? rgba) {
void setRgbaCallback(void Function(int, Uint8List) fun) {
context["onRgba"] = (int display, Uint8List? rgba) {
if (rgba != null) {
fun(rgba);
fun(display, rgba);
}
};
}
static void startDesktopWebListener() {
void startDesktopWebListener() {
mouseListeners.add(
window.document.onContextMenu.listen((evt) => evt.preventDefault()));
}
static void stopDesktopWebListener() {
void stopDesktopWebListener() {
for (var ml in mouseListeners) {
ml.cancel();
}
@@ -67,9 +149,12 @@ class PlatformFFI {
keyListeners.clear();
}
static void setMethodCallHandler(FMethod callback) {}
void setMethodCallHandler(FMethod callback) {}
static Future<bool> invokeMethod(String method, [dynamic arguments]) async {
invokeMethod(String method, [dynamic arguments]) async {
return true;
}
// just for compilation
void syncAndroidServiceAppDirConfigPath() {}
}

View File

@@ -0,0 +1,13 @@
import 'dart:io';
final isAndroid_ = Platform.isAndroid;
final isIOS_ = Platform.isIOS;
final isWindows_ = Platform.isWindows;
final isMacOS_ = Platform.isMacOS;
final isLinux_ = Platform.isLinux;
final isWeb_ = false;
final isWebDesktop_ = false;
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
String get screenInfo_ => '';

View File

@@ -0,0 +1,43 @@
import 'package:flutter_custom_cursor/cursor_manager.dart'
as custom_cursor_manager;
import 'package:flutter_custom_cursor/flutter_custom_cursor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/models/model.dart';
deleteCustomCursor(String key) =>
custom_cursor_manager.CursorManager.instance.deleteCursor(key);
MouseCursor buildCursorOfCache(
CursorModel cursor, double scale, CursorData? cache) {
if (cache == null) {
return MouseCursor.defer;
} else {
final key = cache.updateGetKey(scale);
if (!cursor.cachedKeys.contains(key)) {
// data should be checked here, because it may be changed after `updateGetKey()`
final data = cache.data;
if (data == null) {
return MouseCursor.defer;
}
debugPrint(
"Register custom cursor with key $key (${cache.hotx},${cache.hoty})");
// [Safety]
// It's ok to call async registerCursor in current synchronous context,
// because activating the cursor is also an async call and will always
// be executed after this.
custom_cursor_manager.CursorManager.instance
.registerCursor(custom_cursor_manager.CursorData()
..name = key
..buffer = data
..width = (cache.width * cache.scale).toInt()
..height = (cache.height * cache.scale).toInt()
..hotX = cache.hotx
..hotY = cache.hoty);
cursor.addKey(key);
}
return FlutterCustomMemoryImageCursor(key: key);
}
}

View File

@@ -0,0 +1,41 @@
import 'dart:ffi' hide Size;
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart' as win32;
/// Get windows target build number.
///
/// [Note]
/// Please use this function wrapped with `Platform.isWindows`.
int getWindowsTargetBuildNumber_() {
final rtlGetVersion = DynamicLibrary.open('ntdll.dll').lookupFunction<
Void Function(Pointer<win32.OSVERSIONINFOEX>),
void Function(Pointer<win32.OSVERSIONINFOEX>)>('RtlGetVersion');
final osVersionInfo = _getOSVERSIONINFOEXPointer();
rtlGetVersion(osVersionInfo);
int buildNumber = osVersionInfo.ref.dwBuildNumber;
calloc.free(osVersionInfo);
return buildNumber;
}
/// Get Windows OS version pointer
///
/// [Note]
/// Please use this function wrapped with `Platform.isWindows`.
Pointer<win32.OSVERSIONINFOEX> _getOSVERSIONINFOEXPointer() {
final pointer = calloc<win32.OSVERSIONINFOEX>();
pointer.ref
..dwOSVersionInfoSize = sizeOf<win32.OSVERSIONINFOEX>()
..dwBuildNumber = 0
..dwMajorVersion = 0
..dwMinorVersion = 0
..dwPlatformId = 0
..szCSDVersion = ''
..wServicePackMajor = 0
..wServicePackMinor = 0
..wSuiteMask = 0
..wProductType = 0
..wReserved = 0;
return pointer;
}

View File

@@ -3,6 +3,8 @@ import 'dart:ui' as ui;
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common.dart';
Future<ui.Image> decodeImageFromPixels(
Uint8List pixels,
int width,
@@ -79,6 +81,11 @@ class ImagePainter extends CustomPainter {
paint.filterQuality = FilterQuality.high;
}
}
// It's strange that if (scale < 0.5 && paint.filterQuality == FilterQuality.medium)
// The canvas.drawImage will not work on web
if (isWeb) {
paint.filterQuality = FilterQuality.high;
}
canvas.drawImage(
image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint);
}

View File

@@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/foundation.dart';
@@ -139,7 +138,7 @@ class RustDeskMultiWindowManager {
overrideType: type,
));
}
if (Platform.isMacOS) {
if (isMacOS) {
Future.microtask(() => windowController.show());
}
registerActiveWindow(windowId);
@@ -194,6 +193,7 @@ class RustDeskMultiWindowManager {
bool? forceRelay,
String? switchUuid,
bool? isRDP,
bool? isSharedPassword,
}) async {
var params = {
"type": type.index,
@@ -207,6 +207,9 @@ class RustDeskMultiWindowManager {
if (isRDP != null) {
params['isRDP'] = isRDP;
}
if (isSharedPassword != null) {
params['isSharedPassword'] = isSharedPassword;
}
final msg = jsonEncode(params);
// separate window for file transfer is not supported
@@ -228,6 +231,7 @@ class RustDeskMultiWindowManager {
Future<MultiWindowCallResult> newRemoteDesktop(
String remoteId, {
String? password,
bool? isSharedPassword,
String? switchUuid,
bool? forceRelay,
}) async {
@@ -239,11 +243,12 @@ class RustDeskMultiWindowManager {
password: password,
forceRelay: forceRelay,
switchUuid: switchUuid,
isSharedPassword: isSharedPassword,
);
}
Future<MultiWindowCallResult> newFileTransfer(String remoteId,
{String? password, bool? forceRelay}) async {
{String? password, bool? isSharedPassword, bool? forceRelay}) async {
return await newSession(
WindowType.FileTransfer,
kWindowEventNewFileTransfer,
@@ -251,11 +256,12 @@ class RustDeskMultiWindowManager {
_fileTransferWindows,
password: password,
forceRelay: forceRelay,
isSharedPassword: isSharedPassword,
);
}
Future<MultiWindowCallResult> newPortForward(String remoteId, bool isRDP,
{String? password, bool? forceRelay}) async {
{String? password, bool? isSharedPassword, bool? forceRelay}) async {
return await newSession(
WindowType.PortForward,
kWindowEventNewPortForward,
@@ -264,6 +270,7 @@ class RustDeskMultiWindowManager {
password: password,
forceRelay: forceRelay,
isRDP: isRDP,
isSharedPassword: isSharedPassword,
);
}

View File

@@ -1,8 +1,7 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/common.dart';
enum SystemWindowTheme { light, dark }
@@ -19,7 +18,7 @@ class RdPlatformChannel {
/// Change the theme of the system window
Future<void> changeSystemWindowTheme(SystemWindowTheme theme) {
assert(Platform.isMacOS);
assert(isMacOS);
if (kDebugMode) {
print(
"[Window ${kWindowId ?? 'Main'}] change system window theme to ${theme.name}");
@@ -30,7 +29,7 @@ class RdPlatformChannel {
/// Terminate .app manually.
Future<void> terminate() {
assert(Platform.isMacOS);
assert(isMacOS);
return _osxMethodChannel.invokeMethod("terminate");
}
}

1577
flutter/lib/web/bridge.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
import 'dart:js' as js;
final isAndroid_ = false;
final isIOS_ = false;
final isWindows_ = false;
final isMacOS_ = false;
final isLinux_ = false;
final isWeb_ = true;
final isWebDesktop_ = !js.context.callMethod('isMobile');
final isDesktop_ = false;
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);

View File

@@ -0,0 +1,121 @@
import 'dart:convert';
import 'dart:js' as js;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/models/model.dart' as model;
class CursorData {
final String key;
final String url;
final double hotX;
final double hotY;
final int width;
final int height;
CursorData({
required this.key,
required this.url,
required this.hotX,
required this.hotY,
required this.width,
required this.height,
});
}
/// The cursor manager
class CursorManager {
final Map<String, CursorData> _cursors = <String, CursorData>{};
String latestKey = '';
CursorManager._();
static CursorManager instance = CursorManager._();
Future<void> registerCursor(CursorData data) async {
_cursors[data.key] = data;
}
Future<void> deleteCursor(String key) async {
_cursors.remove(key);
}
Future<void> setSystemCursor(String key) async {
if (latestKey == key) {
return;
}
latestKey = key;
final CursorData? cursorData = _cursors[key];
if (cursorData != null) {
js.context.callMethod('setByName', [
'cursor',
jsonEncode({
'url': cursorData.url,
'hotx': cursorData.hotX.toInt(),
'hoty': cursorData.hotY.toInt(),
})
]);
}
}
}
class FlutterCustomMemoryImageCursor extends MouseCursor {
final String key;
const FlutterCustomMemoryImageCursor({required this.key});
@override
MouseCursorSession createSession(int device) =>
_FlutterCustomMemoryImageCursorSession(this, device);
@override
String get debugDescription =>
objectRuntimeType(this, 'FlutterCustomMemoryImageCursor');
}
class _FlutterCustomMemoryImageCursorSession extends MouseCursorSession {
_FlutterCustomMemoryImageCursorSession(
FlutterCustomMemoryImageCursor cursor, int device)
: super(cursor, device);
@override
FlutterCustomMemoryImageCursor get cursor =>
super.cursor as FlutterCustomMemoryImageCursor;
@override
Future<void> activate() async {
await CursorManager.instance.setSystemCursor(cursor.key);
}
@override
void dispose() {}
}
deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key);
MouseCursor buildCursorOfCache(
model.CursorModel cursor, double scale, model.CursorData? cache) {
if (cache == null) {
return MouseCursor.defer;
} else {
final key = cache.updateGetKey(scale);
if (!cursor.cachedKeys.contains(key)) {
// data should be checked here, because it may be changed after `updateGetKey()`
final data = cache.data;
if (data == null) {
return MouseCursor.defer;
}
debugPrint(
"Register custom cursor with key $key (${cache.hotx},${cache.hoty})");
CursorManager.instance.registerCursor(CursorData(
key: key,
url: 'data:image/rgba;base64,${base64Encode(data)}',
width: (cache.width * cache.scale).toInt(),
height: (cache.height * cache.scale).toInt(),
hotX: cache.hotx,
hotY: cache.hoty));
cursor.addKey(key);
}
return FlutterCustomMemoryImageCursor(key: key);
}
}

View File

@@ -0,0 +1,14 @@
abstract class NativeHandler {
bool onEvent(Map<String, dynamic> evt);
}
class NativeUiHandler extends NativeHandler {
NativeUiHandler._();
static NativeUiHandler instance = NativeUiHandler._();
@override
bool onEvent(Map<String, dynamic> evt) {
throw UnimplementedError();
}
}

View File

@@ -0,0 +1,20 @@
import 'dart:typed_data';
class TextureRgbaRenderer {
Future<int> createTexture(int key) {
throw UnimplementedError();
}
Future<bool> closeTexture(int key) {
throw UnimplementedError();
}
Future<bool> onRgba(
int key, Uint8List data, int height, int width, int strideAlign) {
throw UnimplementedError();
}
Future<int> getTexturePtr(int key) {
throw UnimplementedError();
}
}

View File

@@ -0,0 +1,5 @@
/// No use, for compilation only.
int getWindowsTargetBuildNumber_() {
return 0;
}

View File

@@ -10,9 +10,6 @@ PODS:
- flutter_custom_cursor (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- package_info_plus (0.0.1):
- FlutterMacOS
- path_provider_foundation (0.0.1):
@@ -20,9 +17,9 @@ PODS:
- FlutterMacOS
- screen_retriever (0.0.1):
- FlutterMacOS
- sqflite (0.0.2):
- sqflite (0.0.3):
- Flutter
- FlutterMacOS
- FMDB (>= 2.7.5)
- texture_rgba_renderer (0.0.1):
- FlutterMacOS
- uni_links_desktop (0.0.1):
@@ -49,7 +46,7 @@ DEPENDENCIES:
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
- texture_rgba_renderer (from `Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos`)
- uni_links_desktop (from `Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
@@ -58,10 +55,6 @@ DEPENDENCIES:
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
SPEC REPOS:
trunk:
- FMDB
EXTERNAL SOURCES:
desktop_drop:
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
@@ -82,7 +75,7 @@ EXTERNAL SOURCES:
screen_retriever:
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos
sqflite:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
texture_rgba_renderer:
:path: Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos
uni_links_desktop:
@@ -105,19 +98,18 @@ SPEC CHECKSUMS:
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9
flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2
uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
video_player_avfoundation: 8563f13d8fc8b2c29dc2d09e60b660e4e8128837
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
COCOAPODS: 1.12.1
COCOAPODS: 1.15.2

View File

@@ -23,9 +23,9 @@
/* Begin PBXBuildFile section */
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
7ED2B35F2BB007420099885C /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 7ED2B35E2BB007420099885C /* AppIcon.icns */; };
84010BA8292CF66600152837 /* liblibrustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (Weak, ); }; };
84010BA9292CF68300152837 /* liblibrustdesk.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; };
@@ -62,7 +62,6 @@
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* RustDesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RustDesk.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
@@ -74,6 +73,7 @@
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
7ED2B35E2BB007420099885C /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; name = AppIcon.icns; path = Runner/AppIcon.icns; sourceTree = "<group>"; };
84010BA7292CF66600152837 /* liblibrustdesk.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = liblibrustdesk.dylib; path = ../../target/release/liblibrustdesk.dylib; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
@@ -127,7 +127,7 @@
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
7ED2B35E2BB007420099885C /* AppIcon.icns */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
@@ -253,8 +253,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
7ED2B35F2BB007420099885C /* AppIcon.icns in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

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